A component is the fundamental UI building block in Angular: a TypeScript class carrying application state and logic, paired with an HTML template that renders it and reacts to it. The @Component decorator is what wires that pairing together and tells Angular's compiler how to treat the class.
Anatomy of the @Component Decorator
import { Component, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-inventory-item',
// standalone: true means this component declares its own dependencies
// (see Module 2) instead of relying on an enclosing NgModule.
standalone: true,
imports: [],
// External template/style files — the more common convention for
// components with non-trivial markup or styling.
templateUrl: './inventory-item.component.html',
styleUrl: './inventory-item.component.css',
// OnPush restricts change detection to run only when an @Input()
// reference changes, an event fires inside this component, or an
// Observable/Signal it reads emits — covered in depth in Module 9's
// cheat sheet. It is the enterprise-recommended default.
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class InventoryItemComponent {}For small, tightly-scoped components, inline template and style strings avoid the overhead of extra files. Both styles are equally valid Angular — the choice is a team convention, not a framework requirement.
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-confirm-badge',
standalone: true,
template: `<span class="badge" [class.badge--ok]="confirmed">{{ confirmed ? 'Confirmed' : 'Pending' }}</span>`,
styles: [`
.badge { padding: 0.25rem 0.6rem; border-radius: 999px; font-size: 0.75rem; background: #f3f4f6; color: #374151; }
.badge--ok { background: #dcfce7; color: #166534; }
`],
})
export class ConfirmBadgeComponent {
@Input({ required: true }) confirmed = false;
}| Property | Purpose |
|---|---|
| selector | The custom HTML tag name (or attribute/class selector) this component matches in a parent template |
| standalone | Whether this component supplies its own imports instead of requiring an NgModule declaration |
| templateUrl / template | External file path vs inline string for the component's view |
| styleUrl / styleUrls / styles | External file path(s) vs inline array of CSS strings, scoped to this component by default via ViewEncapsulation.Emulated |
| changeDetection | Default (checks on every zone.js tick) vs OnPush (checks only on input/event/async triggers) — see Module 9 |
The Four Data Binding Techniques
Every binding type below targets a different direction of data flow — component-to-view, view-to-component, or both. Mixing them up (e.g. using property binding where event binding is needed) is the most common early-Angular mistake, so the syntax is worth internalizing precisely.
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { FormsModule } from '@angular/forms';
export interface InventoryItem {
id: string;
name: string;
quantity: number;
lowStockThreshold: number;
}
@Component({
selector: 'app-inventory-item',
standalone: true,
imports: [FormsModule], // FormsModule is required for [(ngModel)]
templateUrl: './inventory-item.component.html',
styleUrl: './inventory-item.component.css',
})
export class InventoryItemComponent {
@Input({ required: true }) item!: InventoryItem;
// Emits the item's id whenever the parent should re-fetch or persist a change.
@Output() quantityChanged = new EventEmitter<{ id: string; quantity: number }>();
draftNote = '';
get isLowStock(): boolean {
return this.item.quantity <= this.item.lowStockThreshold;
}
onIncrement(): void {
this.item = { ...this.item, quantity: this.item.quantity + 1 };
this.quantityChanged.emit({ id: this.item.id, quantity: this.item.quantity });
}
onQuantityInput(event: Event): void {
// Typed narrowing: Event -> HTMLInputElement is not automatic in TS,
// so the DOM element must be explicitly cast before reading .value.
const input = event.target as HTMLInputElement;
const parsed = Number(input.value);
if (Number.isNaN(parsed) || parsed < 0) return;
this.item = { ...this.item, quantity: parsed };
this.quantityChanged.emit({ id: this.item.id, quantity: parsed });
}
}<article class="item" [class.item--low]="isLowStock">
<!-- 1. Interpolation {{ }} — embeds an expression's stringified result into text content -->
<h4>{{ item.name }}</h4>
<p>Current stock: {{ item.quantity }} unit{{ item.quantity === 1 ? '' : 's' }}</p>
<!-- 2. Property binding [] — sets a DOM property or component @Input from a TS expression -->
<input
type="number"
[value]="item.quantity"
[attr.aria-label]="'Quantity for ' + item.name"
[disabled]="item.quantity < 0"
(input)="onQuantityInput($event)"
/>
<!-- 3. Event binding () — wires a DOM event to a typed component method, $event is the native Event -->
<button type="button" (click)="onIncrement()">+1</button>
<!-- 4. Two-way binding [()] — shorthand for [ngModel]="draftNote" + (ngModelChange)="draftNote = $event" -->
<textarea [(ngModel)]="draftNote" placeholder="Add a note..."></textarea>
<small>{{ draftNote.length }}/200 characters</small>
</article>How to Tell the Four Bindings Apart at a Glance
{{ expr }}— Interpolation: text-content only, always reads component → view[prop]="expr"— Property binding: sets a DOM property/@Input from component state, reads component → view(event)="handler($event)"— Event binding: calls a component method on a DOM/custom event, reads view → component[(ngModel)]="field"— Two-way binding (banana-in-a-box syntax): a bundled property binding plus event binding, requiringFormsModuleand keeps view and component perpetually in sync
Typed Event Handling for Structural Browser Inputs
$event inside an Angular template is typed based on context: for a native DOM event binding like (input) or (click), it is the browser's Event (or a more specific subtype like MouseEvent, KeyboardEvent); Angular does not narrow it to HTMLInputElement automatically because event.target is typed as the generic EventTarget | null. The cast in onQuantityInput above (event.target as HTMLInputElement) is required, not optional boilerplate — TypeScript has no way to know which element fired the event without it.
import { Directive, HostListener, Output, EventEmitter } from '@angular/core';
@Directive({
selector: '[appSubmitOnEnter]',
standalone: true,
})
export class SubmitOnEnterDirective {
@Output() enterPressed = new EventEmitter<void>();
@HostListener('keydown', ['$event'])
onKeydown(event: KeyboardEvent): void {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
this.enterPressed.emit();
}
}
}Module 3 Checkpoint
Q1. What two things does [(ngModel)] expand into?
Q2. Why does `event.target as HTMLInputElement` require an explicit cast inside an (input) handler?