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
@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:
[product]="product"- Passes a product object[showDiscount]="showSpecialOffers"- Passes a boolean that controls display behavior
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:
- Validate or transform input data
- Execute additional logic when inputs change
- Maintain derived state based on inputs
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.
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:
- The child
QuantitySelectorComponenthas a quantityChanged@Output()that emits events when the quantity changes - The parent
ProductDetailComponentlistens for these events with(quantityChanged)="onQuantityChanged($event)" - 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:
- Create an
@Input()property (e.g.,value) - Create an
@Output()property with the same name plus "Change" (e.g.,valueChange) - 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:
- Parent to Child: Product catalog passes product details to product cards
- Child to Parent: Product cards emit events when "Add to Cart" is clicked
- Two-way Binding: Used for quantity selectors or customization options
Social Media Feed
In a social media app:
- Parent to Child: Feed component passes post data to post components
- Child to Parent: Post components emit events when likes, shares, or comments are made
- Service Communication: Notification events that affect multiple components
Best Practices and Common Pitfalls
Best Practices
- Keep components focused: Each component should have a single responsibility
- Use descriptive event names: Make event names clear about what happened, e.g.,
itemSelected, not justchange - Document inputs and outputs: Add JSDoc comments to clarify expected types and behaviors
- Use strongly typed interfaces: Avoid using
anyfor input/output types
Common Pitfalls
- Prop drilling: Passing props through many layers of components
- Tight coupling: Child components that know too much about their parents
- Over-emitting events: Emitting too many fine-grained events rather than meaningful user actions
- Mutating inputs: Modifying input objects directly instead of emitting changes
Practice Activities
Basic Exercise: Building a Todo List
Create a todo list application with these components:
- TodoListComponent (Parent): Manages the array of todos
- TodoItemComponent (Child): Displays a single todo item
- TodoFormComponent (Child): Creates new todo items
Implement these requirements:
- Pass todo items from parent to child using
@Input() - Emit events when a todo is completed or deleted using
@Output() - Add new todos from the form component to the list component
Advanced Exercise: Creating a Shopping Cart System
Build a more complex application with:
- ProductCatalogComponent: Displays available products
- ProductCardComponent: Shows individual product details
- CartComponent: Displays the current cart items
- CartItemComponent: Shows individual cart items with quantity control
Implement these features:
- Add products to the cart from the catalog
- Update quantities in the cart
- Remove items from the cart
- Calculate and display the total price
Summary
We've explored various ways components can communicate in Angular applications:
- Parent to Child: Using
@Input()properties to pass data down - Child to Parent: Using
@Output()properties and EventEmitter to send events up - Two-way Binding: Combining inputs and outputs for bidirectional data flow
- Template References: Direct access to child components in parent templates
In our next session, we'll explore how unrelated components can communicate through services, and dive deeper into dependency injection in Angular.