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" />
// 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).
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>
}
// 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>
}
| Variable | Description |
|---|---|
$index | Current index (0-based) |
$first | true for the first item |
$last | true for the last item |
$even | true for even-indexed items |
$odd | true for odd-indexed items |
$count | Total number of items |
// 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>
}
| Block | Description |
|---|---|
@defer | The lazily loaded content |
@placeholder | Shown before the deferred block loads |
@loading | Shown while downloading |
@error | Shown 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 />
}
// 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>
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>
// 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();
}
}
// 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
| Pipe | Description | Example |
|---|---|---|
date | Format a date | {{ date | date:'yyyy/MM/dd' }} |
currency | Currency display | {{ price | currency:'USD' }} |
number | Number formatting | {{ num | number:'1.2-2' }} |
uppercase | Convert to uppercase | {{ str | uppercase }} |
lowercase | Convert to lowercase | {{ str | lowercase }} |
titlecase | Title case | {{ str | titlecase }} |
slice | Slice an array or string | {{ items | slice:0:5 }} |
json | Stringify to JSON (debug) | {{ obj | json }} |
async | Subscribe to Observable/Promise | {{ data$ | async }} |
keyvalue | Convert object to key-value pairs | {{ map | keyvalue }} |
percent | Percentage display | {{ 0.75 | percent }} |
i18nPlural | Plural 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
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
| Pure | Impure | |
|---|---|---|
| Execution | Only when input reference changes | Every change detection cycle |
| Performance | High | Lower (use with caution) |
| Use case | String formatting, computation | Array filtering (when items are added) |
| Configuration | pure: 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));
}
}
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.
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
| Feature | Angular | React |
|---|---|---|
| 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 ... track | map() + key |
| Deferred loading | @defer | React.lazy + Suspense |
| Class toggle | [class.active]="bool" | className |
| Style binding | [style.color]="val" | style={{ color: val }} |
| Data transform | | pipe | utility function |