Skip to main content

πŸ”— Templates and Data Binding

A template is where a component's data becomes something you can see and click. This lesson covers the four kinds of binding, Angular's new built-in control flow (@if, @for, @switch), attribute directives, template reference variables, and pipes β€” the full vocabulary for building dynamic views.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Use interpolation and property binding to show data in the view
  • Respond to user actions with event binding, and sync form fields with two-way binding
  • Render conditionally and in loops with the new @if / @for / @switch control flow
  • Apply class and style bindings and reference elements with template variables
  • Transform displayed values with built-in and custom pipes

Estimated Time: 35–45 minutes  β€’  Difficulty: Intermediate

Hands-on: Build a live-filtered task list using every binding type.

In This Lesson

The Four Kinds of Binding

A template is HTML plus Angular's binding syntax. Data binding is the machinery that keeps the component's data and the DOM in sync, so you never write manual document.querySelector updates. There are four flavors, distinguished entirely by their punctuation:

The four kinds of data binding Interpolation and property binding flow from component to DOM; event binding flows from DOM to component; two-way binding flows both directions. Component state & logic DOM the view {{ }} & [prop] β€” data out (event) β€” actions in [(ngModel)] β€” both directions at once
Figure 1 β€” Interpolation and property binding push data to the view; event binding brings actions back; two-way binding does both. The syntax tells you the direction at a glance.
BindingSyntaxDirection
Interpolation{{ value }}Component β†’ View
Property binding[prop]="value"Component β†’ View
Event binding(event)="handler()"View β†’ Component
Two-way binding[(ngModel)]="value"Both

Interpolation

Interpolation is the simplest binding: double curly braces drop a value into the text of the page. With signals, you call the signal to read it.

<h1>{{ title() }}</h1>
<p>Hello, {{ name() }}</p>

<!-- Expressions are allowed -->
<p>1 + 1 = {{ 1 + 1 }}</p>
<p>{{ hero().firstName + ' ' + hero().lastName }}</p>
<p>Status: {{ isActive() ? 'Active' : 'Inactive' }}</p>

<!-- Method calls work too (keep them cheap) -->
<p>{{ getGreeting() }}</p>

Angular evaluates the expression, converts the result to a string, and updates that text node in the DOM. Template expressions are deliberately restricted β€” for safety and performance they cannot:

  • Assign to variables ({{ x = 1 }} is disallowed)
  • Use new, ++, or --
  • Chain with ; or ,
  • Reach global objects like window or document
πŸ’‘ Analogy: Interpolation is a read-only display panel wired to a sensor. It shows the current value but never changes it β€” the flow is one way, screen only.

⚠️ Keep template expressions light

An expression like {{ getGreeting() }} runs on every change detection pass. Avoid heavy computation or anything with side effects in templates β€” move that work into the class, or better, into a computed() signal that recalculates only when its inputs change.

Property, Class & Style Binding

Property binding sets a DOM property to a component value using square brackets. Use it whenever the value isn't a plain string β€” booleans, numbers, objects β€” or when binding to a component's input.

<img [src]="hero().imageUrl" [alt]="hero().name" />
<button [disabled]="isSaving()">Save</button>
<input [value]="username()" />

<!-- Passing a value to a child component's input -->
<app-hero-detail [hero]="selectedHero()"></app-hero-detail>

πŸ“– Property vs. attribute

HTML attributes initialize the page; DOM properties hold the live, current state. Property binding sets the property. For the rare cases with no matching property β€” ARIA, SVG, colspan β€” use attribute binding: [attr.aria-label]="label()".

Class binding

<!-- Toggle one class from a boolean -->
<div [class.active]="isActive()">Highlighted when active</div>

<!-- Multiple classes from an object -->
<div [ngClass]="{ active: isActive(), disabled: !isEnabled() }"></div>

Style binding

<!-- One style, with an optional unit suffix -->
<div [style.color]="textColor()">Colored</div>
<div [style.width.px]="width()">Sized in pixels</div>

<!-- Multiple styles from an object -->
<div [ngStyle]="{ color: textColor(), 'font-weight': isBold() ? 'bold' : 'normal' }"></div>
πŸ’‘ Analogy: If interpolation is a display, property binding is a dial that gets turned to a specific position by a remote signal. Class and style bindings are the wardrobe: class binding picks which pre-made outfit to wear, style binding adjusts individual details like color and size.

Event Binding

Event binding listens for user actions β€” clicks, keystrokes, focus changes β€” using parentheses around the event name. The handler is a statement that usually calls a method on the class.

<button (click)="save()">Save</button>
<input (input)="onInput($event)" />
<div (mouseenter)="hover.set(true)" (mouseleave)="hover.set(false)">Hover me</div>
<form (submit)="onSubmit($event)">...</form>

The special $event variable carries the DOM event object so your handler can inspect it:

onSubmit(event: Event): void {
  event.preventDefault();          // stop the browser's default reload
  this.submitForm();
}

onKeyUp(event: KeyboardEvent): void {
  if (event.key === 'Enter') {
    this.search();
  }
}

Unlike Vue, Angular has no built-in event modifiers (such as .prevent). You handle those explicitly with event.preventDefault() or event.stopPropagation() inside your method β€” a small amount of extra code in exchange for being plain TypeScript.

πŸ’‘ Analogy: Event binding is a set of sensors and alarms. When something happens β€” a button press, a mouse moving over an area β€” the alarm fires and dispatches the response team (your method) to handle it.

Two-Way Binding

Two-way binding combines property binding and event binding so a form field and a value stay in sync in both directions. The classic tool is [(ngModel)] β€” nicknamed the "banana in a box" for its [()] shape.

import { Component, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-greeter',
  standalone: true,
  imports: [FormsModule],  // ngModel lives in FormsModule
  template: `
    <input [(ngModel)]="name" placeholder="Your name" />
    <p>Hello, {{ name }}!</p>
  `,
})
export class GreeterComponent {
  name = 'Ada';
}

Under the hood, the banana-in-a-box is just shorthand for a property binding plus an event binding:

<!-- This… -->
<input [(ngModel)]="name" />

<!-- …is exactly this -->
<input [ngModel]="name" (ngModelChange)="name = $event" />
flowchart LR A[Component value] -->|"[ngModel]"| B[Input element] B -->|"(ngModelChange)"| A

You can give your own components two-way bindable properties with the model() function, which creates a writable signal input that automatically emits a matching ...Change event:

import { Component, model } from '@angular/core';

@Component({
  selector: 'app-counter',
  standalone: true,
  template: `
    <button (click)="count.set(count() - 1)">-</button>
    <span>{{ count() }}</span>
    <button (click)="count.set(count() + 1)">+</button>
  `,
})
export class CounterComponent {
  count = model(0);   // enables [(count)] on the parent
}
<app-counter [(count)]="quantity"></app-counter>
πŸ’‘ Analogy: Two-way binding is a smart thermostat. It shows the current temperature (value β†’ view) and, when you nudge the dial, sends the new setting back to the furnace (view β†’ value) β€” one control, both directions.

Control Flow: @if, @for, @switch

Angular 17 introduced a built-in control flow block syntax that replaces the older structural directives (*ngIf, *ngFor, *ngSwitch). It reads like ordinary code, needs no imports, and is faster. This is the syntax to learn today.

⚠️ Old vs. new

You'll still see *ngIf="cond" and *ngFor="let x of items" in existing projects β€” they continue to work. But new code should use the @if and @for blocks below. They're the current recommendation and the default in generated code.

@if / @else

@if (heroes().length > 0) {
  <p>There are {{ heroes().length }} heroes.</p>
} @else if (loading()) {
  <p>Loading…</p>
} @else {
  <p>No heroes found.</p>
}

@for

The @for block requires a track expression so Angular can identify each item and reuse DOM nodes efficiently. It also offers a handy @empty block.

<ul>
  @for (hero of heroes(); track hero.id) {
    <li>{{ hero.name }}</li>
  } @empty {
    <li>The roster is empty.</li>
  }
</ul>

<!-- Contextual variables: $index, $first, $last, $even, $odd, $count -->
<ul>
  @for (hero of heroes(); track hero.id; let i = $index) {
    <li [class.first]="$first">{{ i + 1 }}. {{ hero.name }}</li>
  }
</ul>

πŸ’‘ Why track matters

Without a stable key, re-rendering a list forces Angular to tear down and rebuild every row. Tracking by a unique id lets it move and reuse existing DOM nodes, which keeps long lists fast and preserves things like input focus and animations.

@switch

@switch (hero().type) {
  @case ('warrior') {
    <app-warrior-detail [hero]="hero()" />
  }
  @case ('mage') {
    <app-mage-detail [hero]="hero()" />
  }
  @default {
    <app-unknown-detail [hero]="hero()" />
  }
}

Attribute directives still apply

Control flow decides whether and how many times elements appear. Attribute directives like ngClass and ngStyle instead modify existing elements, as we saw in the property-binding section. You can also write your own to encapsulate reusable behavior:

import { Directive, ElementRef, HostListener, inject, input } from '@angular/core';

@Directive({
  selector: '[appHighlight]',
  standalone: true,
})
export class HighlightDirective {
  private el = inject(ElementRef);
  color = input('yellow', { alias: 'appHighlight' });

  @HostListener('mouseenter') onEnter() {
    this.el.nativeElement.style.backgroundColor = this.color();
  }
  @HostListener('mouseleave') onLeave() {
    this.el.nativeElement.style.backgroundColor = '';
  }
}

// Usage:  <p appHighlight="lightblue">Hover me</p>
πŸ’‘ Analogy: Control flow blocks are the foreman deciding which sections of a building to construct and how many floors to repeat. Attribute directives are the finishing crew who change the appearance of walls that already exist.

Template Variables & Pipes

Template reference variables

A template reference variable, declared with #, gives you a handle on an element, component, or directive from elsewhere in the same template.

<input #nameInput placeholder="Name" />
<button (click)="greet(nameInput.value)">Greet</button>

<!-- Reference to a child component, then call its method -->
<app-counter #counter></app-counter>
<button (click)="counter.reset()">Reset counter</button>

Reference variables are scoped to the template they're declared in β€” you can't reach them from the class or another template.

Pipes

Pipes transform a value for display without changing the underlying data. Apply one with the | operator; pass arguments after a colon; chain several together.

<p>{{ name() | uppercase }}</p>
<p>{{ price() | currency:'USD' }}</p>
<p>{{ ratio() | percent:'1.0-1' }}</p>
<p>{{ joined() | date:'mediumDate' }}</p>
<p>{{ data() | json }}</p>

<!-- Chained: format the date, then uppercase it -->
<p>{{ joined() | date:'fullDate' | uppercase }}</p>

When the built-ins aren't enough, write your own standalone pipe:

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'truncate',
  standalone: true,
})
export class TruncatePipe implements PipeTransform {
  transform(value: string, limit = 100, trail = '…'): string {
    if (!value || value.length <= limit) return value ?? '';
    return value.substring(0, limit) + trail;
  }
}

// In a component's imports, then:  {{ longText() | truncate:50 }}

πŸ“– Pure vs. impure pipes

By default pipes are pure: they re-run only when their input reference changes, which is fast. An impure pipe (pure: false) re-runs on every change detection cycle β€” occasionally necessary, but a performance risk. Prefer pure pipes, and prefer a computed() signal over an impure pipe when you can.

πŸ’‘ Analogy: Pipes are kitchen appliances on a countertop. Raw ingredients (data) go in, a nicely prepared version comes out β€” and the pantry (your actual data) is never altered.

Hands-on Exercise

πŸ‹οΈ Build a Live-Filtered Task List

Objective: Combine every binding type and the new control flow into one small, working component.

Instructions:

  1. Create a standalone TaskListComponent with a signal array of tasks (each { id, title, done }) and a filter signal that can be 'all', 'active', or 'done'.
  2. Add a two-way-bound <select> to change the filter.
  3. Use a computed() signal to derive the visible tasks from the filter.
  4. Render the visible tasks with @for (with track and an @empty block). Show a count above the list using a pipe or interpolation.
  5. Add a checkbox per task (event binding) that toggles its done state, and use a class binding to strike through completed tasks.
πŸ’‘ Hint

Import FormsModule for [(ngModel)] on the select. For the derived list, filter inside computed(() => ...) so it recalculates whenever the tasks or the filter change. Toggle done with tasks.update(...).

βœ… Example solution
import { Component, signal, computed } from '@angular/core';
import { FormsModule } from '@angular/forms';

interface Task { id: number; title: string; done: boolean; }

@Component({
  selector: 'app-task-list',
  standalone: true,
  imports: [FormsModule],
  template: `
    <h2>Tasks ({{ visible().length }})</h2>

    <label>
      Show:
      <select [(ngModel)]="filter">
        <option value="all">All</option>
        <option value="active">Active</option>
        <option value="done">Done</option>
      </select>
    </label>

    <ul>
      @for (task of visible(); track task.id) {
        <li [class.done]="task.done">
          <input type="checkbox" [checked]="task.done"
                 (change)="toggle(task.id)" />
          {{ task.title }}
        </li>
      } @empty {
        <li>Nothing to show.</li>
      }
    </ul>
  `,
  styles: [`.done { text-decoration: line-through; opacity: 0.6; }`],
})
export class TaskListComponent {
  filter = signal<'all' | 'active' | 'done'>('all');
  tasks = signal<Task[]>([
    { id: 1, title: 'Learn interpolation', done: true },
    { id: 2, title: 'Learn @for', done: false },
    { id: 3, title: 'Build the exercise', done: false },
  ]);

  visible = computed(() => {
    const f = this.filter();
    return this.tasks().filter(t =>
      f === 'all' ? true : f === 'done' ? t.done : !t.done);
  });

  toggle(id: number): void {
    this.tasks.update(list =>
      list.map(t => t.id === id ? { ...t, done: !t.done } : t));
  }
}

🎯 Quick Quiz

Question 1: You need to set a button's disabled property from a boolean signal isSaving. Which binding is correct?

Question 2: What does the track expression in an @for block do?

Question 3: Which statement about pipes is true?

Summary & Quiz

πŸŽ‰ Key Takeaways

  • Four bindings, distinguished by punctuation: {{ }} and [prop] push data out, (event) brings actions in, and [(ngModel)] does both.
  • Use property binding for non-string values and inputs; use class/style bindings to drive appearance from state.
  • The new @if / @for / @switch control flow replaces *ngIf/*ngFor; @for requires track.
  • Template variables (#ref) reach elements and child components; pipes transform values for display, and pure pipes are the fast default.

πŸ“š Further Reading

πŸš€ What's Next?

You can now build expressive, reactive templates. Next we go deeper into how components talk to one another β€” component communication patterns for parent, child, and unrelated components across a growing app.

πŸŽ‰ Excellent!

Templates are where an Angular app comes to life. You now speak the full binding vocabulary.