Technology  /  Angular

๐Ÿ…ฐ๏ธ Angular 15 guides ยท updated 2026

Standalone components, signals, RxJS, and dependency injection โ€” building maintainable frontends with the framework enterprises trust.

Angular Data Binding and Control Flow: @if, @for, track, and Two-Way Binding

Templates earn their keep when they start making decisions โ€” showing this, hiding that, repeating rows, switching layouts. Angular v17 rebuilt this layer with built-in control flow (@if, @for, @switch): faster, clearer, and requiring no imports. Youโ€™ll write the new syntax and read the legacy structural-directive syntax (*ngIf, *ngFor) for years, since a decade of codebases and tutorials use it.

This guide completes the binding model โ€” including two-way binding and its famous โ€œbanana in a boxโ€ โ€” and covers the control-flow blocks with the performance details (track!) that separate smooth lists from janky ones.


The Binding Model, Completed

The components guide established the core three; hereโ€™s the full map:

{{ total() }} <!-- interpolation: state โ†’ text -->
<img [src]="url" /> <!-- property binding: state โ†’ property -->
<button (click)="save()"> <!-- event binding: event โ†’ method -->
<input [(ngModel)]="query" /> <!-- two-way: both directions -->

Plus two specialized binding families youโ€™ll use weekly:

<!-- class and style bindings โ€” conditional appearance without manual classList -->
<div [class.active]="isActive()" [class.error]="hasError()">
<div [style.width.%]="progress()">
<!-- attribute binding โ€” for true HTML attributes (ARIA, data-*, colspan) -->
<td [attr.colspan]="span()">
<button [attr.aria-expanded]="open()">

[class.x]="bool" toggles a single class by condition โ€” the declarative replacement for the classList toggling you did by hand in DOM work. The [attr.] prefix exists because most bindings target DOM properties; things that are only attributes (ARIA, colspan) need the explicit form โ€” a distinction youโ€™ve met before.

Two-way binding: sugar, decoded

[(ngModel)] โ€” brackets and parentheses, the โ€œbanana in a boxโ€ โ€” is exactly what its punctuation says: a property binding plus an event binding, fused:

<input [(ngModel)]="query" />
<!-- is precisely: -->
<input [ngModel]="query" (ngModelChange)="query = $event" />

State flows in; user edits flow out and update the state; the loop closes. It requires importing FormsModule and shines for simple cases โ€” a search box, a settings toggle. Two calibrations: for real forms with validation, reactive forms replace scattered ngModels with a proper model; and the same [()] sugar works on your own components via model inputs โ€” any x input paired with an xChange output gets the banana box for free. Itโ€™s a convention, not magic.


@if / @else: Conditional Rendering

@if (order(); as o) {
<p>Order #{{ o.id }} โ€” {{ o.status }}</p>
} @else if (loading()) {
<app-spinner />
} @else {
<p>No order found.</p>
}

Details worth internalizing:


@for: Lists, and Why track Is Not Optional

<ul>
@for (item of items(); track item.id) {
<li>
{{ $index + 1 }}. {{ item.name }}
<button (click)="remove(item.id)">โœ•</button>
</li>
} @empty {
<li class="muted">Nothing here yet.</li>
}
</ul>

The pieces: iterate any iterable; $index, $first, $last, $even, $odd are available inside; and @empty renders when the collection is empty โ€” a small block that eliminates the boilerplate @if (items().length) wrapper pattern entirely.

track is mandatory in @for, and the framework is right to insist. When the array changes, Angular must decide which DOM rows correspond to which data items. track item.id says โ€œidentity = the id,โ€ enabling minimal DOM surgery: one item added โ†’ one row created; reordered โ†’ rows moved, not rebuilt. Get it wrong and correctness suffers, not just speed:

If you take one performance habit from this guide: stable ids in data, track item.id in templates. (Legacy *ngFor made tracking optional via trackBy โ€” its omission is the number-one performance smell in old Angular code reviews.)

A closing composition note: blocks nest freely โ€” @for inside @if, @switch inside @for โ€” and when a rowโ€™s body grows past a few lines, that body wants to be a child component taking the item as an input.


@switch, @defer, and the Rest of the Family

@switch โ€” cleaner than chained @else if for discriminated states:

@switch (status()) {
@case ('loading') { <app-spinner /> }
@case ('error') { <app-error-panel [detail]="error()" /> }
@case ('ready') { <app-results [data]="data()" /> }
@default { <p>Unknown state</p> }
}

Pairs beautifully with union-typed state ('loading' | 'error' | 'ready') โ€” the template equivalent of exhaustive switching over sealed states, and a shape youโ€™ll use for every async view.

@defer โ€” lazy-load a template chunk (and its component code!) on a trigger:

@defer (on viewport) {
<app-reviews-panel [productId]="id()" /> <!-- code loads when scrolled near -->
} @placeholder {
<div class="skeleton"></div>
} @loading (minimum 200ms) {
<app-spinner />
} @error {
<p>Couldn't load reviews.</p>
}

Triggers include on viewport, on idle, on interaction, on hover, and when condition. This is dynamic-import code splitting promoted to a template primitive โ€” below-the-fold widgets, heavy charts, and rarely-opened panels stop taxing initial load, with placeholder/loading/error states declared inline. Itโ€™s one of modern Angularโ€™s best features and deserves a place in your default toolkit, not your advanced one.


Reading Legacy: The Structural-Directive Rosetta Stone

You will maintain code written before v17. The translation table:

<!-- legacy --> <!-- modern -->
<div *ngIf="user; else empty">...</div> @if (user) { ... } @else { ... }
<ng-template #empty>...</ng-template>
<li *ngFor="let item of items; @for (item of items;
trackBy: trackById; let i = index"> track item.id) { ... }
<div [ngSwitch]="status"> @switch (status) {
<p *ngSwitchCase="'a'">A</p> @case ('a') { <p>A</p> }
<p *ngSwitchDefault>?</p> @default { <p>?</p> }
</div> }

The * prefix marked structural directives โ€” directives that add/remove DOM, with the asterisk as sugar over <ng-template> wrappers (which is why the legacy else clause needs an explicit template reference). They required importing CommonModule/NgIf/NgFor; the new blocks are template-language natives. Migration is mechanical (ng generate @angular/core:control-flow automates it), but mixed codebases are the norm โ€” fluency in both directions is a genuine job skill for the next several years.


Composing the Blocks: An Async List, Complete

The blocks earn their keep composed. Hereโ€™s the full pattern for the most common UI in existence โ€” a filterable, loadable list โ€” with every state handled:

<input [(ngModel)]="query" placeholder="Filter ordersโ€ฆ" />
@if (orders.isLoading()) {
<app-skeleton-rows [count]="5" />
} @else if (orders.error()) {
<app-error-panel (retry)="orders.reload()" />
} @else {
<table>
@for (order of visible(); track order.id) {
<tr [class.overdue]="order.dueDate < today"
[class.selected]="order.id === selectedId()">
<td>{{ order.id }}</td>
<td>{{ order.customer }}</td>
<td>
@switch (order.status) {
@case ('pending') { <span class="badge warn">Pending</span> }
@case ('shipped') { <span class="badge ok">Shipped</span> }
@default { <span class="badge">{{ order.status }}</span> }
}
</td>
</tr>
} @empty {
<tr><td colspan="3">
@if (query()) { No orders match "{{ query() }}". }
@else { No orders yet. }
</td></tr>
}
</table>
}

Read the states it renders: loading (skeleton), failed (retriable error), empty-because-filtered vs genuinely-empty (different messages โ€” a detail users notice), and the happy path with per-row conditional styling and per-cell status switching. visible() is a computed filtering by query โ€” the filter logic lives in the class, not the template, and track order.id keeps row updates surgical as filtering changes the array. This template shape โ€” outer async @if-chain, inner @for with @empty, @switch for enum display โ€” is worth having in muscle memory; a large fraction of every data-driven app is this pattern with different nouns.

Frequently Asked Questions

Can I call methods in bindings โ€” {{ formatPrice(item) }}? It works, but every change-detection pass re-runs it โ€” fine for trivial functions, a trap for expensive ones. Prefer computed signals (cached, re-run only when dependencies change) or pipes (memoized by input). The general rule: bindings should be cheap reads.

Why does my @if-wrapped form lose its values? Because @if destroys and recreates โ€” thatโ€™s its contract. State that must survive visibility toggles belongs in the class (or use a CSS-based toggle for the quick-show/hide case).

@let โ€” whatโ€™s that? A newer block for naming template-local values: @let vat = total() * 0.18; then use vat below. It kills repeated long expressions; use it for readability, not for smuggling logic into templates.

How do I iterate object entries? Convert in the class โ€” Object.entries(obj) as ever โ€” exposed via a computed. (Legacy code used the keyvalue pipe; it still works.)

Is @defer a replacement for route-level lazy loading? Complementary โ€” route-level splitting chunks by page; @defer chunks within a page. Big apps use both.


Binding Edge Cases Worth Knowing Early

Four small behaviors that each cost someone an afternoon: Event binding $event types โ€” on DOM events itโ€™s the raw event (e.target needs casting; prefer template refs: #inp then inp.value); on component outputs itโ€™s whatever the child emitted โ€” one name, context-dependent meaning. Bindings run in order of neither appearance nor alphabet โ€” writing code that depends on [a] binding before [b] on the same element is undefined-behavior territory; if two inputs are interdependent, the child should derive rather than assume arrival order. [class] and [style] full-object bindings coexist with single bindings ([class.active]) but the single form wins conflicts โ€” mixing both on one element for the same class reads as a bug even when it works. Interpolation stringifies via toString โ€” {{ user }} printing [object Object] means you bound the object, not a field; and null/undefined interpolate as empty string (mercifully), while {{ obj.missing.deep }} still throws โ€” optional chaining works in templates and is the idiomatic guard.

Self-Check

  1. A list of editable rows uses track $index. The user adds a row at the top and existing rowsโ€™ input values shift down one. Explain the mechanism.
  2. <div [hidden]="!showPanel"> vs @if (showPanel) โ€” name a scenario where each is the right choice.
  3. Decode [(ngModel)]="email" into its two constituent bindings.
  4. A dashboardโ€™s below-the-fold analytics chart weighs 300KB of JS. Which control-flow block fixes the initial-load cost, and with what trigger?
  5. In legacy code you see *ngFor with no trackBy. What symptoms would confirm itโ€™s causing harm?

(Answers: index-tracking told Angular โ€œrow identity = position,โ€ so the insertion re-bound every rowโ€™s data to shifted indexes while DOM state (inputs) stayed put โ€” identity and state disagree. [hidden] for cheap content toggled rapidly whose state must survive (a filter panel mid-edit); @if for expensive or truly conditional content. [ngModel]="email" + (ngModelChange)="email = $event". @defer (on viewport) with a @placeholder skeleton. Full-list DOM rebuilds on every data refresh: flicker, lost focus/selection, GC churn โ€” visible in DevTools as every row node replaced.)

Next: Directives โ€” attribute directives, host bindings, and building your own reusable element behaviors.