Component Communication
Overview
Components need to communicate with each other to share data and coordinate behavior. Angular provides several patterns for component communication.
Parent to Child: @Input
Pass data from parent to child component using @Input decorator.
Basic Input
// child.component.ts
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-user-card',
standalone: true,
template: `
<div class="card">
<h3>{{ userName }}</h3>
<p>{{ userEmail }}</p>
</div>
`,
})
export class UserCardComponent {
@Input() userName!: string;
@Input() userEmail!: string;
}
// parent.component.ts
@Component({
selector: 'app-user-list',
standalone: true,
imports: [UserCardComponent],
template: `
<app-user-card
[userName]="'Alice Johnson'"
[userEmail]="'alice@example.com'"
>
</app-user-card>
`,
})
export class UserListComponent {}
Required Inputs (Angular 16+)
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-product-card',
standalone: true,
template: `
<div>
<h3>{{ product.name }}</h3>
<p>${{ product.price }}</p>
</div>
`
})
export class ProductCardComponent {
// Required input - must be provided
@Input({ required: true }) product!: Product;
// Optional input with default value
@Input() showRating = true;
}
interface Product {
id: number;
name: string;
price: number;
}
Input Alias
@Component({
selector: 'app-article',
standalone: true,
template: `<h1>{{ title }}</h1>`,
})
export class ArticleComponent {
// External name: 'articleTitle', Internal name: 'title'
@Input('articleTitle') title!: string;
}
// Usage
// <app-article [articleTitle]="'My Article'"></app-article>
Input with Setter
@Component({
selector: 'app-name-display',
standalone: true,
template: `<p>{{ formattedName }}</p>`,
})
export class NameDisplayComponent {
formattedName = '';
@Input()
set name(value: string) {
// Transform input when it changes
this.formattedName = value.toUpperCase();
}
}
Child to Parent: @Output
Emit events from child to parent using @Output and EventEmitter.
Basic Output
// child.component.ts
import { Component, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-counter',
standalone: true,
template: `
<div>
<p>Count: {{ count }}</p>
<button (click)="increment()">+1</button>
</div>
`,
})
export class CounterComponent {
count = 0;
@Output() countChanged = new EventEmitter<number>();
increment() {
this.count++;
this.countChanged.emit(this.count);
}
}
// parent.component.ts
@Component({
selector: 'app-parent',
standalone: true,
imports: [CounterComponent],
template: `
<p>Total from child: {{ totalCount }}</p>
<app-counter (countChanged)="onCountChanged($event)"></app-counter>
`,
})
export class ParentComponent {
totalCount = 0;
onCountChanged(newCount: number) {
this.totalCount = newCount;
}
}
Custom Events with Data
interface AddToCartEvent {
productId: number;
quantity: number;
}
@Component({
selector: 'app-product-card',
standalone: true,
template: `
<div>
<h3>{{ product.name }}</h3>
<button (click)="addToCart()">Add to Cart</button>
</div>
`,
})
export class ProductCardComponent {
@Input({ required: true }) product!: Product;
@Output() productAdded = new EventEmitter<AddToCartEvent>();
addToCart() {
this.productAdded.emit({
productId: this.product.id,
quantity: 1,
});
}
}
// Parent
@Component({
selector: 'app-shop',
standalone: true,
imports: [ProductCardComponent],
template: `
<app-product-card
[product]="product"
(productAdded)="handleProductAdded($event)"
>
</app-product-card>
`,
})
export class ShopComponent {
handleProductAdded(event: AddToCartEvent) {
console.log('Product added:', event);
}
}
Two-Way Binding: @Input/@Output Pair
Create custom two-way binding with [(property)] syntax.
// custom-input.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-custom-input',
standalone: true,
template: ` <input [value]="value" (input)="onInputChange($event)" /> `,
})
export class CustomInputComponent {
@Input() value = '';
@Output() valueChange = new EventEmitter<string>();
onInputChange(event: Event) {
const newValue = (event.target as HTMLInputElement).value;
this.valueChange.emit(newValue);
}
}
// Parent usage
@Component({
selector: 'app-parent',
standalone: true,
imports: [CustomInputComponent],
template: `
<app-custom-input [(value)]="searchText"></app-custom-input>
<p>You typed: {{ searchText }}</p>
`,
})
export class ParentComponent {
searchText = '';
}
Modern Two-Way Binding (Angular 17.2+)
import { Component, model } from '@angular/core';
@Component({
selector: 'app-custom-input',
standalone: true,
template: `
<input [value]="value()" (input)="value.set($any($event.target).value)" />
`,
})
export class CustomInputComponent {
value = model<string>(''); // Creates both input and output
}
// Usage is the same
// <app-custom-input [(value)]="searchText"></app-custom-input>
Template Reference Variables
Access child component instance from parent template.
// child.component.ts
@Component({
selector: 'app-video-player',
standalone: true,
template: `<video #videoElement></video>`,
})
export class VideoPlayerComponent {
play() {
console.log('Playing video...');
}
pause() {
console.log('Pausing video...');
}
}
// parent.component.ts
@Component({
selector: 'app-parent',
standalone: true,
imports: [VideoPlayerComponent],
template: `
<app-video-player #player></app-video-player>
<button (click)="player.play()">Play</button>
<button (click)="player.pause()">Pause</button>
`,
})
export class ParentComponent {}
@ViewChild - Access Child from Component Class
Access child component, directive, or element from parent component class.
import { Component, ViewChild, AfterViewInit } from '@angular/core';
import { VideoPlayerComponent } from './video-player.component';
@Component({
selector: 'app-parent',
standalone: true,
imports: [VideoPlayerComponent],
template: `
<app-video-player></app-video-player>
<button (click)="playVideo()">Play</button>
`,
})
export class ParentComponent implements AfterViewInit {
@ViewChild(VideoPlayerComponent) player!: VideoPlayerComponent;
ngAfterViewInit() {
// Child component is available after view initialization
console.log('Player component:', this.player);
}
playVideo() {
this.player.play();
}
}
ViewChild with Template Reference
import { Component, ViewChild, ElementRef, AfterViewInit } from '@angular/core';
@Component({
selector: 'app-focus-demo',
standalone: true,
template: `
<input #searchInput type="text" placeholder="Search..." />
<button (click)="focusInput()">Focus Input</button>
`,
})
export class FocusDemoComponent implements AfterViewInit {
@ViewChild('searchInput') searchInput!: ElementRef<HTMLInputElement>;
ngAfterViewInit() {
// Auto-focus on component load
this.searchInput.nativeElement.focus();
}
focusInput() {
this.searchInput.nativeElement.focus();
}
}
@ViewChildren - Multiple Children
Query multiple children.
import {
Component,
ViewChildren,
QueryList,
AfterViewInit,
} from '@angular/core';
import { UserCardComponent } from './user-card.component';
@Component({
selector: 'app-user-list',
standalone: true,
imports: [UserCardComponent, CommonModule],
template: `
@for (user of users; track user.id) {
<app-user-card [user]="user"></app-user-card>
}
`,
})
export class UserListComponent implements AfterViewInit {
@ViewChildren(UserCardComponent) userCards!: QueryList<UserCardComponent>;
users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
];
ngAfterViewInit() {
console.log('Number of cards:', this.userCards.length);
// Subscribe to changes
this.userCards.changes.subscribe(cards => {
console.log('Cards changed:', cards);
});
}
}
Content Projection with ng-content
Pass content from parent to child.
Single Slot Projection
// card.component.ts
@Component({
selector: 'app-card',
standalone: true,
template: `
<div class="card">
<ng-content></ng-content>
</div>
`,
styles: [
`
.card {
border: 1px solid #ddd;
padding: 1rem;
border-radius: 8px;
}
`,
],
})
export class CardComponent {}
// Usage
@Component({
selector: 'app-parent',
standalone: true,
imports: [CardComponent],
template: `
<app-card>
<h2>Card Title</h2>
<p>Card content goes here</p>
<button>Action</button>
</app-card>
`,
})
export class ParentComponent {}
Multi-Slot Projection
// panel.component.ts
@Component({
selector: 'app-panel',
standalone: true,
template: `
<div class="panel">
<div class="panel-header">
<ng-content select="[header]"></ng-content>
</div>
<div class="panel-body">
<ng-content select="[body]"></ng-content>
</div>
<div class="panel-footer">
<ng-content select="[footer]"></ng-content>
</div>
</div>
`,
})
export class PanelComponent {}
// Usage
@Component({
selector: 'app-parent',
standalone: true,
imports: [PanelComponent],
template: `
<app-panel>
<div header>
<h2>Panel Title</h2>
</div>
<div body>
<p>Panel content</p>
</div>
<div footer>
<button>Save</button>
</div>
</app-panel>
`,
})
export class ParentComponent {}
@ContentChild / @ContentChildren
Access projected content from component class.
import {
Component,
ContentChild,
AfterContentInit,
ElementRef,
} from '@angular/core';
@Component({
selector: 'app-card',
standalone: true,
template: `
<div class="card">
<ng-content></ng-content>
</div>
`,
})
export class CardComponent implements AfterContentInit {
@ContentChild('cardTitle') title!: ElementRef<HTMLElement>;
ngAfterContentInit() {
if (this.title) {
console.log('Card title:', this.title.nativeElement.textContent);
}
}
}
// Usage
@Component({
template: `
<app-card>
<h2 #cardTitle>My Title</h2>
<p>Content</p>
</app-card>
`,
})
export class ParentComponent {}
Best Practices
✅ Do
// Use descriptive event names
@Output() userSelected = new EventEmitter<User>();
@Output() itemDeleted = new EventEmitter<number>();
// Use required inputs for essential data
@Input({ required: true }) user!: User;
// Emit specific, typed data
interface DeleteEvent {
id: number;
confirmed: boolean;
}
@Output() itemDeleted = new EventEmitter<DeleteEvent>();
// Unsubscribe from EventEmitters in ngOnDestroy
ngOnDestroy() {
this.subscription.unsubscribe();
}
❌ Don't
// Don't modify input objects directly
@Input() user!: User;
ngOnInit() {
this.user.name = 'New Name'; // ❌ Don't mutate inputs
}
// Don't use generic event names
@Output() event = new EventEmitter(); // ❌ Not descriptive
// Don't emit from parent to child (use inputs instead)
// ❌ Wrong direction of communication
Communication Patterns Summary
| Pattern | Direction | Use Case |
|---|---|---|
@Input | Parent → Child | Pass data down |
@Output | Child → Parent | Send events up |
@ViewChild | Parent → Child | Access child methods/properties |
@ContentChild | Host → Projected | Access projected content |
| Template Ref | Template → Component | Access elements/components |
| Service | Any → Any | Share data across components |
Interview Questions
Q: What's the difference between @ViewChild and @ContentChild?
A: @ViewChild queries elements in the component's own template. @ContentChild queries elements projected into the component via <ng-content>.
Q: When does @ViewChild become available?
A: After ngAfterViewInit lifecycle hook. Before that, it's undefined.
Q: How do you create custom two-way binding?
A: Create an @Input property and an @Output property with the same name plus "Change" suffix. Example: @Input() value and @Output() valueChange.
Next Steps
- Learn about Services & Dependency Injection
- Understand Directives