Data Binding — Interpolation, Property, Event, and Two-Way Binding

Data binding is the mechanism that connects a component’s TypeScript class to its HTML template — synchronising data from class to view, and user interactions from view back to class. Angular has four binding forms: interpolation (displaying values), property binding (setting DOM properties), event binding (listening to DOM events), and two-way binding (synchronising form inputs). Mastering these four binding types is the foundation of building any interactive Angular UI. Everything from displaying a task title to handling a form submission uses one of these four patterns.

The Four Binding Types

Type Syntax Direction Use For
Interpolation {{ expression }} Class → Template Display text, numbers, computed values
Property Binding [property]="expression" Class → Template Set DOM properties, component inputs, attribute values
Event Binding (event)="handler($event)" Template → Class Handle clicks, inputs, form submissions, custom events
Two-Way Binding [(ngModel)]="property" Both directions Form inputs — sync input value and class property

Property Binding — Common DOM Properties

Binding Sets Example
[src] Image source URL [src]="user.avatarUrl"
[href] Anchor href [href]="task.externalLink"
[disabled] Button disabled state [disabled]="form.invalid || loading"
[class] CSS class string [class]="'btn btn--' + task.priority"
[class.active] Toggle single CSS class [class.active]="isSelected"
[style.color] Single CSS style [style.color]="task.isOverdue ? 'red' : 'inherit'"
[style] Style object [style]="{ opacity: loading ? 0.5 : 1 }"
[attr.data-id] HTML attribute (not property) [attr.data-testid]="'task-' + task._id"
[innerHTML] Inner HTML (sanitised) [innerHTML]="task.descriptionHtml"
[value] Input value (one-way) [value]="searchQuery"

Common Event Bindings

Event Fires When $event Type
(click) Element clicked MouseEvent
(input) Input value changes (each keystroke) Event — read via $event.target.value
(change) Input value committed (blur/enter) Event
(submit) Form submitted SubmitEvent
(keydown.enter) Enter key pressed KeyboardEvent
(keyup) Key released KeyboardEvent
(focus) Element receives focus FocusEvent
(blur) Element loses focus FocusEvent
(mouseenter) Mouse enters element MouseEvent
(mouseleave) Mouse leaves element MouseEvent
Note: There is a subtle but important difference between property binding [property]="value" and attribute binding [attr.name]="value". Property binding sets JavaScript DOM properties (which exist on the element object in memory). Attribute binding sets HTML attributes (which exist in the HTML markup). For most cases, property binding is correct. Use attribute binding for ARIA attributes ([attr.aria-label]), data attributes ([attr.data-id]), and SVG attributes, which are attribute-based rather than property-based.
Tip: Prefer [class.className]="condition" over [class]="condition ? 'className' : ''" for toggling individual classes — it is cleaner and does not interfere with other classes on the element. For multiple conditional classes, use [ngClass] with an object: [ngClass]="{ 'active': isActive, 'highlighted': isHighlighted }". This adds classes for truthy values and removes them for falsy ones without wiping other classes.
Warning: Two-way binding with [(ngModel)] requires FormsModule in the component’s imports array. Forgetting it causes a template error: ngModel is not a known property of 'input'. For reactive forms (covered in a later chapter), use ReactiveFormsModule with [formControl] instead of [(ngModel)]. Do not mix the two approaches in the same form.

Complete Binding Examples

// task-card.component.ts
import { Component, Input, Output, EventEmitter, signal } from '@angular/core';
import { CommonModule, DatePipe }   from '@angular/common';
import { FormsModule }              from '@angular/forms';
import { Task }                     from '../../../shared/models/task.model';

@Component({
    selector:   'app-task-card',
    standalone: true,
    imports:    [CommonModule, FormsModule, DatePipe],
    templateUrl:'./task-card.component.html',
})
export class TaskCardComponent {
    @Input()  task!: Task;
    @Output() completed   = new EventEmitter<string>();
    @Output() deleted     = new EventEmitter<string>();

    isExpanded  = signal(false);
    editingTitle = signal(false);
    titleDraft  = signal('');

    get isOverdue(): boolean {
        return !!this.task.dueDate &&
               new Date(this.task.dueDate) < new Date() &&
               this.task.status !== 'completed';
    }

    startEditTitle(): void {
        this.titleDraft.set(this.task.title);
        this.editingTitle.set(true);
    }

    saveTitle(): void {
        if (this.titleDraft().trim()) {
            // Emit to parent — parent calls the API
            // (component should not directly call services for simple updates)
        }
        this.editingTitle.set(false);
    }

    onComplete(): void {
        this.completed.emit(this.task._id);
    }

    onDelete(): void {
        this.deleted.emit(this.task._id);
    }
}
<!-- task-card.component.html -- all four binding types demonstrated -->
<div class="task-card"
     [class.task-card--completed]="task.status === 'completed'"
     [class.task-card--overdue]="isOverdue"
     [attr.data-task-id]="task._id">

    <!-- ── Interpolation: display text values ────────────────────────── -->
    <h3 class="task-card__title">{{ task.title }}</h3>
    <p>Priority: {{ task.priority | titlecase }}</p>
    <p>Due: {{ task.dueDate | date:'mediumDate' }}</p>
    <p>{{ isOverdue ? 'OVERDUE' : 'On track' }}</p>

    <!-- ── Property binding: set DOM properties ──────────────────────── -->
    <img [src]="'/assets/icons/' + task.priority + '.svg'"
         [alt]="task.priority + ' priority icon'"
         [title]="'Priority: ' + task.priority">

    <span [class]="'badge badge--' + task.status">
        {{ task.status }}
    </span>

    <!-- Binding multiple classes conditionally -->
    <div [ngClass]="{
        'highlight': task.priority === 'high',
        'dimmed':    task.status === 'completed',
        'overdue':   isOverdue
    }">
        {{ task.description }}
    </div>

    <!-- Binding inline styles -->
    <div [style.opacity]="task.status === 'completed' ? '0.6' : '1'"
         [style.text-decoration]="task.status === 'completed' ? 'line-through' : 'none'">
        {{ task.title }}
    </div>

    <!-- ── Event binding: listen to DOM events ───────────────────────── -->
    <button (click)="isExpanded.set(!isExpanded())">
        {{ isExpanded() ? 'Collapse' : 'Expand' }}
    </button>

    <button (click)="onComplete()"
            [disabled]="task.status === 'completed'">
        Mark Complete
    </button>

    <button (click)="onDelete()"
            (mouseenter)="$event.target.classList.add('hover')"
            (mouseleave)="$event.target.classList.remove('hover')">
        Delete
    </button>

    <!-- Handle keyboard events -->
    <input (keydown.enter)="saveTitle()"
           (keydown.escape)="editingTitle.set(false)"
           [placeholder]="'Edit: ' + task.title">

    <!-- ── Two-way binding: sync input and class property ────────────── -->
    <div *ngIf="editingTitle()">
        <input [(ngModel)]="titleDraft"
               placeholder="Edit title..."
               (keydown.enter)="saveTitle()">
        <!-- [(ngModel)] requires FormsModule in imports -->
        <!-- Equivalent: [ngModel]="titleDraft" (ngModelChange)="titleDraft.set($event)" -->
    </div>

    <!-- ── Event emitting to parent component ────────────────────────── -->
    <!-- Parent template: <app-task-card (completed)="handleComplete($event)"> -->
    <button (click)="completed.emit(task._id)">Complete</button>

</div>

<!-- Expanded section - shown based on signal value -->
<div *ngIf="isExpanded()" class="task-card__details">
    <p>{{ task.description }}</p>
    <ul>
        <li *ngFor="let tag of task.tags">{{ tag }}</li>
    </ul>
</div>

How It Works

Step 1 — Interpolation Converts Expressions to Strings

Angular evaluates the expression inside {{ }} at runtime and converts the result to a string for display. The expression can be a property name, a method call, a ternary, arithmetic, or a pipe transform. Angular re-evaluates and updates the DOM whenever the component’s change detection runs. Interpolation automatically HTML-encodes special characters — < becomes &lt; — preventing XSS.

Step 2 — Property Binding Sets JavaScript Properties

[property]="expression" evaluates the expression and assigns the result to the DOM element’s JavaScript property (not the HTML attribute). [src]="imageUrl" is equivalent to element.src = imageUrl in JavaScript. Angular updates the property binding whenever the expression’s value changes during change detection. Property bindings accept any TypeScript expression — variables, method calls, object literals.

Step 3 — Event Binding Listens to DOM Events

(click)="handler($event)" adds an event listener equivalent to element.addEventListener('click', handler). The $event object is the native browser event. Angular also supports event filtering: (keydown.enter) only fires when the Enter key is pressed. When the handler executes, Angular runs change detection afterwards to update bindings that may have changed as a result.

Step 4 — Two-Way Binding Is Property + Event Combined

[(ngModel)]="property" is syntactic sugar for [ngModel]="property" (ngModelChange)="property = $event". The square bracket [ngModel] sets the input’s value from the class property. The round bracket (ngModelChange) updates the class property when the input changes. The combination keeps both the view and the class in sync — any change to either is reflected in the other immediately.

Step 5 — Banana in a Box: Understanding [()] Syntax

The [()] syntax (called “banana in a box”) is Angular’s two-way binding notation. It works for any component that follows the convention: an input property named X and an output event named XChange. [(ngModel)] works because ngModel has both an @Input() ngModel and an @Output() ngModelChange. You can create your own two-way bindable components following the same pattern.

Common Mistakes

Mistake 1 — Using attribute syntax instead of property binding for dynamic values

❌ Wrong — string concatenation in HTML attribute does not update dynamically:

<img src="/images/{{ user.avatar }}">
<!-- This works for initial render but can cause a broken request before Angular processes it -->

✅ Correct — use property binding for dynamic values:

<img [src]="'/images/' + user.avatar">

Mistake 2 — Forgetting FormsModule for ngModel

❌ Wrong — ngModel not available, template error:

@Component({
    standalone: true,
    imports:    [CommonModule],  // FormsModule missing!
    template:   `<input [(ngModel)]="title">`
})
// Error: Can't bind to 'ngModel' since it isn't a known property of 'input'

✅ Correct — add FormsModule to imports:

@Component({
    standalone: true,
    imports: [CommonModule, FormsModule],
    template: `<input [(ngModel)]="title">`
})

Mistake 3 — Calling methods in templates that have side effects

❌ Wrong — getFilteredTasks() called on every change detection cycle:

<li *ngFor="let task of getFilteredTasks()">
<!-- getFilteredTasks() is called many times per second — avoid API calls or heavy computation here -->

✅ Correct — compute derived data in the class as a getter or signal:

filteredTasks = computed(() =>
    this.tasks().filter(t => t.status === this.filterStatus())
);
// Template: *ngFor="let task of filteredTasks()"
// Only recomputed when tasks() or filterStatus() changes

Quick Reference

Binding Syntax Example
Interpolation {{ expr }} {{ task.title }}
Property [prop]="expr" [disabled]="form.invalid"
Attribute [attr.name]="expr" [attr.aria-label]="label"
Class toggle [class.name]="bool" [class.active]="isSelected"
Style [style.prop]="expr" [style.color]="'red'"
Event (event)="fn($event)" (click)="onSave()"
Key event (keydown.enter)="fn()" (keydown.escape)="cancel()"
Two-way [(ngModel)]="prop" [(ngModel)]="searchQuery"
NgClass [ngClass]="obj" [ngClass]="{ active: isActive }"
NgStyle [ngStyle]="obj" [ngStyle]="{ color: 'red' }"

🧠 Test Yourself

An Angular template needs to disable a button when a form is invalid and also show a loading spinner. Which binding disables the button?