Week 4: Component Fundamentals & Templates

This is the week it stops being abstract. You've bootstrapped an app and understand the mental model — now you'll build real components with real inputs, outputs, and templates, using the exact APIs you'll reach for in every component for the rest of the course.

Phase 1 of 7 Week 4 of 26 ~4 Hours Hands-on Exercise Included

By the end of this week, you'll be able to

  • Build components with typed, signal-based inputs and outputs
  • Read and write Angular templates fluently
  • Hook into a component's lifecycle the modern, signal-first way

1. Component Decorator Options

You've seen @Component a few times already; this week you learn it properly. Three options account for almost everything you'll configure day to day:

rating-stars.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-rating-stars',
  templateUrl: './rating-stars.html',
  styleUrl: './rating-stars.css',
})
export class RatingStars {}
  • selector — the HTML tag this component is used as, e.g. <app-rating-stars>
  • templateUrl / template — an external file, or an inline string for very small components
  • styleUrl / styleUrls — one or more stylesheets scoped to just this component (more on that scoping in Week 7)

Small, purely presentational components are often written with an inline template instead of a separate file — a judgment call you'll get a feel for as you write more of them.

2. Interpolation, Property & Event Binding

Three binding syntaxes cover almost every template you'll write:

example.html
<!-- Interpolation: render a value as text -->
<h2>{{ product.name }}</h2>

<!-- Property binding: set a DOM/component property -->
<img [src]="product.imageUrl" [alt]="product.name" />
<app-badge [count]="cartItemCount" />

<!-- Event binding: call a method when something happens -->
<button (click)="addToCart(product)">Add to cart</button>

<!-- Class and style bindings -->
<div [class.out-of-stock]="product.stock === 0"
     [style.opacity]="product.stock === 0 ? 0.5 : 1">
  {{ product.name }}
</div>
Attribute vs. property

[src]="..." sets the DOM property, not the HTML attribute — that distinction rarely matters, except for a handful of attributes (like custom data-* ones) where you need [attr.data-id]="..." instead. Angular defaults to property binding because it's faster and type-checked.

3. input(), output() & model()

These three functions are how a component receives data, emits events, and supports two-way binding — the signal-based replacement for the older @Input()/@Output() decorators.

rating-stars.ts
import { Component, input, output, model } from '@angular/core';

@Component({
  selector: 'app-rating-stars',
  templateUrl: './rating-stars.html',
})
export class RatingStars {
  // Required input -- consumer MUST provide a value
  max = input.required<number>();

  // Optional input with a default
  readOnly = input(false);

  // Output: an event this component emits
  rated = output<number>();

  // model(): two-way bindable signal -- both an input and an output, paired
  value = model(0);

  select(stars: number) {
    if (this.readOnly()) return;
    this.value.set(stars);
    this.rated.emit(stars);
  }
}
using it — parent.html
<app-rating-stars
  [max]="5"
  [(value)]="productRating"
  (rated)="onRated($event)"
/>

[(value)] is Angular's "banana in a box" syntax — shorthand for binding [value] and listening to (valueChange) at once. model() generates that valueChange output for you automatically; you never write it by hand.

4. Template Reference Variables

A #name on any element gives you a handle to it — the DOM element, or the component/directive instance if there is one — usable anywhere else in that same template.

search.html
<input #searchBox type="text" (keyup.enter)="search(searchBox.value)" />
<button (click)="search(searchBox.value)">Search</button>

This is a lightweight escape hatch for simple cases — reading an input's current value without wiring up a full FormControl (which you'll learn properly in Week 12). For anything beyond "read this one value," reactive forms are the better tool.

5. ng-template & ng-container

ng-container groups elements — for a binding, or just for organization — without adding an extra element to the rendered DOM. It's invisible at runtime.

ng-container.html
<ng-container [ngTemplateOutlet]="header"></ng-container>

<!-- No wrapping <div> in the rendered output -- useful when you need
     a binding target but a real DOM element would break your CSS layout -->

ng-template defines a block of markup that isn't rendered by default — something else decides when (or whether) to render it. You'll meet it again this way in Week 11, where @defer uses a similar "placeholder content" idea for lazy-loaded UI:

ng-template.html
@for (item of items(); track item.id) {
  <li>{{ item.name }}</li>
} @empty {
  <ng-template #noItems>
    <li>No items yet.</li>
  </ng-template>
}

In practice, you'll write @if/@for's own @empty and @else blocks (Week 6) far more often than a raw ng-template — but recognizing it matters, because libraries and older codebases still use it directly.

6. Lifecycle Hooks & Signal-Based Alternatives

Angular calls specific methods on your component class at specific moments. The three you'll use most:

  • ngOnInit() — once, after the component's inputs are first set
  • ngOnChanges(changes) — whenever an @Input()-decorated property changes
  • ngOnDestroy() — right before the component is removed, for cleanup (unsubscribing, clearing timers)
classic lifecycle hooks
import { Component, OnInit, OnDestroy } from '@angular/core';

export class SearchBox implements OnInit, OnDestroy {
  private intervalId?: ReturnType<typeof setInterval>;

  ngOnInit() {
    this.intervalId = setInterval(() => this.refresh(), 30_000);
  }

  ngOnDestroy() {
    clearInterval(this.intervalId);
  }

  refresh() { /* ... */ }
}

With signals, ngOnChanges specifically is often unnecessary — effect() (Week 5) reacts to a signal input changing directly, without the boilerplate of comparing an old and new value yourself:

signal-based alternative
import { Component, input, effect } from '@angular/core';

export class SearchBox {
  query = input('');

  constructor() {
    effect(() => {
      const q = this.query(); // re-runs automatically whenever `query` changes
      console.log('Query changed to', q);
    });
  }
}

ngOnInit and ngOnDestroy remain useful and aren't going anywhere — it's specifically ngOnChanges that signals make largely redundant.

7. Hands-on Exercise

Hands-on

Build a reusable rating-stars component

Turn the code from Section 3 into a fully working, reusable component.

Requirements:

  1. Create RatingStars with a required max input, a readOnly input defaulting to false, a two-way value via model(), and a rated output.
  2. In the template, render max star icons (plain / characters are fine) and highlight however many match the current value.
  3. Clicking a star updates value and emits rated — unless readOnly is true, in which case clicks do nothing.
  4. In a parent component, bind [(value)] to a signal and display the live value next to the component, proving the two-way binding works both directions (also try setting the parent's signal programmatically and confirm the stars update).
  5. Render a second, readOnly instance of the same component to prove one component handles both modes.
Hint

To render "max stars, some filled," an @for loop over an array built with Array.from({ length: this.max() }) works well — you'll get proper practice with the new control-flow syntax a couple weeks early.

8. Knowledge Check

Four quick questions. Expand each to check your answer.

Q1

What does [(value)]="productRating" actually expand to?

[value]="productRating" plus (valueChange)="productRating = $event". It's shorthand for a property binding and an event binding to a conventionally-named xChange output at once — model() is what generates that paired output automatically.

Q2

When would you need [attr.data-id]="..." instead of a plain property binding?

Whenever there's no matching DOM property to bind to — custom data-* attributes, ARIA attributes, and SVG attributes are the common cases. Angular's default [x]="..." binds a DOM property; [attr.x]="..." explicitly sets the HTML attribute instead.

Q3

What's the key difference between ng-container and ng-template?

ng-container renders its contents immediately, just without adding a wrapping DOM element. ng-template's contents render not at all by default — something else (a structural directive, @defer, a template outlet) has to explicitly decide to render them.

Q4

Why does effect() often replace the need for ngOnChanges, but not ngOnInit or ngOnDestroy?

ngOnChanges exists specifically to react to input changes — which effect() does automatically and more precisely, by tracking exactly which signals it reads. ngOnInit (run setup once) and ngOnDestroy (run cleanup once) aren't about reacting to changing values at all, so an effect isn't a substitute for either.