Angular Component Communication

Understanding how components talk to each other in Angular applications

Introduction to Component Communication

In modern web applications, particularly those built with Angular, the UI is composed of many independent components that need to work together. Understanding how these components share data and notify each other of changes is crucial for building robust, maintainable applications.

Think of Angular components as team members in an office. Each has specific responsibilities, but they need effective communication channels to collaborate. Without proper communication patterns, the team (or application) becomes dysfunctional.

Communication Patterns Overview

graph TD A[Communication Patterns] A --> B[Parent to Child
@Input()] A --> C[Child to Parent
@Output() & EventEmitter] A --> D[Unrelated Components
Services] A --> E[Component to Component
Router Parameters] A --> F[Component to Content
ContentChild & ContentChildren] A --> G[Parent to View
ViewChild & ViewChildren]

Angular provides multiple ways for components to communicate, each appropriate for different relationships and scenarios. We'll explore each of these patterns in detail.

Parent to Child: Input Properties

The most straightforward way for components to communicate is through input properties using the @Input() decorator. This allows parent components to pass data to their children.

Parent Component (product-list.component.ts)


@Component({
  selector: 'app-product-list',
  template: `
    <div>
      <h2>Product List</h2>
      <app-product-card 
        *ngFor="let product of products" 
        [product]="product"
        [showDiscount]="showSpecialOffers">
      </app-product-card>
      
      <button (click)="toggleOffers()">
        {{ showSpecialOffers ? 'Hide' : 'Show' }} Special Offers
      </button>
    </div>
  `
})
export class ProductListComponent {
  products = [
    { id: 1, name: 'Laptop', price: 1299, discount: 0.1 },
    { id: 2, name: 'Phone', price: 799, discount: 0.05 },
    { id: 3, name: 'Headphones', price: 199, discount: 0.15 }
  ];
  
  showSpecialOffers = false;
  
  toggleOffers() {
    this.showSpecialOffers = !this.showSpecialOffers;
  }
}
          

Child Component (product-card.component.ts)


@Component({
  selector: 'app-product-card',
  template: `
    <div class="product-card">
      <h3>{{ product.name }}</h3>
      <p>Price: ${{ product.price }}</p>
      
      <p *ngIf="showDiscount" class="discount">
        Discount: {{ product.discount | percent }}
        <br>
        Final Price: ${{ product.price * (1 - product.discount) | number:'1.2-2' }}
      </p>
    </div>
  `,
  styles: [`
    .product-card {
      border: 1px solid #ddd;
      padding: 15px;
      margin: 10px;
      border-radius: 5px;
    }
    .discount {
      color: green;
      font-weight: bold;
    }
  `]
})
export class ProductCardComponent {
  @Input() product: any;
  @Input() showDiscount = false;
}
          

In this example, ProductListComponent (parent) passes data to ProductCardComponent (child) using two input properties:

This pattern is analogous to a manager (parent) giving instructions and information to team members (children). The information flows in one direction: downward.

Input Property Deep Dive

Input properties can be declared in multiple ways:


// Method 1: Separate input decorator and property declaration
@Input() product: any;

// Method 2: Combined declaration with default value
@Input() showDiscount = false;

// Method 3: Using an alias (HTML will use [specialProduct], component uses myProduct)
@Input('specialProduct') myProduct: any;
        

You can also use getters and setters with input properties for more control:


private _rating = 0;

@Input()
set rating(value: number) {
  this._rating = Math.max(0, Math.min(5, value)); // Ensure rating is between 0-5
  this.ratingChanged.emit(this._rating); // Emit an event when rating changes
}

get rating(): number {
  return this._rating;
}
        

This is particularly useful when you need to:

Handling Input Changes with ngOnChanges

Sometimes you need to react when input properties change. The ngOnChanges lifecycle hook gives you access to current and previous values of inputs.


import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';

@Component({
  selector: 'app-price-tracker',
  template: `
    <div class="tracker">
      <h3>{{ stockSymbol }} Price Tracker</h3>
      <p>Current: ${{ currentPrice }}</p>
      <p [class.increase]="priceChange > 0" [class.decrease]="priceChange < 0">
        Change: {{ priceChange | number:'1.2-2' }}
        ({{ priceChangePercent | percent:'1.2-2' }})
      </p>
    </div>
  `
})
export class PriceTrackerComponent implements OnChanges {
  @Input() stockSymbol: string;
  @Input() currentPrice: number;
  
  priceChange = 0;
  priceChangePercent = 0;
  previousPrice = 0;
  
  ngOnChanges(changes: SimpleChanges) {
    if (changes['currentPrice']) {
      // If this isn't the first change
      if (!changes['currentPrice'].firstChange) {
        const oldValue = changes['currentPrice'].previousValue;
        const newValue = changes['currentPrice'].currentValue;
        
        this.previousPrice = oldValue;
        this.priceChange = newValue - oldValue;
        this.priceChangePercent = this.priceChange / oldValue;
      }
    }
  }
}
        

Real-world application: This pattern is very common in financial dashboards, real-time monitoring systems, or any component that needs to react to changing inputs like stock tickers, IoT sensor dashboards, or analytics reports.

Child to Parent: Output Events

While inputs allow parents to pass data down, outputs enable children to send data and notifications up to their parents. This is implemented using @Output() properties and the EventEmitter class.

sequenceDiagram participant Child Component participant Parent Component Note over Child Component: User interaction triggers event Child Component->>Parent Component: Emit event with data Note over Parent Component: Event handler processes data Parent Component->>Child Component: May update inputs based on event

Child Component (quantity-selector.component.ts)


@Component({
  selector: 'app-quantity-selector',
  template: `
    <div class="quantity-control">
      <button (click)="decrease()" [disabled]="quantity <= 1">-</button>
      <span>{{ quantity }}</span>
      <button (click)="increase()" [disabled]="quantity >= maxQuantity">+</button>
    </div>
  `
})
export class QuantitySelectorComponent {
  @Input() quantity = 1;
  @Input() maxQuantity = 10;
  
  @Output() quantityChanged = new EventEmitter<number>();
  
  increase() {
    if (this.quantity < this.maxQuantity) {
      this.quantity++;
      this.quantityChanged.emit(this.quantity);
    }
  }
  
  decrease() {
    if (this.quantity > 1) {
      this.quantity--;
      this.quantityChanged.emit(this.quantity);
    }
  }
}
          

Parent Component (product-detail.component.ts)


@Component({
  selector: 'app-product-detail',
  template: `
    <div class="product-detail">
      <h2>{{ product.name }}</h2>
      <p>{{ product.description }}</p>
      <p>Price: ${{ product.price }}</p>
      
      <div class="purchase-controls">
        <p>Quantity:</p>
        <app-quantity-selector
          [quantity]="selectedQuantity"
          [maxQuantity]="product.stockCount"
          (quantityChanged)="onQuantityChanged($event)">
        </app-quantity-selector>
        
        <p>Total: ${{ totalPrice | number:'1.2-2' }}</p>
        <button (click)="addToCart()">Add to Cart</button>
      </div>
    </div>
  `
})
export class ProductDetailComponent {
  product = {
    id: 1,
    name: 'Wireless Headphones',
    description: 'Premium noise-cancelling headphones with 20-hour battery life',
    price: 249.99,
    stockCount: 15
  };
  
  selectedQuantity = 1;
  totalPrice = this.product.price;
  
  onQuantityChanged(newQuantity: number) {
    this.selectedQuantity = newQuantity;
    this.totalPrice = this.product.price * this.selectedQuantity;
  }
  
  addToCart() {
    // Add to cart logic
    console.log(`Added ${this.selectedQuantity} ${this.product.name} to cart`);
  }
}
          

In this example:

  1. The child QuantitySelectorComponent has a quantityChanged @Output() that emits events when the quantity changes
  2. The parent ProductDetailComponent listens for these events with (quantityChanged)="onQuantityChanged($event)"
  3. When the event is received, the parent updates its own state (selectedQuantity and totalPrice)

This pattern is like an employee (child component) reporting progress or requesting assistance from a manager (parent component). It ensures that the child remains decoupled from its parent, only knowing it needs to emit events, not how those events are handled.

Two-way Data Binding with ngModel

Angular also provides a convenient syntax for two-way data binding, combining an input and output in a single expression using [(ngModel)] or creating your own two-way binding properties.


// Custom component with two-way binding
@Component({
  selector: 'app-rating',
  template: `
    <div class="star-rating">
      <span 
        *ngFor="let star of stars; let i = index" 
        (click)="rate(i + 1)"
        [class.filled]="i < value">
        ★
      </span>
    </div>
  `,
  styles: [`
    .star-rating span {
      cursor: pointer;
      font-size: 24px;
      color: #ddd;
    }
    .star-rating span.filled {
      color: gold;
    }
  `]
})
export class RatingComponent {
  stars = [0, 1, 2, 3, 4];
  
  @Input() value = 0;
  @Output() valueChange = new EventEmitter<number>();
  
  rate(value: number) {
    this.value = value;
    this.valueChange.emit(value);
  }
}

// Parent component using the two-way binding
@Component({
  selector: 'app-feedback-form',
  template: `
    <div class="feedback-form">
      <h2>Product Feedback</h2>
      <p>How would you rate this product?</p>
      
      <app-rating [(value)]="productRating"></app-rating>
      
      <p>You rated this product {{ productRating }} out of 5 stars.</p>
    </div>
  `
})
export class FeedbackFormComponent {
  productRating = 0;
}
        

The key pattern for two-way binding is:

  1. Create an @Input() property (e.g., value)
  2. Create an @Output() property with the same name plus "Change" (e.g., valueChange)
  3. Emit the new value through the output property whenever the input changes

This is like a collaborative document that multiple team members can edit. Changes made by anyone are automatically visible to everyone else.

Using Template Reference Variables

Another simple way to communicate between components is through template reference variables, which allow parent components to directly reference child component instances in their templates.


// Parent template
<div class="timer-control">
  <app-countdown-timer #timer [seconds]="60"></app-countdown-timer>
  
  <div class="controls">
    <button (click)="timer.start()">Start</button>
    <button (click)="timer.pause()">Pause</button>
    <button (click)="timer.reset()">Reset</button>
  </div>
</div>
        

Here, the #timer reference variable gives the parent template direct access to the child component's public methods. This is useful for simple interactions but should be used sparingly as it creates tight coupling between components.

Real-world Applications

E-commerce Product Catalog

In an e-commerce application, component communication is crucial:

Social Media Feed

In a social media app:

Best Practices and Common Pitfalls

Best Practices

Common Pitfalls

Practice Activities

Basic Exercise: Building a Todo List

Create a todo list application with these components:

Implement these requirements:

  1. Pass todo items from parent to child using @Input()
  2. Emit events when a todo is completed or deleted using @Output()
  3. Add new todos from the form component to the list component

Advanced Exercise: Creating a Shopping Cart System

Build a more complex application with:

Implement these features:

  1. Add products to the cart from the catalog
  2. Update quantities in the cart
  3. Remove items from the cart
  4. Calculate and display the total price

Summary

We've explored various ways components can communicate in Angular applications:

In our next session, we'll explore how unrelated components can communicate through services, and dive deeper into dependency injection in Angular.

Additional Resources