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

inventory-item.component.tstypescript
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.

confirm-badge.component.ts (inline variant)typescript
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;
}
PropertyPurpose
selectorThe custom HTML tag name (or attribute/class selector) this component matches in a parent template
standaloneWhether this component supplies its own imports instead of requiring an NgModule declaration
templateUrl / templateExternal file path vs inline string for the component's view
styleUrl / styleUrls / stylesExternal file path(s) vs inline array of CSS strings, scoped to this component by default via ViewEncapsulation.Emulated
changeDetectionDefault (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.

inventory-item.component.ts (full class)typescript
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 });
  }
}
inventory-item.component.htmlhtml
<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, requiring FormsModule and 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.

keyboard-shortcut.directive.tstypescript
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

  1. Q1. What two things does [(ngModel)] expand into?

  2. Q2. Why does `event.target as HTMLInputElement` require an explicit cast inside an (input) handler?