Angular templates bind component state to DOM properties, text, events, forms, and control-flow blocks. Learn each binding direction separately, then combine them without hiding application logic inside template expressions.
Use double curly braces {{ }} to embed expressions in the template. Angular evaluates the expression and converts the result to a string.
Expressions can read component members, signals, literals, operators, optional chaining, and pipes, but they are not full JavaScript. Keep them deterministic and inexpensive; move allocation, sorting, and business rules into computed state or a pure pipe.
import { Component, signal } from '@angular/core';
@Component({ selector: 'app-demo', standalone: true, templateUrl: './demo.component.html' })
export class DemoComponent {
name = signal('Angular');
version = signal(21);
price = signal(9.99);
items = signal(['Apple', 'Banana', 'Cherry']);
}
<!-- Basic interpolation -->
<h1>Hello, {{ name() }}!</h1>
<p>Version: {{ version() }}</p>
<!-- Expressions -->
<p>Price: ${{ price() | number:'1.2-2' }}</p>
<p>Items: {{ items().length }}</p>
<p>Upper: {{ name().toUpperCase() }}</p>
<p>Sum: {{ 1 + 2 + 3 }}</p>
Bind a DOM property to a component expression using square brackets [property]="expression".
The target can be a native property, a component or directive input, an attribute through attr., a class, or a style. Bind disabled, value, and checked as properties; use attr.aria-label and other attribute bindings when no corresponding writable property exists.
<!-- Bind to DOM properties -->
<img [src]="imageUrl()" [alt]="imageAlt()" />
<button [disabled]="isLoading()">Submit</button>
<input [value]="username()" />
<!-- Class and style binding -->
<div [class.active]="isActive()">...</div>
<div [class]="{ active: isActive(), error: hasError() }">...</div>
<p [style.color]="textColor()">Styled text</p>
<p [style]="{ fontSize: fontSize() + 'px', fontWeight: 'bold' }">...</p>
<!-- Attribute binding (for non-DOM attributes like aria-*) -->
<button [attr.aria-label]="buttonLabel()">Click</button>
Listen to DOM events using parentheses (event)="handler($event)".
The event expression may assign local state or call a short component method. Use $event when the handler needs the typed event object, and use key modifiers such as keydown.enter for keyboard-specific behavior.
export class EventDemoComponent {
count = signal(0);
inputValue = signal('');
increment() { this.count.update(v => v + 1); }
onInput(event: Event) {
this.inputValue.set((event.target as HTMLInputElement).value);
}
onKeydown(event: KeyboardEvent) {
if (event.key === 'Enter') console.log('Enter pressed');
}
}
<button (click)="increment()">Count: {{ count() }}</button>
<input (input)="onInput($event)" (keydown)="onKeydown($event)" />
<p>You typed: {{ inputValue() }}</p>
<!-- Inline expression -->
<button (click)="count.set(0)">Reset</button>
Two-way syntax combines an input named value with an output named valueChange. Use FormsModule and [(ngModel)] for form controls, or expose a model() field from a child component for a typed custom two-way contract.
Use separate property and event bindings when the parent must validate, transform, reject, or log the proposed change before updating state. Two-way binding should express genuine shared ownership, not hide a command.
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-two-way',
standalone: true,
imports: [FormsModule],
templateUrl: './two-way.component.html'
})
export class TwoWayComponent {
username = '';
}
<!-- [(ngModel)] requires FormsModule -->
<input [(ngModel)]="username" placeholder="Enter name" />
<p>Hello, {{ username }}!</p>
<!-- Equivalent manual two-way -->
<input [value]="username" (input)="username = $any($event.target).value" />
Use @if, @for, and @switch for conditional and repeated template regions. @for requires a track expression and supports @empty; choose a stable domain key so Angular can retain DOM and component state as the collection changes.
Use @let to name one expression for the current view and @defer to split noncritical dependencies with explicit placeholder, loading, and error states. A template variable follows view scope and cannot be reassigned like a component field.
<!-- @if / @else if / @else -->
@if (isLoggedIn()) {
<p>Welcome back, {{ username() }}!</p>
} @else if (isGuest()) {
<p>Browsing as guest</p>
} @else {
<a href="/login">Please log in</a>
}
<!-- @for with track (required) -->
@for (item of items(); track item.id) {
<li>{{ item.name }} - ${{ item.price }}</li>
} @empty {
<li>No items found</li>
}
<!-- Loop variables -->
@for (item of items(); track item.id; let i = $index, last = $last) {
<li [class.last]="last">{{ i + 1 }}. {{ item.name }}</li>
}
<!-- @switch -->
@switch (status()) {
@case ('loading') {
<app-spinner />
}
@case ('error') {
<app-error-message />
}
@case ('success') {
<app-data-table />
}
@default {
<p>Unknown status</p>
}
}
A template reference such as #search points to the element, component, directive, TemplateRef, or exported API at that location. Its scope is the current view; control-flow blocks create child views with their own variable boundaries.
@let stores the current value of an expression for reuse within its view. It updates when the expression changes but cannot be assigned from an event handler.
<!-- #ref creates a reference to the element -->
<input #nameInput type="text" />
<button (click)="greet(nameInput.value)">Greet</button>
<!-- Reference to a component -->
<app-child #child />
<button (click)="child.doSomething()">Call child method</button>
<!-- @defer - lazy load a block -->
@defer (on viewport) {
<app-heavy-component />
} @placeholder {
<p>Scroll down to load...</p>
} @loading {
<app-spinner />
}
A template should make data flow visible. Use semantic HTML first, bind the smallest necessary values, label interactive controls, and keep event expressions short enough that their intent is obvious.
Use @if for conditional views, @for with a stable track expression for collections, @switch for exclusive states, @let for one reusable expression, and @defer only for noncritical dependencies. Give placeholders stable dimensions when deferral could move surrounding content.
Template reference variables point to an element, directive, component, or exported form directive in the current view. They are not general mutable variables and do not cross view boundaries created by control flow.
Do not assume a signal behaves like a plain assignable property. A writable signal is read with username() and changed with set or update, while ngModel’s traditional two-way binding expects to assign to its target.
The track expression tells Angular which logical item each rendered view represents. Tracking by a non-unique or changing value can cause Angular to reuse the wrong DOM node when the array is reordered, inserted into, or filtered.
Square-bracket property binding normally writes to a DOM element property, which is appropriate for src, disabled, and value. Some values, including many ARIA attributes, are attributes rather than corresponding writable DOM properties in Angular templates.
Explore 500+ free tutorials across 20+ languages and frameworks.