AngularJSTemplates

Templates

An Angular template is HTML enhanced with Angular-specific syntax. When Angular compiles your app (Ahead-of-Time), it transforms these templates into highly optimized JavaScript. The result: zero runtime template parsing, fast rendering, and full TypeScript type checking on your HTML.

Angular templates support the following special syntax on top of standard HTML:

  • Interpolation: {{ expression }} — render a value as text

  • Property binding: [property]="expression" — bind to a DOM or component property

  • Event binding: (event)="handler()" — listen to DOM events

  • Two-way binding: [(ngModel)]="value" — sync model and view

  • Control flow: @if, @for, @switch, @defer (Angular 17+)

  • Template variables: #refName — reference to an element or component

  • Pipes: value | pipeName — transform displayed values

  • Safe navigation: obj?.prop — prevent null reference errors

Interpolation

Double curly braces {{ }} evaluate a TypeScript expression and render it as a string. You can use any valid TypeScript expression that produces a value:

HTML
<!-- Simple property -->
<h1>{{ title }}</h1>

<!-- Method call -->
<p>{{ getFullName() }}</p>

<!-- Arithmetic expression -->
<p>Total: {{ price * quantity }}</p>

<!-- Ternary -->
<p>{{ isLoggedIn ? 'Welcome back!' : 'Please sign in' }}</p>

<!-- String method -->
<p>{{ name.toUpperCase() }}</p>

<!-- Safe navigation for potentially null values -->
<p>{{ user?.profile?.bio }}</p>

<!-- Pipe transform -->
<p>{{ birthday | date:'longDate' }}</p>
Note
Interpolation expressions are evaluated in the context of the component class. They should be side-effect free — Angular calls them often during change detection.
Property Binding

Square brackets [property] bind an expression to a DOM property (not an HTML attribute). This is different from interpolation — property binding sets the property's value directly:

HTML
<!-- DOM property binding -->
<img [src]="imageUrl" [alt]="imageDescription" />
<button [disabled]="isLoading">Submit</button>
<input [value]="searchTerm" [placeholder]="placeholderText" />

<!-- Component @Input binding -->
<app-user-card [user]="currentUser" [isHighlighted]="true" />

<!-- Class binding -->
<div [class]="dynamicClass">...</div>
<div [class.active]="isActive">...</div>            <!-- single class toggle -->
<div [class.loading]="isLoading" [class.error]="hasError">...</div>

<!-- Style binding -->
<div [style.color]="textColor">...</div>
<div [style.font-size.px]="fontSize">...</div>      <!-- with unit -->
<div [style]="styleObject">...</div>                <!-- object with multiple styles -->

<!-- Attribute binding (for ARIA and non-standard attributes) -->
<div [attr.aria-label]="buttonLabel">...</div>
<td [attr.colspan]="colSpan">...</td>

Property vs Attribute binding:

  • Use [property] for DOM properties (src, value, disabled)
  • Use [attr.name] for HTML attributes that have no corresponding DOM property (colspan, aria-*)
Event Binding

Parentheses (event) listen to DOM events and call a component method when the event fires:

HTML
<!-- Click event -->
<button (click)="onSave()">Save</button>

<!-- Input event — fires on every keystroke -->
<input (input)="onSearch($event)" />

<!-- Change event — fires when value is committed -->
<select (change)="onCategoryChange($event)">...</select>

<!-- Form submit -->
<form (ngSubmit)="onSubmit()">...</form>

<!-- Mouse events -->
<div (mouseenter)="onHover(true)" (mouseleave)="onHover(false)">...</div>

<!-- Keyboard events -->
<input (keydown.enter)="onEnter()" (keydown.escape)="onEscape()" />

<!-- Passing data to the handler -->
<button (click)="onDelete(item.id)">Delete</button>
<app-card (selected)="onCardSelected($event)" />

TS
// Component class handling events
export class SearchComponent {
  query = '';

  onSearch(event: Event): void {
    const input = event.target as HTMLInputElement;
    this.query = input.value;
  }

  onCategoryChange(event: Event): void {
    const select = event.target as HTMLSelectElement;
    this.loadCategory(select.value);
  }

  private loadCategory(category: string): void {
    // fetch data...
  }
}
Two-Way Binding

Two-way binding [(ngModel)] synchronizes a form control's value with a component property in both directions — automatically. It requires FormsModule:

TS
// Import FormsModule in the component
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-search',
  standalone: true,
  imports: [FormsModule],
  template: `
    <input [(ngModel)]="query" placeholder="Search..." />
    <p>You typed: {{ query }}</p>
    <button (click)="query = ''">Clear</button>
  `,
})
export class SearchComponent {
  query = '';
}

[(ngModel)]="query" is shorthand for:

HTML
<!-- Two-way binding is shorthand for this: -->
<input [value]="query" (input)="query = $event.target.value" />
Control Flow — @if

Angular 17 introduced built-in control flow that replaces structural directives (*ngIf, *ngFor). The new syntax uses @ prefixed blocks:

HTML
<!-- @if (replaces *ngIf) -->
@if (user) {
  <p>Welcome, {{ user.name }}!</p>
} @else if (isLoading) {
  <app-spinner />
} @else {
  <p>Please sign in.</p>
}

<!-- Old syntax (still works, but @if is preferred) -->
<p *ngIf="user; else loadingTemplate">Welcome, {{ user.name }}!</p>
<ng-template #loadingTemplate><app-spinner /></ng-template>
Control Flow — @for

HTML
<!-- @for (replaces *ngFor) — track is REQUIRED -->
@for (product of products; track product.id) {
  <app-product-card [product]="product" />
} @empty {
  <p>No products found.</p>
}

<!-- Accessing loop variables -->
@for (item of items; track item.id; let i = $index, last = $last) {
  <li [class.last]="last">{{ i + 1 }}. {{ item.name }}</li>
}

<!-- Available loop context variables:
  $index   — current index (0-based)
  $first   — true if first item
  $last    — true if last item
  $even    — true if index is even
  $odd     — true if index is odd
  $count   — total number of items
-->
Warning
The track expression in @for is required (unlike trackBy in *ngFor). It must return a unique identifier for each item. This is how Angular efficiently updates the DOM when the list changes.
Control Flow — @switch

HTML
@switch (user.role) {
  @case ('admin') {
    <app-admin-panel />
  }
  @case ('editor') {
    <app-editor-tools />
  }
  @default {
    <app-viewer-panel />
  }
}
@defer — Lazy Loading Template Blocks

@defer (Angular 17+) lazily loads a part of the template, reducing initial bundle size. The deferred block is only downloaded when the trigger condition is met:

HTML
<!-- Load the component only when it scrolls into view -->
@defer (on viewport) {
  <app-heavy-chart [data]="chartData" />
} @placeholder {
  <div class="chart-placeholder">Chart loading...</div>
} @loading (minimum 500ms) {
  <app-spinner />
} @error {
  <p>Failed to load chart.</p>
}

<!-- Defer triggers:
  on idle          — when browser is idle
  on viewport      — when element enters viewport
  on interaction   — on click or focus
  on hover         — on hover
  on timer(2000)   — after a delay
  when condition   — when an expression becomes truthy
-->
Template Variables

Template variables (prefixed with #) give you a reference to an element or component in the template:

HTML
<!-- Reference a DOM element -->
<input #emailInput type="email" />
<button (click)="submit(emailInput.value)">Submit</button>

<!-- Reference a component instance -->
<app-dialog #myDialog />
<button (click)="myDialog.open()">Open Dialog</button>

<!-- Reference an NgForm (template-driven forms) -->
<form #loginForm="ngForm" (ngSubmit)="onLogin(loginForm)">
  <input name="email" ngModel required />
  <button [disabled]="loginForm.invalid">Login</button>
</form>

TS
// Access template variables in the component via @ViewChild
import { ViewChild, ElementRef, AfterViewInit } from '@angular/core';

@Component({ ... })
export class LoginComponent implements AfterViewInit {
  @ViewChild('emailInput') emailInputRef!: ElementRef<HTMLInputElement>;

  ngAfterViewInit() {
    this.emailInputRef.nativeElement.focus();
  }
}
Pipes in Templates

Pipes transform data for display without changing the underlying value:

HTML
<!-- Built-in pipes -->
<p>{{ price | currency:'USD':'symbol':'1.2-2' }}</p>          <!-- $1,234.56 -->
<p>{{ birthday | date:'MMMM d, y' }}</p>                      <!-- July 1, 2024 -->
<p>{{ 0.75 | percent }}</p>                                    <!-- 75% -->
<p>{{ message | uppercase }}</p>                               <!-- HELLO WORLD -->
<p>{{ title | titlecase }}</p>                                 <!-- Hello World -->
<p>{{ longText | slice:0:100 }}...</p>                         <!-- first 100 chars -->
<p>{{ items | json }}</p>                                      <!-- JSON string (debug) -->
<p>{{ data$ | async }}</p>                                     <!-- unwrap Observable -->

<!-- Chaining pipes -->
<p>{{ name | uppercase | slice:0:5 }}</p>

<!-- Custom pipe -->
<p>{{ text | truncate:50 }}</p>
ng-container — Grouping Without Adding DOM Nodes

&lt;ng-container&gt; is a logical grouping element that does not render to the DOM. Use it to apply control flow to multiple elements without a wrapper div:

HTML
<!-- Without ng-container — adds an unwanted div to the DOM -->
<div *ngIf="isLoggedIn">
  <span>Welcome</span>
  <a href="/profile">Profile</a>
</div>

<!-- With ng-container — no extra DOM element -->
<ng-container *ngIf="isLoggedIn">
  <span>Welcome</span>
  <a href="/profile">Profile</a>
</ng-container>

<!-- @if does not need ng-container (new control flow is block-based) -->
@if (isLoggedIn) {
  <span>Welcome</span>
  <a href="/profile">Profile</a>
}

<!-- ng-container for multiple structural directives on one element -->
<ng-container *ngFor="let item of items">
  <ng-container *ngIf="item.isVisible">
    <app-item [data]="item" />
  </ng-container>
</ng-container>
ng-template — Reusable Template Fragments

HTML
<!-- Define a reusable template fragment -->
<ng-template #loadingTpl>
  <div class="loading">
    <app-spinner />
    <p>Loading...</p>
  </div>
</ng-template>

<!-- Reference it elsewhere -->
@if (isLoading) {
  <ng-container *ngTemplateOutlet="loadingTpl" />
} @else {
  <app-content />
}

<!-- Pass context to a template -->
<ng-template #itemTpl let-item let-index="index">
  <li>{{ index + 1 }}. {{ item.name }}</li>
</ng-template>
Template Syntax Comparison: Old vs New

Old syntax

New syntax (Angular 17+)

*ngIf="condition"

@if (condition) { }

*ngIf="c; else t"

@if (c) { } @else { } (using ng-template #t)

*ngFor="let x of xs; trackBy: fn"

@for (x of xs; track x.id) { }

*ngFor="let x of xs; let i = index"

@for (x of xs; track x.id; let i = $index) { }

[ngSwitch]="val" + *ngSwitchCase

@switch (val) { @case (v) { } }

No defer block

@defer (on viewport) { }

Note
Both syntaxes are supported. The Angular team recommends migrating to the new @if / @for syntax for all new code. You can run ng generate @angular/core:control-flow to automatically migrate an existing project.