Skip to main content

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

  1. Define a property in the child component with the @Input() decorator
  2. 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):

typescript
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):

typescript
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

  1. Define an EventEmitter property in the child component with the @Output() decorator
  2. Emit events using this property
  3. 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):

typescript
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):

typescript
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.

typescript
// 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:

typescript
// 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:

typescript
// 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:

  1. Create a service that uses a Subject or BehaviorSubject
  2. Components subscribe to the observable to receive updates
  3. Components call service methods to update data

Example

Let's create a service for managing notification messages across components:

typescript
// 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):

typescript
// 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):

typescript
// 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

typescript
// 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;
}
}
typescript
// 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:

typescript
// 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:

typescript
// 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:

typescript
// 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:

typescript
// 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:

  1. We use a service for communication between unrelated components
  2. The ProductListComponent adds products to the cart
  3. The CartComponent subscribes to cart changes through an observable
  4. Both components are siblings in the component tree, but they communicate effectively

Summary

Angular offers several mechanisms for component communication:

  1. @Input() - For passing data from parent to child
  2. @Output() and EventEmitter - For passing data from child to parent
  3. Services with RxJS - For communication between unrelated components
  4. ViewChild/ContentChild - For direct access to child components
  5. 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

Exercises

  1. Create a parent component that passes a list of items to a child component using @Input().
  2. Modify the child component to allow selecting an item and notifying the parent using @Output().
  3. Create a service that allows two unrelated components to share data.
  4. Implement a custom two-way binding property on a component.
  5. 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! :)