Skip to main content

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

PatternDirectionUse Case
@InputParent → ChildPass data down
@OutputChild → ParentSend events up
@ViewChildParent → ChildAccess child methods/properties
@ContentChildHost → ProjectedAccess projected content
Template RefTemplate → ComponentAccess elements/components
ServiceAny → AnyShare 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