Angular Component Communication
Angular applications are built as a tree of components, each responsible for a specific part of the user interface. For these components to work together effectively, they need to share data and communicate with each other. In this guide, we'll explore various methods for component communication in Angular.
Introduction
In real-world applications, components rarely exist in isolation. They need to exchange data, trigger events, and synchronize state. Angular provides several mechanisms for component communication:
- Parent to child communication using @Input()
- Child to parent communication using @Output() and EventEmitter
- Communication through services
- ViewChild/ContentChild for direct component access
- NgRx for state management in complex applications
Let's explore each approach with examples.
Parent to Child Communication with @Input()
The most straightforward way for a parent component to send data to its child component is through input properties.
How @Input() Works
- Define a property in the child component with the
@Input()
decorator - In the parent template, bind to this property using property binding (
[property]="value"
)
Example
Let's create a parent component that passes data to a child component:
Child Component (user-profile.component.ts
):
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-user-profile',
template: `
<div class="user-card">
<h2>{{ name }}</h2>
<p>Role: {{ role }}</p>
<p>Experience: {{ experienceYears }} years</p>
</div>
`,
styles: [`
.user-card {
border: 1px solid #ccc;
padding: 15px;
border-radius: 4px;
margin-bottom: 10px;
}
`]
})
export class UserProfileComponent {
@Input() name: string = '';
@Input() role: string = '';
@Input() experienceYears: number = 0;
}
Parent Component (user-list.component.ts
):
import { Component } from '@angular/core';
@Component({
selector: 'app-user-list',
template: `
<div>
<h1>Development Team</h1>
<app-user-profile
[name]="users[0].name"
[role]="users[0].role"
[experienceYears]="users[0].experienceYears">
</app-user-profile>
<app-user-profile
[name]="users[1].name"
[role]="users[1].role"
[experienceYears]="users[1].experienceYears">
</app-user-profile>
</div>
`
})
export class UserListComponent {
users = [
{ name: 'Alice Smith', role: 'Frontend Developer', experienceYears: 5 },
{ name: 'Bob Johnson', role: 'Backend Developer', experienceYears: 3 }
];
}
In this example, the UserListComponent
(parent) passes user information to multiple instances of UserProfileComponent
(child).
Child to Parent Communication with @Output()
For a child component to send data or events back to its parent, we use the @Output()
decorator along with EventEmitter
.
How @Output() Works
- Define an
EventEmitter
property in the child component with the@Output()
decorator - Emit events using this property
- In the parent component's template, bind to these events using event binding (
(eventName)="handler($event)"
)
Example
Let's extend our previous example to allow the child component to notify the parent when a user is selected.
Child Component (user-profile.component.ts
):
import { Component, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-user-profile',
template: `
<div class="user-card" (click)="selectUser()">
<h2>{{ name }}</h2>
<p>Role: {{ role }}</p>
<p>Experience: {{ experienceYears }} years</p>
</div>
`,
styles: [`
.user-card {
border: 1px solid #ccc;
padding: 15px;
border-radius: 4px;
margin-bottom: 10px;
cursor: pointer;
}
.user-card:hover {
background-color: #f5f5f5;
}
`]
})
export class UserProfileComponent {
@Input() name: string = '';
@Input() role: string = '';
@Input() experienceYears: number = 0;
@Output() userSelected = new EventEmitter<string>();
selectUser() {
this.userSelected.emit(this.name);
}
}
Parent Component (user-list.component.ts
):
import { Component } from '@angular/core';
@Component({
selector: 'app-user-list',
template: `
<div>
<h1>Development Team</h1>
<p *ngIf="selectedUser">Selected User: {{ selectedUser }}</p>
<app-user-profile
*ngFor="let user of users"
[name]="user.name"
[role]="user.role"
[experienceYears]="user.experienceYears"
(userSelected)="onUserSelected($event)">
</app-user-profile>
</div>
`
})
export class UserListComponent {
users = [
{ name: 'Alice Smith', role: 'Frontend Developer', experienceYears: 5 },
{ name: 'Bob Johnson', role: 'Backend Developer', experienceYears: 3 }
];
selectedUser: string = '';
onUserSelected(name: string) {
this.selectedUser = name;
console.log(`User selected: ${name}`);
}
}
In this example, when a user clicks on a profile card, the child component emits an event with the user's name, which the parent component captures and displays.
Two-way Data Binding with ngModel
Sometimes you need two-way binding, where both components can update the same data. Angular uses the banana-in-a-box [(ngModel)]
syntax for this purpose.
Example of Two-way Binding
First, make sure you have the FormsModule
imported in your Angular module.
// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, FormsModule],
bootstrap: [AppComponent]
})
export class AppModule { }
Now create a custom two-way binding component:
// name-editor.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-name-editor',
template: `
<div>
<label>Edit name: </label>
<input [value]="name" (input)="nameChange.emit($event.target.value)">
</div>
`
})
export class NameEditorComponent {
@Input() name!: string;
@Output() nameChange = new EventEmitter<string>();
}
And use it with two-way binding:
// app.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<h1>Name Editor</h1>
<p>Current name: {{ userName }}</p>
<app-name-editor [(name)]="userName"></app-name-editor>
`
})
export class AppComponent {
userName = 'Alice Smith';
}
Notice the pattern: for a property name
, create an output called nameChange
to enable Angular's two-way binding.
Communication via Services
For unrelated components or deeply nested components, communication through parent-child chains becomes cumbersome. In such cases, services with RxJS observables provide an elegant solution.
Steps to Implement Service Communication:
- Create a service that uses a Subject or BehaviorSubject
- Components subscribe to the observable to receive updates
- Components call service methods to update data
Example
Let's create a service for managing notification messages across components:
// notification.service.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class NotificationService {
private messageSource = new BehaviorSubject<string>('');
currentMessage = this.messageSource.asObservable();
constructor() { }
sendMessage(message: string) {
this.messageSource.next(message);
}
clearMessage() {
this.messageSource.next('');
}
}
Component A (Sender):
// sender.component.ts
import { Component } from '@angular/core';
import { NotificationService } from '../notification.service';
@Component({
selector: 'app-sender',
template: `
<div>
<h2>Message Sender</h2>
<input #messageInput placeholder="Type a message">
<button (click)="sendMessage(messageInput.value)">Send Notification</button>
</div>
`
})
export class SenderComponent {
constructor(private notificationService: NotificationService) { }
sendMessage(message: string) {
if (message) {
this.notificationService.sendMessage(message);
}
}
}
Component B (Receiver):
// receiver.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { NotificationService } from '../notification.service';
import { Subscription } from 'rxjs';
@Component({
selector: 'app-receiver',
template: `
<div>
<h2>Message Receiver</h2>
<div *ngIf="message" class="notification">
New message: {{ message }}
</div>
<div *ngIf="!message">
No new messages
</div>
</div>
`,
styles: [`
.notification {
padding: 10px;
background-color: #dff0d8;
border: 1px solid #d6e9c6;
border-radius: 4px;
color: #3c763d;
}
`]
})
export class ReceiverComponent implements OnInit, OnDestroy {
message: string = '';
private subscription: Subscription;
constructor(private notificationService: NotificationService) { }
ngOnInit() {
this.subscription = this.notificationService.currentMessage
.subscribe(message => this.message = message);
}
ngOnDestroy() {
// Always unsubscribe to prevent memory leaks
this.subscription.unsubscribe();
}
}
Now SenderComponent
and ReceiverComponent
can communicate even if they're in different parts of the component tree.
ViewChild for Direct Access
Sometimes you need direct access to a child component from its parent. The @ViewChild
decorator allows this.
Example
// timer-child.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-timer-child',
template: `
<h2>Timer: {{ seconds }} seconds</h2>
`
})
export class TimerChildComponent {
seconds = 0;
intervalId: any;
start() {
this.intervalId = setInterval(() => {
this.seconds++;
}, 1000);
}
stop() {
clearInterval(this.intervalId);
}
reset() {
this.seconds = 0;
}
}
// timer-parent.component.ts
import { Component, ViewChild, AfterViewInit } from '@angular/core';
import { TimerChildComponent } from './timer-child.component';
@Component({
selector: 'app-timer-parent',
template: `
<div>
<h1>Timer Control</h1>
<button (click)="startTimer()">Start</button>
<button (click)="stopTimer()">Stop</button>
<button (click)="resetTimer()">Reset</button>
<app-timer-child></app-timer-child>
</div>
`
})
export class TimerParentComponent implements AfterViewInit {
@ViewChild(TimerChildComponent) timerChild!: TimerChildComponent;
ngAfterViewInit() {
// Child component is available after view is initialized
console.log('Child component accessed');
}
startTimer() {
this.timerChild.start();
}
stopTimer() {
this.timerChild.stop();
}
resetTimer() {
this.timerChild.reset();
}
}
With @ViewChild
, the parent can directly call methods on the child component.
Real-world Example: Shopping Cart Application
Let's create a simplified shopping cart application using various component communication techniques:
Product Service:
// product.service.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
export interface Product {
id: number;
name: string;
price: number;
}
export interface CartItem extends Product {
quantity: number;
}
@Injectable({
providedIn: 'root'
})
export class ProductService {
private products: Product[] = [
{ id: 1, name: 'Laptop', price: 1200 },
{ id: 2, name: 'Smartphone', price: 800 },
{ id: 3, name: 'Headphones', price: 100 }
];
private cart: CartItem[] = [];
private cartSubject = new BehaviorSubject<CartItem[]>([]);
cartItems = this.cartSubject.asObservable();
constructor() { }
getProducts(): Product[] {
return this.products;
}
addToCart(product: Product) {
const existingItem = this.cart.find(item => item.id === product.id);
if (existingItem) {
existingItem.quantity += 1;
} else {
this.cart.push({...product, quantity: 1});
}
this.cartSubject.next([...this.cart]);
}
getCartTotal(): number {
return this.cart.reduce((total, item) => total + (item.price * item.quantity), 0);
}
}
Product List Component:
// product-list.component.ts
import { Component, OnInit } from '@angular/core';
import { Product, ProductService } from '../product.service';
@Component({
selector: 'app-product-list',
template: `
<div class="product-list">
<h2>Products</h2>
<div *ngFor="let product of products" class="product-card">
<h3>{{ product.name }}</h3>
<p>{{ product.price | currency }}</p>
<button (click)="addToCart(product)">Add to Cart</button>
</div>
</div>
`,
styles: [`
.product-list {
display: flex;
flex-direction: column;
gap: 15px;
}
.product-card {
border: 1px solid #ddd;
padding: 15px;
border-radius: 5px;
}
`]
})
export class ProductListComponent implements OnInit {
products: Product[] = [];
constructor(private productService: ProductService) { }
ngOnInit() {
this.products = this.productService.getProducts();
}
addToCart(product: Product) {
this.productService.addToCart(product);
}
}
Cart Component:
// cart.component.ts
import { Component, OnInit } from '@angular/core';
import { CartItem, ProductService } from '../product.service';
@Component({
selector: 'app-cart',
template: `
<div class="cart">
<h2>Shopping Cart</h2>
<div *ngIf="cartItems.length === 0">
Your cart is empty
</div>
<div *ngFor="let item of cartItems" class="cart-item">
<span>{{ item.name }}</span>
<span>{{ item.price | currency }}</span>
<span>Quantity: {{ item.quantity }}</span>
</div>
<div *ngIf="cartItems.length > 0" class="cart-total">
<strong>Total: {{ cartTotal | currency }}</strong>
</div>
</div>
`,
styles: [`
.cart {
border: 1px solid #ddd;
padding: 15px;
border-radius: 5px;
margin-top: 20px;
}
.cart-item {
display: flex;
justify-content: space-between;
padding: 10px 0;
border-bottom: 1px solid #eee;
}
.cart-total {
margin-top: 15px;
text-align: right;
}
`]
})
export class CartComponent implements OnInit {
cartItems: CartItem[] = [];
cartTotal: number = 0;
constructor(private productService: ProductService) { }
ngOnInit() {
this.productService.cartItems.subscribe(items => {
this.cartItems = items;
this.cartTotal = this.productService.getCartTotal();
});
}
}
App Component:
// app.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<div class="container">
<h1>Online Store</h1>
<div class="app-layout">
<app-product-list></app-product-list>
<app-cart></app-cart>
</div>
</div>
`,
styles: [`
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.app-layout {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 20px;
}
`]
})
export class AppComponent { }
In this shopping cart example:
- We use a service for communication between unrelated components
- The
ProductListComponent
adds products to the cart - The
CartComponent
subscribes to cart changes through an observable - Both components are siblings in the component tree, but they communicate effectively
Summary
Angular offers several mechanisms for component communication:
- @Input() - For passing data from parent to child
- @Output() and EventEmitter - For passing data from child to parent
- Services with RxJS - For communication between unrelated components
- ViewChild/ContentChild - For direct access to child components
- Two-way binding - For synchronized updates between components
Choosing the right communication method depends on your component relationships and application complexity. For simple parent-child relationships, Input/Output is usually sufficient. For more complex scenarios, services with observables provide a more scalable solution.
Additional Resources
- Angular Official Documentation on Component Interaction
- RxJS Documentation
- NgRx for State Management
Exercises
- Create a parent component that passes a list of items to a child component using @Input().
- Modify the child component to allow selecting an item and notifying the parent using @Output().
- Create a service that allows two unrelated components to share data.
- Implement a custom two-way binding property on a component.
- Build a simple todo application with components for adding, listing, and filtering todos.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)