Skip to main content

Data binding in Angular

Data binding in Angular automatically syncs a component's TypeScript properties with its HTML template. No manual DOM manipulation needed.

Theory

TL;DR

  • Think of it like a live scoreboard: the component pushes data to the template, and user actions send events back
  • Four types: interpolation {{ }}, property binding [prop], event binding (event), two-way [(ngModel)]
  • Default direction is one-way (component to template), which is predictable and easier to debug
  • Use [(ngModel)] only for form inputs, not as a general solution

Quick example

typescript
@Component({ template: ` <p>{{ title }}</p> // interpolation: renders text <img [src]="imageUrl"> // property binding: sets DOM property <button (click)="onClick()">Go</button> // event binding: calls method on click <input [(ngModel)]="name"> // two-way: input and property stay in sync <p>You typed: {{ name }}</p> ` }) export class AppComponent { title = 'Hello'; imageUrl = 'https://example.com/logo.png'; name = ''; onClick() { console.log('Clicked!'); } }

The template reads directly from component properties. Change title in the class and the <p> updates on the next change detection cycle.

The four binding types

Interpolation {{ expression }} converts a value to a string and places it in the DOM. Good for text content, not the right tool for DOM properties.

Property binding [property]="value" sets a DOM property directly, not an HTML attribute. That distinction matters: [src]="imageUrl" sets img.src (the DOM property). src="{{ imageUrl }}" sets the HTML attribute and relies on the browser to sync it. For plain strings both look the same, but [disabled]="isLoading" only works correctly as property binding.

Event binding (event)="handler($event)" listens to DOM events and runs a component method. The $event object gives you the native event. No addEventListener in TypeScript, no inline JS in the template.

Two-way binding [(ngModel)]="prop" is shorthand for [ngModel]="prop" (ngModelChange)="prop = $event". The square brackets set the value, the parentheses listen for changes. Requires FormsModule imported in your module.

When to use

SituationBinding type
Display text or computed values{{ value }}
Set attributes, classes, disabled state[property]="value"
Handle clicks, input events, form submit(event)="method()"
Simple form inputs needing two-way sync[(ngModel)]="prop"

For complex state, skip [(ngModel)] and use event binding with property binding separately. Data flows in one direction and stays easy to trace.

How Angular runs this

Angular's compiler (AOT) converts your template into change detection instructions at build time. Zone.js patches browser APIs like addEventListener and setTimeout. When an event fires, Angular walks the component tree and updates only the bindings that changed. With OnPush change detection, it skips components whose inputs haven't changed, which helps with large component trees.

Common mistakes

Mistake 1: Interpolation instead of property binding

html
<!-- Sets the HTML attribute, not the DOM property --> <img src="{{ imageUrl }}"> <!-- Sets img.src directly --> <img [src]="imageUrl">

For dynamic values that are not plain strings, attribute-based interpolation breaks silently. Use property binding for DOM properties.

Mistake 2: Forgetting FormsModule

Error: Can't bind to 'ngModel' since it isn't a known property of 'input'.

Add FormsModule to your @NgModule imports array. In standalone components, import it directly in the component decorator.

Mistake 3: Calling methods in templates

html
<!-- Called on every change detection cycle --> <div>{{ getUserName() }}</div> <!-- Bind to a property instead --> <div>{{ userName }}</div>

This is probably the most common performance issue I see in Angular codebases. Angular runs change detection many times per second during fast user interactions. A method call in a template gets invoked every single cycle.

Mistake 4: Two-way binding on non-form elements

html
<!-- ngModel only works on form controls --> <div [(ngModel)]="data">Content</div>

For custom elements, use the explicit pattern: [value]="data" (input)="data = $event.target.value".

Real-world usage

  • Angular Material buttons: [disabled]="isLoading" while a request is in flight
  • Login forms: [(ngModel)] on simple fields, [formControl] for reactive forms
  • Data tables: (selectionChange)="onSelect($event)" on selection components
  • Dynamic styling: [class.active]="isSelected", [style.color]="textColor"

Follow-up questions

Q: What is the difference between [src] and [attr.src]?
A: [src] sets the DOM property. [attr.src] sets the HTML attribute. Use property binding by default. Attribute binding is mainly useful for ARIA attributes like [attr.aria-label] that have no matching DOM property.

Q: How does [(ngModel)] work internally?
A: It expands to [ngModel]="value" (ngModelChange)="value = $event". The [] part sets the property, the () part listens for changes. You can use this same pattern to build two-way binding into your own components.

Q: Why does binding to a method hurt performance?
A: Angular's change detection runs on every async event. Each template method call executes every cycle. Bind to a property instead and compute the value once in the component class.

Q: Why avoid two-way binding in large apps?
A: It creates bidirectional data flow that is hard to trace. The preferred pattern is unidirectional: data flows down via property binding, events bubble up via event binding. This is also how NgRx and other state managers work.

Examples

Basic: One-way display

typescript
@Component({ template: `<h2>{{ hero.name }} (ID: {{ hero.id }})</h2>` }) export class HeroComponent { hero = { id: 1, name: 'Windstorm' }; // Renders: Windstorm (ID: 1) }

Interpolation reads from the hero object on every change detection cycle. Update hero.name and the template reflects it automatically.

Intermediate: Todo list using all four types

typescript
@Component({ template: ` <ul> <li *ngFor="let todo of todos" [class.completed]="todo.done" (click)="toggle(todo)"> {{ todo.text }} </li> </ul> <input [(ngModel)]="newTodo" (keyup.enter)="add()"> ` }) export class TodoComponent { todos = [{ text: 'Buy milk', done: false }]; newTodo = ''; add() { this.todos.push({ text: this.newTodo, done: false }); this.newTodo = ''; } toggle(todo: { text: string; done: boolean }) { todo.done = !todo.done; } }

All four types in one component. {{ todo.text }} renders text. [class.completed] toggles a CSS class based on a boolean. (click) handles the toggle action. [(ngModel)] keeps the input field in sync with newTodo. Add FormsModule to your module imports for ngModel to work.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?