# Angular Component Patterns ## Table of Contents - [Model Inputs (Two-Way Binding)](#model-inputs-two-way-binding) - [View Queries](#view-queries) - [Content Queries](#content-queries) - [Dependency Injection in Components](#dependency-injection-in-components) - [Component Communication Patterns](#component-communication-patterns) - [Dynamic Components](#dynamic-components) ## Model Inputs (Two-Way Binding) For two-way binding with `[(value)]` syntax: ```typescript import { Component, model } from "@angular/core"; @Component({ selector: "app-slider", host: { "(input)": "onInput($event)", }, template: ` {{ value() }} `, }) export class Slider { // Model creates both input and output value = model(0); min = input(0); max = input(100); onInput(event: Event) { const target = event.target as HTMLInputElement; this.value.set(Number(target.value)); } } // Usage: ``` Required model: ```typescript value = model.required(); ``` ## View Queries Query elements and components in the template: ```typescript import { Component, viewChild, viewChildren, ElementRef } from "@angular/core"; @Component({ selector: "app-gallery", template: ` `, }) export class Gallery { images = input.required(); // Query single element container = viewChild.required>("container"); // Query single component (optional) firstCard = viewChild(ImageCard); // Query all matching components allCards = viewChildren(ImageCard); } ``` ## Content Queries Query projected content: ```typescript import { Component, contentChild, contentChildren, effect, signal, } from "@angular/core"; @Component({ selector: "app-tabs", template: `
@for (tab of tabs(); track tab.label()) { }
`, }) export class Tabs { // Query all projected Tab children tabs = contentChildren(Tab); // Query single projected element header = contentChild("tabHeader"); activeTab = signal(undefined); constructor() { // Set first tab as active when tabs are available effect(() => { const firstTab = this.tabs()[0]; if (firstTab && !this.activeTab()) { this.activeTab.set(firstTab); } }); } selectTab(tab: Tab) { this.activeTab.set(tab); } } @Component({ selector: "app-tab", template: ``, host: { "[class.active]": "isActive()", "[style.display]": 'isActive() ? "block" : "none"', }, }) export class Tab { label = input.required(); isActive = input(false); } ``` ## Dependency Injection in Components Use `inject()` function instead of constructor injection: ```typescript import { Component, inject } from "@angular/core"; import { Router } from "@angular/router"; @Component({ selector: "app-dashboard", template: `...`, }) export class Dashboard { private router = inject(Router); private userService = inject(User); private config = inject(APP_CONFIG); // Optional injection private analytics = inject(Analytics, { optional: true }); // Self-only injection private localService = inject(Local, { self: true }); navigateToProfile() { this.router.navigate(["/profile"]); } } ``` ## Component Communication Patterns ### Parent to Child (Inputs) ```typescript // Parent @Component({ template: ``, }) export class Parent { parentData = signal({ name: "Test" }); config = { theme: "dark" }; } // Child @Component({ selector: "app-child" }) export class Child { data = input.required(); config = input(); } ``` ### Child to Parent (Outputs) ```typescript // Child @Component({ selector: "app-child", template: ``, }) export class Child { saved = output(); save() { this.saved.emit({ id: 1, name: "Item" }); } } // Parent @Component({ template: ``, }) export class Parent { onSaved(data: Data) { console.log("Saved:", data); } } ``` ### Shared Service Pattern ```typescript // Shared state service @Injectable({ providedIn: "root" }) export class Cart { private items = signal([]); readonly items$ = this.items.asReadonly(); readonly total = computed(() => this.items().reduce((sum, item) => sum + item.price, 0), ); addItem(item: CartItem) { this.items.update((items) => [...items, item]); } removeItem(id: string) { this.items.update((items) => items.filter((i) => i.id !== id)); } } // Component A @Component({ template: `` }) export class Product { private cart = inject(Cart); product = input.required(); add() { this.cart.addItem({ ...this.product(), quantity: 1 }); } } // Component B @Component({ template: `Total: {{ cart.total() }}` }) export class CartSummary { cart = inject(Cart); } ``` ## Dynamic Components Using `@defer` for lazy loading: ```typescript @Component({ template: ` @defer (on viewport) { } @placeholder {
Loading chart...
} @loading (minimum 500ms) { } @error {

Failed to load chart

} `, }) export class Dashboard { chartData = input.required(); } ``` Defer triggers: - `on viewport` - When element enters viewport - `on idle` - When browser is idle - `on interaction` - On user interaction (click, focus) - `on hover` - On mouse hover - `on immediate` - Immediately after non-deferred content - `on timer(500ms)` - After specified delay - `when condition` - When expression becomes true ```typescript @Component({ template: ` @defer (on interaction; prefetch on idle) { } @placeholder { } `, }) export class Post { postId = input.required(); } ``` ## Attribute Directives on Components ```typescript @Directive({ selector: "[appHighlight]", host: { "[style.backgroundColor]": "color()", }, }) export class Highlight { color = input("yellow", { alias: "appHighlight" }); } // Usage on component @Component({ imports: [Highlight], template: ``, }) export class Page {} ``` ## Error Boundaries ```typescript @Component({ selector: "app-error-boundary", template: ` @if (hasError()) {

Something went wrong

} @else { } `, }) export class ErrorBoundary { hasError = signal(false); private errorHandler = inject(ErrorHandler); retry() { this.hasError.set(false); } } ```