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
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) {
Your cart is empty.
} @else if (cart().length < 10) {
{{ cart().length }} items.
} @else {
Bulk order!
}
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
@for (todo of todos(); track todo.id) {
{{ todo.text }}
} @empty {
Nothing to do.
}
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
;
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
{{ expr }}for text;[prop]="expr"for live property binding - bare attributes are just strings.(event)="statement"runs on the event;$eventis the native object; calls-with-parens are correct here.@if / @elseunmounts on false;[class.x]toggles are the keep-alive alternative.@for (... ; track item.id)- track is mandatory, andtrack $indexon mutable lists is the old corruption bug, opted into.- Legacy dialect:
*ngIf/*ngFor/trackBy- same semantics, older spelling. [(ngModel)]gives two-way form binding; it needsFormsModulein 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?