A component owns one view boundary: a TypeScript class, template, selector, host element, optional styles, and declared dependencies. This lesson builds the full component contract, from composition and selectors to signal inputs, typed outputs, models, queries, dynamic views, and cleanup.
An Angular component owns a view boundary. Its TypeScript class holds behavior and state, its template describes the DOM, and its selector tells Angular where to create that view.
The object passed to @Component is component metadata. Current Angular components are standalone by default, so a component normally imports its own template dependencies instead of belonging to an NgModule.
| Metadata | Responsibility |
|---|---|
| selector | Defines the compile-time CSS selector used to create the component. |
| imports | Makes standalone components, directives, pipes, or NgModules available to this template. |
| template / templateUrl | Provides inline HTML or one external template file. templateUrl is relative to the component file. |
| styles / styleUrl / styleUrls | Provides inline styles or one or more component stylesheet files. |
| providers | Creates providers available from the component host and its descendants. |
| viewProviders | Creates providers visible to the component view but not to projected content. |
| host | Declares properties, attributes, classes, styles, and events on the host element. |
| changeDetection | Selects the Default or OnPush change-detection strategy. |
| encapsulation | Selects Emulated, ShadowDom, or None style encapsulation. |
| standalone | Defaults to true in current Angular. Set false only for a component declared by an NgModule. |
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { CurrencyPipe } from '@angular/common';
@Component({
selector: 'app-price-badge',
imports: [CurrencyPipe],
template: `
<strong>{{ label() }}</strong>
<span>{{ price() | currency }}</span>
`,
styles: `:host { display: inline-flex; gap: 0.5rem; }`,
host: { '[class.sale]': 'onSale()' },
changeDetection: ChangeDetectionStrategy.OnPush
})
export class PriceBadge {
label = signal('Keyboard');
price = signal(49);
onSale = signal(true);
}
Keyboard $49.00
The component imports only the pipe its template uses. The host binding adds the sale class to <app-price-badge>, while OnPush keeps the view compatible with signal-driven updates.
Lifecycle APIs are timing boundaries for work that cannot be expressed as normal state derivation. A component page needs the ownership rule; the dedicated lifecycle lesson explains every hook and render callback in sequence.
Prefer signals and computed state for data flow. Use lifecycle APIs for input-transition logic, initialized content or view access, post-render DOM integration, and resource cleanup.
| Boundary | Component responsibility |
|---|---|
| constructor | Inject dependencies and initialize fields; inputs and the component view are not ready. |
| ngOnChanges / ngOnInit | Respond to assigned inputs or perform one-time setup after initial inputs. |
| ngDoCheck | Implement an exceptional custom check only after measuring the cost and ruling out reactive state. |
| Content hooks | Read initialized projected-content queries without mutating already-checked state. |
| View hooks | Read initialized view queries; avoid repeated expensive work in checked hooks. |
| afterNextRender | Perform one-time browser DOM measurement or integration after the next complete render. |
| afterEveryRender | Synchronize a non-reactive DOM integration after every complete render; keep the callback small. |
| ngOnDestroy / DestroyRef | Stop timers, listeners, observers, sockets, and subscriptions that could outlive the component. |
import { Component, DestroyRef, inject, signal } from '@angular/core';
@Component({
selector: 'app-clock',
template: `<time>{{ now() }}</time>`
})
export class Clock {
private destroyRef = inject(DestroyRef);
now = signal(new Date().toLocaleTimeString());
constructor() {
const timer = window.setInterval(
() => this.now.set(new Date().toLocaleTimeString()),
1000
);
this.destroyRef.onDestroy(() => window.clearInterval(timer));
}
}
The interval belongs to this component instance. DestroyRef keeps the teardown beside the setup and prevents the timer from continuing after the view is removed.
A parent composes a child by importing the child class and placing an element that matches its selector in the parent template. Angular creates one child instance for each matching host element.
The host element is the element matched by the selector. The DOM produced from the component template is its view. Repeating this relationship creates the application component tree, which also shapes injector and query boundaries.
| Composition choice | Use it when |
|---|---|
| Standalone import | The dependency is standalone and the component template uses it directly. |
| NgModule import | Existing compatibility code exports a non-standalone declaration. |
| ApplicationConfig provider | A router, HTTP client, hydration feature, or application-wide service belongs at bootstrap. |
| Route-level lazy import | The component should load only when a route is activated. |
import { Component } from '@angular/core';
import { PriceBadge } from './price-badge';
@Component({
selector: 'app-catalog-page',
imports: [PriceBadge],
template: `
<h1>Catalog</h1>
<app-price-badge />
`
})
export class CatalogPage {}
The import makes PriceBadge available to this template. The app-price-badge element is the child host, and the PriceBadge template renders inside it.
Inputs carry parent-owned data into a child, outputs report child events to a parent, and model creates an intentionally writable two-way contract. Keep ownership visible: most values should flow in through inputs and changes should flow out as events.
Angular records input, output, and model declarations statically. Declare them in property initializers; do not try to add component APIs dynamically at runtime.
| API | Contract |
|---|---|
| value = input(0) | Creates a read-only InputSignal with an inferred default value. |
| value = input<number>() | Creates an optional input whose value may be undefined. |
| value = input.required<number>() | Requires the parent template to provide the input at build time. |
| label = input('', { transform: trimString }) | Normalizes an assigned value with a statically analyzable transform. |
| value = input(0, { alias: 'sliderValue' }) | Uses sliderValue as the public template binding name. |
| saved = output<Order>() | Creates a typed OutputEmitterRef; emit with saved.emit(order). |
| value = model(0) | Creates writable component state and a valueChange output for [(value)] binding. |
import { Component, input, output } from '@angular/core';
@Component({
selector: 'app-greeting',
template: `
<p>Hello, {{ name() }}!</p>
<button (click)="greet.emit(name())">Say Hi</button>
`
})
export class GreetingComponent {
name = input.required<string>();
greet = output<string>();
}
name is read-only inside the child. Clicking the button emits a typed event; it does not mutate parent state directly.
import { Component } from '@angular/core';
import { GreetingComponent } from './greeting.component';
@Component({
selector: 'app-root',
imports: [GreetingComponent],
template: `
<app-greeting
name="Angular 22"
(greet)="onGreet($event)"
/>
<p>{{ message }}</p>
`
})
export class AppComponent {
message = '';
onGreet(name: string) {
this.message = \`Greeted: \${name}\`;
}
}
The literal sets the required name input. The event binding receives the emitted string as $event and lets the parent decide how its state changes.
// quantity-stepper.ts
import { Component, model } from '@angular/core';
@Component({
selector: 'app-quantity-stepper',
template: `
<button (click)="quantity.update(value => Math.max(1, value - 1))">-</button>
<output>{{ quantity() }}</output>
<button (click)="quantity.update(value => value + 1)">+</button>
`
})
export class QuantityStepper {
quantity = model(1);
}
// parent template
// <app-quantity-stepper [(quantity)]="cartQuantity" />
model allows the child to update quantity and emits quantityChange for the parent binding. Use this only when two-way ownership is the intended component contract.
Angular matches component selectors statically when it compiles a template. One element can match only one component, selector matching is case-sensitive, and changing classes or attributes later does not create a different component instance.
The matched DOM element is the component host. Use host metadata when the component must expose native semantics, accessibility attributes, classes, styles, or event behavior on that element.
| Selector | Match |
|---|---|
| app-product-card | A custom <app-product-card> element; this is the normal component selector. |
| button[app-save] | A native button carrying the app-save attribute. |
| .app-panel | An element carrying the app-panel class; use sparingly because element selectors are clearer. |
| [dropzone]:not(textarea) | An element with dropzone except a textarea. |
| app-card, [app-card] | Either selector in a comma-separated selector list. |
import { Component, input, output } from '@angular/core';
@Component({
selector: 'button[app-save]',
template: `{{ label() }}`,
host: {
'type': 'button',
'[attr.aria-busy]': 'saving()',
'[disabled]': 'saving()',
'(click)': 'save.emit()'
}
})
export class SaveButton {
label = input('Save');
saving = input(false);
save = output<void>();
}
The attribute selector preserves native button semantics. Host metadata reflects busy and disabled state on the host and emits one component event from the native click.
A component can reference children in its own view, inspect content supplied by a parent, project that content into named slots, or create a component type dynamically. These APIs cross a view boundary, so use the narrowest one that matches the ownership relationship.
| API | Boundary |
|---|---|
| viewChild / viewChildren | Returns reactive queries for matching children in the component view. |
| contentChild / contentChildren | Returns reactive queries for matching projected content. |
| <ng-content> / select | Projects all caller markup or routes matching content into named slots. |
| NgComponentOutlet | Renders a component type declaratively in a template. |
| ViewContainerRef.createComponent | Creates and inserts a component programmatically at a view-container location. |
| createComponent | Creates a component with an explicit host and environment when lower-level control is required. |
import { Component, ElementRef, afterNextRender, viewChild } from '@angular/core';
@Component({
selector: 'app-search-box',
template: `<input #searchInput type="search" aria-label="Search" />`
})
export class SearchBox {
searchInput = viewChild.required<ElementRef<HTMLInputElement>>('searchInput');
constructor() {
afterNextRender(() => this.searchInput().nativeElement.focus());
}
}
The query belongs to the component view. afterNextRender waits until Angular has committed the browser DOM before focusing the required element.
import { NgComponentOutlet } from '@angular/common';
import { Component } from '@angular/core';
import { CompactSummary } from './compact-summary';
@Component({
selector: 'app-summary-host',
imports: [NgComponentOutlet],
template: `<ng-container *ngComponentOutlet="summaryType" />`
})
export class SummaryHost {
summaryType = CompactSummary;
}
NgComponentOutlet keeps a runtime component choice declarative. Use ViewContainerRef only when code must control the insertion location or lifecycle directly.
A useful component has one coherent view responsibility and a small public contract. Split by ownership and behavior, not merely because a file crossed an arbitrary line count.
| Need | Choose |
|---|---|
| Own a rendered view | Component |
| Add behavior to an existing host without owning a view | Directive |
| Share data access or business coordination | Injectable service |
| Transform a value for display | Pipe or computed state |
| Reuse caller-provided markup | Content projection |
Apply the page outcome
0 of 2 completed
Angular cannot reliably decide which component should own that element, and compilation normally fails with a selector-collision error. Treat selectors as public names: give application components a consistent prefix and keep library prefixes distinct.
input.required<string>() defines a mandatory component contract rather than a signal with a default value. Angular expects every use of app-greeting to provide name, either as a literal such as name="Angular" or as a property binding such as [name]="currentName".
ngAfterViewChecked can run after many change-detection passes, not only when the particular child or DOM value your code cares about changes. Expensive calculations, layout measurements, network calls, or unconditional state updates placed there may execute repeatedly.
Explore 500+ free tutorials across 20+ languages and frameworks.