Updated Jul 18, 2026

Components and Templates

Angular templates are HTML plus two bracket conventions and a block syntax. The brackets carry the whole binding model: [square] means data flowing into a DOM property, (round) means events flowing out. Once those two register, every Angular template reads left to right - and the modern @if/@for blocks (which replaced the older *ngIf/*ngFor you'll meet in legacy code) read like the TypeScript around them.

Interpolation and property binding: data in

@Component({
  selector: 'app-product',
  template: `
    <h2>{{ name() }}</h2>
    <img [src]="imageUrl()" [alt]="name()" />
    <button [disabled]="!inStock()">Buy</button>
  `,
})
export class Product {
  name = signal('Kettle');
  imageUrl = signal('/kettle.jpg');
  inStock = signal(true);
}

What just happened: {{ expr }} interpolates into text. [src]="expr" binds a DOM property to an expression - re-evaluated whenever the signals it reads change. Without the brackets, disabled="false" would set a plain HTML attribute to the literal string "false" - which, for boolean attributes, means disabled (the string is truthy). The brackets are the difference between "this text" and "this expression, kept live" - the same string-vs-expression rule as every framework, wearing square brackets.

Class and style get ergonomic forms you'll use daily: [class.active]="isActive()" toggles one class by boolean; [style.width.px]="size()" binds one style with units.

Event binding: actions out

template: `
  <button (click)="addToCart(product().id)">Add</button>
  <input (input)="onSearch($event)" />
  <form (submit)="save(); $event.preventDefault()">...</form>
`

What just happened: (click)="..." runs the statement when the event fires - a method call, usually. $event is the native event object when you need it. Like Vue's compiled templates (and unlike JSX), writing the call with parentheses is correct - it runs on the event, not during render; there's no pass-the-function-don't-call-it trap here.

Branching: @if

@if (cart().length === 0) {
  <p>Your cart is empty.</p>
} @else if (cart().length < 10) {
  <p>{{ cart().length }} items.</p>
} @else {
  <p>Bulk order!</p>
}

Block syntax, braces and all, right in the template. A false branch unmounts its contents - DOM gone, component state inside destroyed - the standard conditional-rendering semantics. For hide-but-keep-alive, bind the class: [class.hidden]="!open()" plus a CSS rule.

📝 Terminology: legacy spelling: <p *ngIf="cart.length === 0"> - a "structural directive" with its asterisk, plus ng-template gymnastics for the else branch. The @if block does the same job with less ceremony; codebases are migrating incrementally, so read both fluently.

Loops: @for and the mandatory track

<ul>
  @for (todo of todos(); track todo.id) {
    <li>{{ todo.text }}</li>
  } @empty {
    <li>Nothing to do.</li>
  }
</ul>

What just happened: one <li> per item, an @empty block for the zero case - and a track expression that is mandatory. Angular took the identity lesson every framework teaches (React's key, Vue and Svelte's keyed lists) and made it required syntax: track todo.id tells the renderer how to match items across updates, so reorders move DOM instead of rewriting it and row state stays with its row.

💡 Key point: Angular forcing track is the framework encoding a decade of production bugs into the compiler. You can still write track $index - and for lists that reorder or delete, that's the same corruption-by-position bug as an unkeyed list elsewhere, just explicitly chosen. Track a stable id whenever the list's shape can change. (Legacy spelling: *ngFor="let t of todos; trackBy: myTrackFn" - where trackBy was optional, usually omitted, and its omission was a notorious performance foot-gun on big lists.)

Two-way on inputs: banana in a box

import { FormsModule } from '@angular/forms';

@Component({
  imports: [FormsModule],
  template: `
    <input [(ngModel)]="email" placeholder="[email protected]" />
    <p>Signing up: {{ email }}</p>
  `,
})
export class Signup {
  email = '';
}

What just happened: [(ngModel)] - the community reads the syntax as "banana in a box" - is two-way binding on a form control: value in ([ ]), changes out (( )), composed. It requires importing FormsModule into the component (note the imports array - standalone components declare their template dependencies, which phase 7 revisits as a classic error source). [(ngModel)] serves simple forms well; Angular's fuller answer for serious forms - reactive forms - is deferred to a follow-up guide, and phase 8 places it on the map.

Recap

  1. {{ expr }} for text; [prop]="expr" for live property binding - bare attributes are just strings.
  2. (event)="statement" runs on the event; $event is the native object; calls-with-parens are correct here.
  3. @if / @else unmounts on false; [class.x] toggles are the keep-alive alternative.
  4. @for (... ; track item.id) - track is mandatory, and track $index on mutable lists is the old corruption bug, opted into.
  5. Legacy dialect: *ngIf/*ngFor/trackBy - same semantics, older spelling.
  6. [(ngModel)] gives two-way form binding; it needs FormsModule in the component's imports.
[
  {
    "q": "A button written as <button disabled=\"false\"> stays disabled. Why?",
    "choices": [
      "Angular inverts boolean attributes by default",
      "Without brackets it's a plain HTML attribute set to the string \"false\", which is truthy - [disabled]=\"false\" binds the expression",
      "disabled requires FormsModule",
      "The component forgot to import CommonModule"
    ],
    "answer": 1,
    "why": [
      "Angular doesn't touch bare attributes at all - that's the point: without brackets, the framework isn't involved.",
      null,
      "FormsModule powers ngModel, not native attributes.",
      "No module fixes a binding that was never a binding."
    ],
    "explain": "Square brackets mean 'evaluate this expression and bind the DOM property.' Without them you wrote static HTML, and any non-empty string on a boolean attribute means true."
  },
  {
    "q": "Why did Angular make the track expression in @for mandatory rather than optional like the old trackBy?",
    "choices": [
      "To make templates more verbose for readability",
      "Item identity is what lets the renderer move DOM instead of rewriting it - optional trackBy was omitted so often it became a chronic performance and state bug",
      "track is needed for TypeScript type inference",
      "It replaces the need for @empty blocks"
    ],
    "answer": 1,
    "why": [
      "Verbosity is a cost here, paid deliberately for correctness.",
      null,
      "Types flow from the iterated array either way.",
      "@empty handles the zero case - unrelated to identity."
    ],
    "explain": "Every framework needs list identity (React keys, Vue/Svelte keyed each). Angular's old optional trackBy was chronically skipped, so the modern syntax makes the identity decision explicit and required."
  },
  {
    "q": "In an Angular template, (click)=\"remove(item.id)\" - does the call run during rendering, like it would in JSX?",
    "choices": [
      "Yes - wrap it in an arrow function to be safe",
      "No - templates are compiled, and the statement runs only when the event fires",
      "Only if the method returns void",
      "It runs once at render and once per click"
    ],
    "answer": 1,
    "why": [
      "Arrow-wrapping is a JSX necessity; here it's just extra characters.",
      null,
      "Return types don't change when template statements execute.",
      "Nothing executes at render - the compiler wires the statement to the event."
    ],
    "explain": "Angular templates (like Vue's) are compiled: the (event) binding is wiring, not evaluation. The call-during-render trap belongs to JSX, where braces hold live JavaScript."
  }
]

← Phase 1: What Angular Actually Is · Guide overview · Phase 3: Signals →

Before the quiz: without looking back, say (or jot down) the core idea of this phase in your own words.

Check your understanding 3 questions

1. A button written as <button disabled="false"> stays disabled. Why?

2. Why did Angular make the track expression in @for mandatory rather than optional like the old trackBy?

3. In an Angular template, (click)="remove(item.id)" - does the call run during rendering, like it would in JSX?