Skip to main content

Template Syntax and Control Flow

Angular templates have their own syntax for expressing logic inside HTML. This guide covers data binding, the modern control flow syntax introduced in Angular 17+, pipes, and deferred loading.

1. Data Binding

Angular provides four types of data binding.

1.1 Interpolation

Use {{ expression }} to output component property values as text.

<!-- Component: title = 'Angular'; count = 42; -->
<h1>{{ title }}</h1>
<p>Count: {{ count }}</p>
<p>{{ 1 + 1 }}</p>
<p>{{ user.name | uppercase }}</p>

1.2 Property Binding

Use [property]="expression" to bind values to DOM attributes or component @Input() properties.

<img [src]="imageUrl" [alt]="imageAlt" />
<button [disabled]="isLoading">Submit</button>
<input [value]="searchText" />

<!-- Passing data to a child component -->
<app-user-card [user]="currentUser" [editable]="isAdmin" />
Comparison with React
// React: embed values directly with {} in JSX
<img src={imageUrl} alt={imageAlt} />
<button disabled={isLoading}>Submit</button>

In Angular, [attr]="expr" makes the binding explicit.

1.3 Event Binding

Use (event)="handler($event)" to respond to DOM events.

<button (click)="onSave()">Save</button>
<input (input)="onSearch($event)" (keydown.enter)="onSearch($event)" />
<form (submit)="onSubmit($event)">...</form>

<!-- Keyboard shortcut -->
<div (keydown.ctrl.s)="onSave()">...</div>
export class MyComponent {
onSave() { console.log('saved'); }
onSearch(event: Event) {
const value = (event.target as HTMLInputElement).value;
console.log(value);
}
}

1.4 Two-way Binding

Use [(ngModel)] to keep a form input in sync with a component property (FormsModule required).

app.component.ts
import { FormsModule } from '@angular/forms';

@Component({
imports: [FormsModule],
template: `
<input [(ngModel)]="name" />
<p>Hello, {{ name }}!</p>
`,
})
export class AppComponent {
name = '';
}

[(ngModel)] is shorthand for [ngModel]="name" (ngModelChange)="name=$event".


2. New Control Flow Syntax (Angular 17+)

Angular 17 introduced built-in control flow syntax. It is faster and more readable than the legacy structural directives (*ngIf, *ngFor).

2.1 @if / @else if / @else

@if (user.isLoggedIn) {
<p>Welcome, {{ user.name }}!</p>
} @else if (user.isGuest) {
<p>Logged in as guest</p>
} @else {
<a routerLink="/login">Log in</a>
}
Comparison with React
// React: ternary or && operator
{user.isLoggedIn ? (
<p>Welcome, {user.name}!</p>
) : user.isGuest ? (
<p>Logged in as guest</p>
) : (
<a href="/login">Log in</a>
)}

2.2 @for (track required)

@for (item of items; track item.id) {
<li>{{ item.name }}</li>
} @empty {
<li>No items found</li>
}

track is required and optimises list re-rendering. Always use a unique identifier such as id.

<!-- Using built-in variables -->
@for (product of products; track product.id; let i = $index, last = $last) {
<div [class.last-item]="last">
{{ i + 1 }}. {{ product.name }} — ${{ product.price }}
</div>
}
VariableDescription
$indexCurrent index (0-based)
$firsttrue for the first item
$lasttrue for the last item
$eventrue for even-indexed items
$oddtrue for odd-indexed items
$countTotal number of items
Comparison with React
// React: map() + key prop
{items.map((item, index) => (
<li key={item.id}>{item.name}</li>
))}
// Empty state requires a separate condition
{items.length === 0 && <li>No items found</li>}

2.3 @switch / @case / @default

@switch (user.role) {
@case ('admin') {
<app-admin-panel />
}
@case ('editor') {
<app-editor-toolbar />
}
@default {
<app-viewer />
}
}

3. @defer for Deferred Loading (Angular 17+)

@defer lazily loads components and template blocks, reducing the initial bundle size.

Basic Syntax

@defer {
<app-heavy-chart />
} @placeholder {
<p>Loading…</p>
} @loading (minimum 300ms) {
<app-spinner />
} @error {
<p>Failed to load.</p>
}
BlockDescription
@deferThe lazily loaded content
@placeholderShown before the deferred block loads
@loadingShown while downloading
@errorShown if loading fails

Triggers

<!-- Load when the block enters the viewport -->
@defer (on viewport) {
<app-analytics-widget />
}

<!-- Load on user interaction (click) -->
@defer (on interaction) {
<app-comments />
} @placeholder {
<button>Show comments</button>
}

<!-- Load when the browser is idle (default) -->
@defer (on idle) {
<app-recommendation />
}

<!-- Load when a condition becomes true -->
@defer (when showDetails) {
<app-detail-panel />
}
Comparison with React
// React: React.lazy + Suspense
const HeavyChart = React.lazy(() => import('./HeavyChart'));

<Suspense fallback={<p>Loading…</p>}>
<HeavyChart />
</Suspense>

Angular's @defer offers finer-grained trigger control (viewport, interaction, idle, condition).


4. Legacy Structural Directives

Before Angular 17, the following structural directives were used. You will still encounter them in existing codebases and libraries.

4.1 *ngIf

<p *ngIf="isLoggedIn">Logged in</p>
<p *ngIf="!isLoggedIn">Not logged in</p>

<!-- else template -->
<p *ngIf="isLoggedIn; else guestBlock">Welcome!</p>
<ng-template #guestBlock>
<p>You are a guest</p>
</ng-template>

4.2 *ngFor

<li *ngFor="let item of items; let i = index; trackBy: trackById">
{{ i }}: {{ item.name }}
</li>
trackById(index: number, item: Item): number {
return item.id;
}

4.3 *ngSwitch

<div [ngSwitch]="status">
<p *ngSwitchCase="'active'">Active</p>
<p *ngSwitchCase="'inactive'">Inactive</p>
<p *ngSwitchDefault>Unknown</p>
</div>
When to use old vs new syntax

Use @if / @for / @switch for all new development.
Migrate legacy templates using the Angular CLI migration schematic:

ng generate @angular/core:control-flow

5. Attribute Directives

5.1 ngClass

Dynamically toggle CSS classes based on conditions.

<!-- String -->
<p [ngClass]="'active bold'">Text</p>

<!-- Object -->
<p [ngClass]="{ active: isActive, disabled: !isEnabled }">Text</p>

<!-- Array -->
<p [ngClass]="['base-class', isActive ? 'active' : 'inactive']">Text</p>

In Angular 17+ you can also use the class binding directly:

<!-- Single class conditional binding -->
<p [class.active]="isActive" [class.error]="hasError">Text</p>

5.2 ngStyle

Dynamically set inline styles.

<p [ngStyle]="{ color: textColor, fontSize: fontSize + 'px' }">Text</p>

In Angular 17+ you can use the style binding directly:

<p [style.color]="textColor" [style.font-size.px]="fontSize">Text</p>
Comparison with React
// React: className and style prop
<p className={`base ${isActive ? 'active' : ''}`}>Text</p>
<p style={{ color: textColor, fontSize: `${fontSize}px` }}>Text</p>

6. Template Reference Variables

Use #variableName to get a reference to a DOM element or component within a template.

<!-- Reference a DOM element -->
<input #searchInput type="text" />
<button (click)="search(searchInput.value)">Search</button>

<!-- Call a component method -->
<app-modal #modal />
<button (click)="modal.open()">Open modal</button>

<!-- Reference a ng-template -->
<ng-template #loadingTpl>
<app-spinner />
</ng-template>
<ng-container *ngIf="isLoading; else loadingTpl">
<app-content />
</ng-container>

Access the reference from TypeScript with @ViewChild:

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

@Component({ /* ... */ })
export class SearchComponent {
@ViewChild('searchInput') searchInput!: ElementRef<HTMLInputElement>;

focusInput() {
this.searchInput.nativeElement.focus();
}
}
Comparison with React
// React: useRef
const searchInput = useRef<HTMLInputElement>(null);
const focusInput = () => searchInput.current?.focus();

<input ref={searchInput} type="text" />

7. Pipes

Pipes transform data for display in templates using the | operator.

7.1 Built-in Pipes

<!-- date -->
<p>{{ today | date:'yyyy/MM/dd' }}</p>
<p>{{ today | date:'short' }}</p>

<!-- currency -->
<p>{{ price | currency:'USD':'symbol':'1.0-0' }}</p>

<!-- number -->
<p>{{ 1234567.89 | number:'1.1-2' }}</p>

<!-- string transforms -->
<p>{{ 'hello world' | uppercase }}</p>
<p>{{ 'HELLO WORLD' | titlecase }}</p>

<!-- json (great for debugging) -->
<pre>{{ user | json }}</pre>

<!-- async: auto-subscribes to Observable / Promise -->
<p>{{ user$ | async }}</p>

<!-- keyvalue: iterate over an object -->
@for (entry of config | keyvalue; track entry.key) {
<p>{{ entry.key }}: {{ entry.value }}</p>
}

Common Built-in Pipes

PipeDescriptionExample
dateFormat a date{{ date | date:'yyyy/MM/dd' }}
currencyCurrency display{{ price | currency:'USD' }}
numberNumber formatting{{ num | number:'1.2-2' }}
uppercaseConvert to uppercase{{ str | uppercase }}
lowercaseConvert to lowercase{{ str | lowercase }}
titlecaseTitle case{{ str | titlecase }}
sliceSlice an array or string{{ items | slice:0:5 }}
jsonStringify to JSON (debug){{ obj | json }}
asyncSubscribe to Observable/Promise{{ data$ | async }}
keyvalueConvert object to key-value pairs{{ map | keyvalue }}
percentPercentage display{{ 0.75 | percent }}
i18nPluralPlural handling{{ count | i18nPlural:map }}

7.2 Chaining Pipes

<p>{{ name | uppercase | slice:0:10 }}</p>
<p>{{ today | date:'full' | uppercase }}</p>

7.3 Creating a Custom Pipe

src/app/pipes/truncate.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
name: 'truncate',
standalone: true,
pure: true, // default: true
})
export class TruncatePipe implements PipeTransform {
transform(value: string, maxLength = 50, suffix = '…'): string {
if (value.length <= maxLength) return value;
return value.slice(0, maxLength) + suffix;
}
}
<!-- requires imports: [TruncatePipe] in the component -->
<p>{{ longText | truncate:30 }}</p>
<p>{{ longText | truncate:20:'...' }}</p>

7.4 Pure vs Impure Pipes

PureImpure
ExecutionOnly when input reference changesEvery change detection cycle
PerformanceHighLower (use with caution)
Use caseString formatting, computationArray filtering (when items are added)
Configurationpure: true (default)pure: false
@Pipe({ name: 'filter', standalone: true, pure: false })
export class FilterPipe implements PipeTransform {
transform(items: any[], keyword: string): any[] {
return items.filter(item => item.name.includes(keyword));
}
}
caution

Impure pipes run on every change detection cycle. Applying them to large datasets can cause performance issues. When possible, compute the filtered array in the component instead.

Comparison with React

React has no built-in equivalent to pipes. The usual approach is to define utility functions and call them inside JSX.

// React: utility function
function truncate(str: string, max = 50) {
return str.length > max ? str.slice(0, max) + '…' : str;
}
<p>{truncate(longText, 30)}</p>

Quick Reference

FeatureAngularReact
Interpolation{{ value }}{value}
Property binding[src]="url"src={url}
Event binding(click)="fn()"onClick={fn}
Two-way binding[(ngModel)]="val"useState + onChange
Conditional@if / @else? : / &&
List rendering@for ... trackmap() + key
Deferred loading@deferReact.lazy + Suspense
Class toggle[class.active]="bool"className
Style binding[style.color]="val"style={{ color: val }}
Data transform| pipeutility function