Menu

Official website

Data Binding in Angular: Templates, Interpolation, and Property/Event Binding


15 Oct 2025

min read

Data binding is the heartbeat of Angular templates. Learn how data flows between components and templates using interpolation, property binding, event binding, and two-way binding. This topic also introduces new Angular 20 template features like template literals. We’ll keep the focus practical: clear patterns, minimal surprises, and maintainable code that scales.

The mental model: unidirectional data flow, well-timed feedback

  • From component to view: interpolation and property binding render state.

  • From view to component: event binding captures user intent.

  • Two-way binding is a convenience that composes the two, not a separate mechanism.

Favor predictable one-way flows; reach for two-way when it improves clarity without hiding complexity.

Interpolation: simple, readable, one-way display

Interpolation renders values into text nodes using template syntax like {{ …​ }}. It’s ideal for human-readable content.

<h2>{{ title }}</h2>
<p>Welcome, {{ user?.name ?? 'Guest' }}.</p>
<p>Subtotal: {{ price | currency: 'USD' }}</p>

Guidelines:

  • Keep expressions simple. Avoid calling methods that do non-trivial work.

  • Use pipes for presentation logic (formatting, slicing, etc.).

  • Use the safe-navigation operator (?.) to avoid null/undefined errors at render time.

Angular 20 template literals in expressions

Angular 20 enables JavaScript-style template literals inside template expressions for more expressive string composition.

<h2>{{ `${greeting}, ${user?.firstName || 'friend'}!` }}</h2>
<button [attr.aria-label]="`Open details for ${item.title}`">Open</button>

Use them for readable, localized strings or ARIA attributes. Keep them concise. If you’re building complex messages, consider a dedicated formatter or i18n.

Property binding: write to DOM and component inputs

Property binding uses square brackets to set DOM properties, ARIA attributes, styles, classes, and child component inputs.

<!-- DOM properties -->
<input [value]="query()" [disabled]="isSubmitting()" />

<!-- Attributes and accessibility -->
<button [attr.aria-pressed]="isActive" [attr.data-id]="item.id">Toggle</button>

<!-- Classes and styles -->
<div [class.active]="isActive" [style.width.px]="panelWidth"></div>

<!-- Child component inputs -->
<app-avatar [src]="user.photoUrl" [alt]="user.name" [size]="64"></app-avatar>

Under the hood, Angular binds to the actual property when available (safer and more correct than raw attributes). Use [attr.*] only when there is no matching property.

Signals play nicely with bindings

Signals provide push-based reactivity. Call them in templates to read their current value.

import { ChangeDetectionStrategy, Component, computed, signal } from '@angular/core';

@Component({
  selector: 'app-search',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <label>
      Query:
      <input [value]="query()" (input)="onInput($event)" placeholder="Type to search..." />
    </label>

    @if (filtered().length === 0) {
      <p>No results</p>
    } @else {
      <ul>
        @for (item of filtered(); track item) {
          <li>{{ item }}</li>
        }
      </ul>
    }
  `
})
export class SearchComponent {
  readonly items = signal(['apple', 'banana', 'citrus', 'date', 'elderberry']);
  readonly query = signal('');
  readonly filtered = computed(() =>
    this.items().filter(v => v.toLowerCase().includes(this.query().toLowerCase()))
  );

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

Note:

  • Property binding [value]="query()" reads the signal.

  • Event binding (input)="…​" updates the signal.

  • Built-in control flow (@if and @for) keeps the template declarative and fast.

Event binding: user intent back to the component

Event binding uses parentheses to listen to DOM or component events.

<button (click)="toggle()">{{ isOpen ? 'Close' : 'Open' }}</button>
<input (keydown.enter)="submit()" />
<input (input)="onInput($event)" />

Type your handlers for safety and clarity.

toggle(): void {
  this.isOpen = !this.isOpen;
}

submit(): void {
  if (!this.formValid) return;
  this.save();
}

onInput(event: Event): void {
  const target = event.target as HTMLInputElement;
  this.value = target.value;
}
  • Prefer specific event names like keydown.enter for intent-revealing handlers.

  • Avoid expensive work inside template expressions; do it in component methods or computed signals.

Two-way binding: when it helps, use it intentionally

Two-way binding composes property binding and event binding into a single banana-in-a-box syntax.

With forms (ngModel)

This is convenient in simple forms. Import FormsModule in standalone components.

import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-preferences',
  standalone: true,
  imports: [FormsModule],
  template: `
    <label>Nickname: <input [(ngModel)]="nickname" /></label>
    <label>
      <input type="checkbox" [(ngModel)]="emailOptIn" />
      Email me updates
    </label>
    <p>Preview: {{ nickname }} (opt-in: {{ emailOptIn }})</p>
  `
})
export class PreferencesComponent {
  nickname = '';
  emailOptIn = true;
}

With signals, bridge two-way binding by pairing [ngModel] and (ngModelChange):

<input [ngModel]="query()" (ngModelChange)="query.set($event)" />

Custom two-way binding for child components

Define a pair of Input and Output with the Change suffix. Angular will recognize [(value)].

import { Component, EventEmitter, Input, Output } from '@angular/core';
import { CommonModule } from '@angular/common';

@Component({
  selector: 'app-rating',
  standalone: true,
  imports: [CommonModule],
  template: `
    <div class="stars">
      <button *ngFor="let s of [1,2,3,4,5]"
              type="button"
              [class.filled]="s <= value"
              (click)="setValue(s)"
              [attr.aria-label]="'Set rating to ' + s">
        ★
      </button>
    </div>
  `,
  styles: [`.filled { color: #f59e0b; } button { background:none; border:none; font-size:1.25rem; cursor:pointer; }`]
})
export class RatingComponent {
  @Input() value = 0;
  @Output() valueChange = new EventEmitter<number>();

  setValue(v: number) {
    this.value = v;
    this.valueChange.emit(v);
  }
}

Parent usage:

<app-rating [(value)]="product.rating"></app-rating>
<p>Current rating: {{ product.rating }}</p>

Tip: If your project uses the signal-based inputs API, the model() helper can reduce boilerplate. The classic pair above remains broadly compatible and explicit.

Template syntax patterns that scale

  • Keep expressions pure and cheap. Avoid calling functions that allocate arrays or perform filtering on each change detection; use computed signals or memoized selectors.

  • Bind to properties, not attributes. Prefer [value], [disabled], [checked] over [attr.value], except when no property exists.

  • Use track with @for to avoid re-rendering stable items: @for (item of items; track item.id) { …​ }

  • For [innerHTML], sanitize or trust only safe content. Angular’s DomSanitizer exists for exceptional cases—prefer plain text whenever possible.

Angular 20 template literal examples in context

Use template literals where they improve clarity, especially for ARIA and labeling.

<!-- Readable, localized-friendly labels -->
<button
  type="button"
  [attr.aria-label]="`${isPlaying ? 'Pause' : 'Play'} ${track.title}`"
  (click)="togglePlay()">
  {{ isPlaying ? 'Pause' : 'Play' }}
</button>

<!-- Combining with pipes -->
<p>{{ `Hello, ${user?.name || 'Guest'}` | titlecase }}</p>

<!-- Attributes that aren’t DOM properties -->
<a [attr.data-test-id]="`link-${link.id}`" [href]="link.url">{{ link.title }}</a>

Keep logic minimal; push complex decisions into the component or computed signals for testability.

A cohesive example: binding the pieces

import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { RatingComponent } from './rating.component';

@Component({
  selector: 'app-product-card',
  standalone: true,
  imports: [CommonModule, FormsModule, RatingComponent],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <article class="card">
      <header>
        <h3>{{ product().name }}</h3>
        <p class="price">{{ product().price | currency:'USD' }}</p>
      </header>

      <label [attr.aria-label]="`Quantity for ${product().name}`">
        Qty:
        <input type="number"
               min="1"
               [ngModel]="qty()"
               (ngModelChange)="qty.set($event)" />
      </label>

      <app-rating [(value)]="rating"></app-rating>

      <p>{{ description }}</p>

      <button type="button"
              class="add"
              [disabled]="isSubmitting()"
              (click)="addToCart()">
        {{ isSubmitting() ? 'Adding…' : 'Add to cart' }}
      </button>
    </article>
  `,
  styles: [`.card{border:1px solid #e5e7eb;border-radius:.5rem;padding:1rem}.price{color:#374151}.add{margin-top:.75rem}`]
})
export class ProductCardComponent {
  readonly product = signal({ id: 1, name: 'Noise-canceling Headphones', price: 199.99 });
  readonly qty = signal(1);
  rating = 4; // two-way bound to child
  readonly isSubmitting = signal(false);

  get description(): string {
    return `${this.product().name} — rated ${this.rating}/5 by our customers.`;
  }

  async addToCart() {
    this.isSubmitting.set(true);
    try {
      await fakeHttpPost({
        id: this.product().id,
        qty: this.qty(),
        rating: this.rating
      });
    } finally {
      this.isSubmitting.set(false);
    }
  }
}

function fakeHttpPost(payload: unknown) {
  return new Promise<void>(r => setTimeout(r, 500));
}

What you see:

  • Interpolation renders human-readable fields.

  • Property binding controls input state and disabled state.

  • Event binding processes clicks and model changes.

  • Two-way binding streamlines rating updates.

  • Template literals keep labels clear and accessible.

Conclusion

Data binding is how intent flows through your Angular app. Interpolation and property binding project state. Event binding captures change. Two-way binding composes both when it reduces friction. With Angular 20’s template literal support, you can craft clearer strings and ARIA labels without sacrificing readability. Keep expressions simple, push complexity into components or computed signals, and lean on built-in control flow for performance.

Next Steps

  • Audit a component for heavy template expressions. Move logic into computed signals or pure methods.

  • Add ARIA attributes with property binding and template literals to improve accessibility.

  • Refactor a custom control to support [(value)] using the input/output pair.

  • Explore Angular’s built-in control flow (@if, @for, @switch) to reduce directive boilerplate.

  • If your team uses signals broadly, standardize patterns for bridging with forms: [ngModel] + (ngModelChange) for crisp two-way behavior.

expand_less