Skip to content

Data Binding in Angular

What is Data Binding?

Data binding is a technique that connects the application's UI (template) with the component's data (business logic). Angular provides several types of data binding that enable automatic synchronization between the component and the template.

Types of Data Binding

Angular supports four main types of data binding:

  1. Interpolation - Component to Template (one-way)
  2. Property Binding - Component to Template (one-way)
  3. Event Binding - Template to Component (one-way)
  4. Two-way Binding - Bidirectional
┌─────────────────────────────────────────────────────────┐
│                 DATA BINDING FLOW                       │
├─────────────────────────────────────────────────────────┤
│  Component ──────────────────────────► Template        │
│            Interpolation {{ }}                          │
│            Property Binding [ ]                         │
│                                                         │
│  Component ◄────────────────────────── Template        │
│            Event Binding ( )                            │
│                                                         │
│  Component ◄──────────────────────────► Template       │
│            Two-way Binding [( )]                        │
└─────────────────────────────────────────────────────────┘

Interpolation

Basic Interpolation

Interpolation embeds expressions into marked up text using double curly braces:

export class InterpolationComponent {
  title = 'Data Binding Demo';
  userName = 'John Doe';
  age = 30;
  isLoggedIn = true;
  currentDate = new Date();

  calculateAge(birthYear: number): number {
    return new Date().getFullYear() - birthYear;
  }

  getGreeting(): string {
    const hour = new Date().getHours();
    if (hour < 12) return 'Good morning';
    if (hour < 18) return 'Good afternoon';
    return 'Good evening';
  }
}
<!-- Simple property interpolation -->
<h1>{{ title }}</h1>
<p>Welcome, {{ userName }}!</p>
<p>Age: {{ age }}</p>

<!-- Boolean values -->
<p>Status: {{ isLoggedIn ? 'Online' : 'Offline' }}</p>

<!-- Method calls -->
<p>{{ getGreeting() }}, {{ userName }}!</p>
<p>Age calculated: {{ calculateAge(1993) }}</p>

<!-- Expressions -->
<p>Next year you'll be {{ age + 1 }} years old</p>
<p>Your name has {{ userName.length }} characters</p>

<!-- Date with pipe -->
<p>Current date: {{ currentDate | date:'fullDate' }}</p>

Advanced Interpolation

export class AdvancedInterpolationComponent {
  user = {
    firstName: 'John',
    lastName: 'Doe',
    profile: {
      bio: 'Software Developer',
      skills: ['Angular', 'TypeScript', 'JavaScript']
    }
  };

  products = [
    { name: 'Laptop', price: 999.99, inStock: true },
    { name: 'Mouse', price: 29.99, inStock: false },
    { name: 'Keyboard', price: 79.99, inStock: true }
  ];

  config = {
    theme: 'dark',
    language: 'en',
    showAdvanced: true
  };
}
<!-- Object property access -->
<h2>{{ user.firstName }} {{ user.lastName }}</h2>
<p>{{ user.profile.bio }}</p>

<!-- Array access and operations -->
<p>Skills: {{ user.profile.skills.join(', ') }}</p>
<p>First skill: {{ user.profile.skills[0] }}</p>
<p>Number of skills: {{ user.profile.skills.length }}</p>

<!-- Safe navigation operator -->
<p>Bio: {{ user.profile?.bio || 'No bio available' }}</p>

<!-- Complex expressions -->
<p>Available products: {{ products.filter(p => p.inStock).length }}</p>
<p>Total value: {{ products.reduce((sum, p) => sum + p.price, 0) | currency }}</p>

<!-- Conditional interpolation -->
<p>Theme: {{ config.theme === 'dark' ? '🌙 Dark Mode' : '☀️ Light Mode' }}</p>

Property Binding

Property binding sets the property of a target element or directive using square brackets:

Basic Property Binding

export class PropertyBindingComponent {
  imageUrl = 'assets/images/angular-logo.png';
  imageAlt = 'Angular Logo';
  buttonText = 'Click me!';
  isDisabled = false;
  inputValue = 'Hello Angular';
  linkUrl = 'https://angular.io';
  dynamicId = 'unique-element-123';

  toggleButton() {
    this.isDisabled = !this.isDisabled;
    this.buttonText = this.isDisabled ? 'Disabled' : 'Click me!';
  }
}
<!-- Image properties -->
<img [src]="imageUrl" [alt]="imageAlt" [width]="200">

<!-- Button properties -->
<button
  [disabled]="isDisabled"
  [textContent]="buttonText"
  (click)="toggleButton()">
</button>

<!-- Input properties -->
<input
  [value]="inputValue"
  [placeholder]="'Enter text here'"
  [maxlength]="50">

<!-- Link properties -->
<a [href]="linkUrl" [target]="'_blank'">Visit Angular</a>

<!-- Dynamic properties -->
<div [id]="dynamicId" [title]="'Dynamic title: ' + dynamicId">
  Dynamic content
</div>

HTML Attribute vs DOM Property

Understanding the difference between HTML attributes and DOM properties:

<!-- Attribute binding (use attr. prefix) -->
<button [attr.aria-label]="buttonText">{{ buttonText }}</button>
<input [attr.data-user-id]="user.id">
<div [attr.role]="'button'" [attr.tabindex]="0">Custom Button</div>

<!-- Property binding (direct property access) -->
<button [disabled]="isDisabled">Button</button>
<input [value]="inputValue">
<img [src]="imageUrl">

<!-- CSS class binding -->
<div [class]="cssClass">Dynamic class</div>
<div [class.active]="isActive">Conditional class</div>

<!-- Style binding -->
<div [style.color]="textColor">Colored text</div>
<div [style.font-size.px]="fontSize">Sized text</div>

Advanced Property Binding

export class AdvancedPropertyBindingComponent {
  user = {
    id: 1,
    name: 'John Doe',
    isActive: true,
    role: 'admin'
  };

  theme = {
    primaryColor: '#007bff',
    backgroundColor: '#f8f9fa',
    fontSize: 16
  };

  formConfig = {
    showLabels: true,
    isReadonly: false,
    maxLength: 100
  };

  getBackgroundColor(): string {
    return this.user.isActive ? this.theme.primaryColor : '#6c757d';
  }
}
<!-- Object property binding -->
<div [attr.data-user-id]="user.id">
  <span [textContent]="user.name"></span>
</div>

<!-- Dynamic styling -->
<div
  [style.background-color]="getBackgroundColor()"
  [style.color]="user.isActive ? 'white' : 'gray'"
  [style.font-size.px]="theme.fontSize">
  User Status
</div>

<!-- Conditional attributes -->
<input
  [attr.readonly]="formConfig.isReadonly ? '' : null"
  [attr.maxlength]="formConfig.maxLength"
  [placeholder]="formConfig.showLabels ? 'Enter name' : ''">

<!-- Complex property expressions -->
<button [disabled]="!user.isActive || user.role !== 'admin'">
  Admin Action
</button>

Event Binding

Event binding listens to user actions and responds to them using parentheses:

Basic Event Binding

export class EventBindingComponent {
  message = '';
  clickCount = 0;
  inputValue = '';
  selectedOption = '';

  onButtonClick() {
    this.clickCount++;
    this.message = `Button clicked ${this.clickCount} times`;
  }

  onInputChange(event: Event) {
    const target = event.target as HTMLInputElement;
    this.inputValue = target.value;
    this.message = `You typed: ${this.inputValue}`;
  }

  onKeyPress(event: KeyboardEvent) {
    if (event.key === 'Enter') {
      this.message = 'Enter key pressed!';
    }
  }

  onSelectChange(event: Event) {
    const target = event.target as HTMLSelectElement;
    this.selectedOption = target.value;
    this.message = `Selected: ${this.selectedOption}`;
  }

  onFormSubmit(event: Event) {
    event.preventDefault();
    this.message = 'Form submitted!';
  }
}
<!-- Basic click events -->
<button (click)="onButtonClick()">Click me</button>
<p>{{ message }}</p>
<p>Click count: {{ clickCount }}</p>

<!-- Input events -->
<input
  type="text"
  (input)="onInputChange($event)"
  (keypress)="onKeyPress($event)"
  placeholder="Type something...">

<p>Current value: {{ inputValue }}</p>

<!-- Select events -->
<select (change)="onSelectChange($event)">
  <option value="">Choose option</option>
  <option value="option1">Option 1</option>
  <option value="option2">Option 2</option>
  <option value="option3">Option 3</option>
</select>

<!-- Form events -->
<form (submit)="onFormSubmit($event)">
  <input type="text" placeholder="Enter text">
  <button type="submit">Submit</button>
</form>

Advanced Event Binding

export class AdvancedEventBindingComponent {
  mousePosition = { x: 0, y: 0 };
  isDragging = false;
  dragData = '';
  uploadedFiles: File[] = [];

  onMouseMove(event: MouseEvent) {
    this.mousePosition = { x: event.clientX, y: event.clientY };
  }

  onMouseDown() {
    this.isDragging = true;
  }

  onMouseUp() {
    this.isDragging = false;
  }

  onDragStart(event: DragEvent) {
    this.dragData = 'Dragged data';
    event.dataTransfer?.setData('text/plain', this.dragData);
  }

  onDrop(event: DragEvent) {
    event.preventDefault();
    const data = event.dataTransfer?.getData('text/plain');
    console.log('Dropped data:', data);
  }

  onDragOver(event: DragEvent) {
    event.preventDefault(); // Allow drop
  }

  onFileUpload(event: Event) {
    const target = event.target as HTMLInputElement;
    if (target.files) {
      this.uploadedFiles = Array.from(target.files);
    }
  }

  onCustomEvent(customData: any) {
    console.log('Custom event received:', customData);
  }
}
<!-- Mouse events -->
<div
  class="mouse-tracker"
  (mousemove)="onMouseMove($event)"
  (mousedown)="onMouseDown()"
  (mouseup)="onMouseUp()">
  <p>Mouse position: {{ mousePosition.x }}, {{ mousePosition.y }}</p>
  <p>Dragging: {{ isDragging ? 'Yes' : 'No' }}</p>
</div>

<!-- Keyboard events with key filters -->
<input
  (keyup.enter)="onEnterPressed()"
  (keyup.escape)="onEscapePressed()"
  (keyup.space)="onSpacePressed()"
  placeholder="Press Enter, Escape, or Space">

<!-- Drag and drop events -->
<div
  draggable="true"
  (dragstart)="onDragStart($event)"
  class="draggable-item">
  Drag me
</div>

<div
  class="drop-zone"
  (drop)="onDrop($event)"
  (dragover)="onDragOver($event)">
  Drop here
</div>

<!-- File upload -->
<input
  type="file"
  multiple
  (change)="onFileUpload($event)">

<div *ngFor="let file of uploadedFiles">
  {{ file.name }} ({{ file.size }} bytes)
</div>

<!-- Custom component events -->
<app-custom-component (customEvent)="onCustomEvent($event)">
</app-custom-component>

Event Object and $event

export class EventObjectComponent {
  lastEvent: any = null;

  analyzeEvent(event: Event) {
    this.lastEvent = {
      type: event.type,
      target: (event.target as HTMLElement).tagName,
      timestamp: event.timeStamp
    };
  }

  preventDefaultExample(event: Event) {
    event.preventDefault();
    console.log('Default action prevented');
  }

  stopPropagationExample(event: Event) {
    event.stopPropagation();
    console.log('Event propagation stopped');
  }
}
<!-- Accessing event object -->
<button (click)="analyzeEvent($event)">Analyze Event</button>
<pre *ngIf="lastEvent">{{ lastEvent | json }}</pre>

<!-- Preventing default behavior -->
<a href="https://example.com" (click)="preventDefaultExample($event)">
  Prevented Link
</a>

<!-- Stopping event propagation -->
<div (click)="onParentClick()">
  Parent
  <button (click)="stopPropagationExample($event)">
    Child Button
  </button>
</div>

Two-Way Data Binding

Two-way data binding combines property and event binding using the "banana in a box" syntax [()]:

Basic Two-Way Binding

export class TwoWayBindingComponent {
  name = '';
  email = '';
  age = 0;
  isSubscribed = false;
  selectedColor = 'blue';
  rating = 5;

  onFormSubmit() {
    console.log('Form data:', {
      name: this.name,
      email: this.email,
      age: this.age,
      isSubscribed: this.isSubscribed,
      selectedColor: this.selectedColor,
      rating: this.rating
    });
  }
}
<!-- Basic two-way binding with ngModel -->
<form (submit)="onFormSubmit()">
  <div>
    <label>Name:</label>
    <input [(ngModel)]="name" type="text" placeholder="Enter your name">
    <p>Hello, {{ name }}!</p>
  </div>

  <div>
    <label>Email:</label>
    <input [(ngModel)]="email" type="email" placeholder="Enter your email">
    <p>Email: {{ email }}</p>
  </div>

  <div>
    <label>Age:</label>
    <input [(ngModel)]="age" type="number" min="0" max="120">
    <p>Age: {{ age }}</p>
  </div>

  <div>
    <label>
      <input [(ngModel)]="isSubscribed" type="checkbox">
      Subscribe to newsletter
    </label>
    <p>Subscribed: {{ isSubscribed ? 'Yes' : 'No' }}</p>
  </div>

  <div>
    <label>Favorite Color:</label>
    <select [(ngModel)]="selectedColor">
      <option value="red">Red</option>
      <option value="green">Green</option>
      <option value="blue">Blue</option>
    </select>
    <p>Selected: {{ selectedColor }}</p>
  </div>

  <div>
    <label>Rating:</label>
    <input [(ngModel)]="rating" type="range" min="1" max="10">
    <p>Rating: {{ rating }}/10</p>
  </div>

  <button type="submit">Submit</button>
</form>

Custom Two-Way Binding

Create components that support two-way binding:

// counter.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';

@Component({
  selector: 'app-counter',
  template: `
    <div class="counter">
      <button (click)="decrement()">-</button>
      <span class="count">{{ value }}</span>
      <button (click)="increment()">+</button>
    </div>
  `,
  styles: [`
    .counter {
      display: flex;
      align-items: center;
      gap: 10px;
    }
    .count {
      min-width: 40px;
      text-align: center;
      font-weight: bold;
    }
    button {
      width: 30px;
      height: 30px;
    }
  `]
})
export class CounterComponent {
  @Input() value = 0;
  @Output() valueChange = new EventEmitter<number>();

  increment() {
    this.value++;
    this.valueChange.emit(this.value);
  }

  decrement() {
    this.value--;
    this.valueChange.emit(this.value);
  }
}
// parent.component.ts
export class ParentComponent {
  count = 5;

  onCountChanged() {
    console.log('Count changed to:', this.count);
  }
}
<!-- parent.component.html -->
<h3>Custom Two-Way Binding</h3>
<app-counter [(value)]="count"></app-counter>
<p>Current count: {{ count }}</p>

<!-- This is equivalent to: -->
<app-counter
  [value]="count"
  (valueChange)="count = $event">
</app-counter>

Advanced Two-Way Binding

// color-picker.component.ts
@Component({
  selector: 'app-color-picker',
  template: `
    <div class="color-picker">
      <input
        type="color"
        [value]="color"
        (input)="onColorChange($event)">
      <input
        type="text"
        [value]="color"
        (input)="onTextChange($event)"
        placeholder="#000000">
      <div
        class="color-preview"
        [style.background-color]="color">
      </div>
    </div>
  `,
  styles: [`
    .color-picker {
      display: flex;
      gap: 10px;
      align-items: center;
    }
    .color-preview {
      width: 40px;
      height: 40px;
      border: 1px solid #ccc;
      border-radius: 4px;
    }
  `]
})
export class ColorPickerComponent {
  @Input() color = '#000000';
  @Output() colorChange = new EventEmitter<string>();

  onColorChange(event: Event) {
    const target = event.target as HTMLInputElement;
    this.color = target.value;
    this.colorChange.emit(this.color);
  }

  onTextChange(event: Event) {
    const target = event.target as HTMLInputElement;
    const value = target.value;
    if (this.isValidColor(value)) {
      this.color = value;
      this.colorChange.emit(this.color);
    }
  }

  private isValidColor(color: string): boolean {
    return /^#[0-9A-F]{6}$/i.test(color);
  }
}

Data Binding Performance

Change Detection and Data Binding

export class PerformanceComponent {
  users: User[] = [];
  searchTerm = '';

  // Getter - recalculated on every change detection cycle
  get filteredUsers(): User[] {
    console.log('Filtering users...'); // This will log frequently
    return this.users.filter(user =>
      user.name.toLowerCase().includes(this.searchTerm.toLowerCase())
    );
  }

  // Method - only called when explicitly invoked
  getFilteredUsers(): User[] {
    console.log('Filtering users via method...');
    return this.users.filter(user =>
      user.name.toLowerCase().includes(this.searchTerm.toLowerCase())
    );
  }

  // Property - manually updated when needed
  private _filteredUsers: User[] = [];

  updateFilteredUsers() {
    this._filteredUsers = this.users.filter(user =>
      user.name.toLowerCase().includes(this.searchTerm.toLowerCase())
    );
  }

  get cachedFilteredUsers(): User[] {
    return this._filteredUsers;
  }
}
<!-- Avoid - getter called on every change detection -->
<div *ngFor="let user of filteredUsers">{{ user.name }}</div>

<!-- Better - method called only when needed -->
<div *ngFor="let user of getFilteredUsers()">{{ user.name }}</div>

<!-- Best - cached property -->
<input [(ngModel)]="searchTerm" (input)="updateFilteredUsers()">
<div *ngFor="let user of cachedFilteredUsers">{{ user.name }}</div>

OnPush Change Detection Strategy

import { ChangeDetectionStrategy } from '@angular/core';

@Component({
  selector: 'app-optimized',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div>
      <p>{{ data.value }}</p>
      <button (click)="updateData()">Update</button>
    </div>
  `
})
export class OptimizedComponent {
  @Input() data!: { value: string };

  constructor(private cdr: ChangeDetectorRef) {}

  updateData() {
    // Create new object reference for OnPush to detect change
    this.data = { ...this.data, value: 'Updated' };

    // Or manually trigger change detection
    this.cdr.detectChanges();
  }
}

Data Binding Best Practices

1. Use the Right Binding Type

<!-- Use interpolation for simple text -->
<p>{{ userName }}</p>

<!-- Use property binding for element properties -->
<img [src]="imageUrl" [alt]="imageDescription">

<!-- Use event binding for user actions -->
<button (click)="handleClick()">Click me</button>

<!-- Use two-way binding for form controls -->
<input [(ngModel)]="inputValue">

2. Avoid Complex Expressions

<!-- Bad - complex logic in template -->
<p>{{ users.filter(u => u.active).map(u => u.name).join(', ') }}</p>

<!-- Good - move logic to component -->
<p>{{ activeUserNames }}</p>
get activeUserNames(): string {
  return this.users
    .filter(user => user.active)
    .map(user => user.name)
    .join(', ');
}

3. Use Safe Navigation

<!-- Safe navigation prevents errors -->
<p>{{ user?.profile?.bio }}</p>
<img [src]="user?.avatar || 'default-avatar.png'">

4. Optimize Change Detection

// Use trackBy for ngFor
trackByUserId(index: number, user: User): number {
  return user.id;
}
<div *ngFor="let user of users; trackBy: trackByUserId">
  {{ user.name }}
</div>

5. Handle Async Data Properly

export class AsyncDataComponent {
  user$ = this.userService.getUser(1);

  // Or with async handling
  user: User | null = null;
  isLoading = true;

  ngOnInit() {
    this.userService.getUser(1).subscribe(user => {
      this.user = user;
      this.isLoading = false;
    });
  }
}
<!-- Using async pipe -->
<div *ngIf="user$ | async as user">
  <h2>{{ user.name }}</h2>
  <p>{{ user.email }}</p>
</div>

<!-- Manual async handling -->
<div *ngIf="isLoading">Loading...</div>
<div *ngIf="!isLoading && user">
  <h2>{{ user.name }}</h2>
  <p>{{ user.email }}</p>
</div>

Common Data Binding Pitfalls

1. Mutating Objects

// Bad - mutating existing object
updateUser() {
  this.user.name = 'New Name'; // May not trigger change detection with OnPush
}

// Good - creating new object
updateUser() {
  this.user = { ...this.user, name: 'New Name' };
}

2. Memory Leaks

// Bad - no cleanup
ngOnInit() {
  this.dataService.getData().subscribe(data => {
    this.data = data;
  });
}

// Good - cleanup subscription
private destroy$ = new Subject<void>();

ngOnInit() {
  this.dataService.getData()
    .pipe(takeUntil(this.destroy$))
    .subscribe(data => {
      this.data = data;
    });
}

ngOnDestroy() {
  this.destroy$.next();
  this.destroy$.complete();
}

3. Incorrect Event Handling

<!-- Bad - not passing event object when needed -->
<input (input)="onInputChange()">

<!-- Good - passing event object -->
<input (input)="onInputChange($event)">

Summary

Data binding in Angular provides powerful ways to connect components and templates:

  • Interpolation displays component data in templates
  • Property binding sets element properties dynamically
  • Event binding responds to user interactions
  • Two-way binding synchronizes data between component and template

Key concepts:

  • Choose the right binding type for each scenario
  • Keep expressions simple and move complex logic to components
  • Use safe navigation to handle null/undefined values
  • Optimize performance with proper change detection strategies
  • Handle async data appropriately

Next Steps

Now that you understand data binding, you're ready to explore:

  1. Angular directives (structural and attribute)
  2. Forms and form validation
  3. Component communication patterns
  4. Observables and async data handling
  5. Performance optimization techniques

Data binding is fundamental to Angular development - master these concepts to build responsive and interactive applications!