Component Communication
By Syncfusion®, Inc. · Last updated
How parent and child components exchange data: @Input() for data going down, @Output() with EventEmitter for events going up, two-way binding between components, and best practices.
What Is Component Communication?
Component communication is how components that are placed inside each other exchange data and events.
Data flows in two directions:
- Parent to child. The parent passes data down to the child through an input.
- Child to parent. The child sends a notification up to the parent through an output event.
The parent owns the data. The child displays it and reports user actions, and the parent decides how to change the data.
Real-World Example
In a product catalog, the ProductList parent loads products from the API and passes each one to a ProductCard child through an input.
When the user clicks Add to cart on a card, the child emits an addToCart event with the product id. The parent receives it and updates the cart, which the header then displays.
ProductListparent: owns the dataProductCardchild: displays and reports[product]="p"@Input(): data goes down(addToCart)="add($event)"@Output(): events go up
Inputs go down. Events go up.
Parent-to-Child Communication
Parent-to-child communication sends data from a parent component into a child component.
Parent (ProductList)[product]="selected"Child (ProductCard)@Input() productdata flows down
Parent to child: a property binding sends data into an @Input().
The child declares which values it accepts. The parent sets those values in its template with property binding, the same [ ] syntax you use on HTML elements.
The parent passes the signed-in user to a badge
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-user-badge',
template: `<span class="badge">👤 {{ name }}</span>`,
styles: `.badge { padding: 3px 10px; border-radius: 12px; background: #e0e7ff; }`,
})
export class UserBadge {
@Input() name = '';
}
@Component({
selector: 'app-root',
imports: [UserBadge],
template: `
<p>Signed in as <app-user-badge [name]="currentUser" /></p>
<button (click)="currentUser = 'Ravi'">Switch user</button>
`,
})
export class App {
currentUser = 'Asha';
}
Click Switch user. The parent changes currentUser, and Angular passes the new value to the child, which updates its view.
@Input()
@Input() is a decorator that marks a property of the child class as an input. Only input properties can receive values from a parent's template.
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-user-badge',
template: `<span>👤 {{ name }}</span>`,
})
export class UserBadge {
@Input() name = '';
}
You can configure an input:
@Input({ required: true })makes Angular report a build error when the parent forgets to set it.@Input({ alias: 'userName' })lets the parent use a different attribute name than the property name.- A default value, such as
= '', is used until the parent sets one.
Newer Angular code can also declare inputs with the input() function, which returns a signal. The practice editors on this site run Angular without a build step, so they use the @Input() decorator. Both forms work the same way from the parent's point of view.
Passing Data with @Input()
An input can receive any value: text, numbers, booleans, arrays or objects. Passing a whole object keeps the parent template short when the child needs several fields.
The parent passes product objects to a reusable card
import { Component, Input } from '@angular/core';
interface Product {
name: string;
price: number;
inStock: boolean;
}
@Component({
selector: 'app-product-card',
template: `
<div class="card">
<strong>{{ product.name }}</strong>
<span>₹{{ product.price }}</span>
<span>{{ product.inStock ? 'In stock' : 'Sold out' }}</span>
@if (featured) { <em>Featured</em> }
</div>
`,
styles: `.card { display: flex; gap: 12px; padding: 6px 10px; margin: 4px 0; border: 1px solid #d1d5db; border-radius: 8px; }`,
})
export class ProductCard {
@Input({ required: true }) product!: Product;
@Input() featured = false;
}
@Component({
selector: 'app-root',
imports: [ProductCard],
template: `
@for (p of products; track p.name; let first = $first) {
<app-product-card [product]="p" [featured]="first" />
}
`,
})
export class App {
products: Product[] = [
{ name: 'Mechanical keyboard', price: 4999, inStock: true },
{ name: 'USB-C hub', price: 1899, inStock: false },
{ name: '27" monitor', price: 18499, inStock: true },
];
}
The first card receives featured = true from the loop variable $first. The other cards keep the default false.
Child-to-Parent Communication
Child-to-parent communication sends a notification from a child component up to its parent, usually because the user did something inside the child.
Parent (Cart)(added)="addToCart($event)"Child (ProductCard)@Output() added.emit(product)events flow up
Child to parent: the child emits an event, and the parent listens to it.
The child does not change the parent's data directly. It emits an event, optionally with a value. The parent listens with event binding, the same ( ) syntax you use for DOM events:
<!-- parent template -->
<app-rating (rated)="saveRating($event)" />
$event holds the value the child emitted.
@Output()
@Output() is a decorator that marks a child property as an output: an event that the parent can listen to.
The property must hold an EventEmitter. The child calls its emit() method to send the event.
The child reports a click, and the parent counts it
import { Component, EventEmitter, Output } from '@angular/core';
@Component({
selector: 'app-like-button',
template: `<button (click)="liked.emit()">👍 Like</button>`,
})
export class LikeButton {
@Output() liked = new EventEmitter<void>();
}
@Component({
selector: 'app-root',
imports: [LikeButton],
template: `
<app-like-button (liked)="likes = likes + 1" />
<p>{{ likes }} people like this post</p>
`,
})
export class App {
likes = 12;
}
Do not give an output the name of a native DOM event, such as click or change. The parent would receive both your event and the browser's event under the same name. Use a descriptive name, such as liked.
EventEmitter
EventEmitter is the Angular class that sends output events. Its type parameter describes the value the event carries:
EventEmitter<void>sends an event without a value.EventEmitter<number>sends a number, for example a rating.EventEmitter<Product>sends an object.
emit(value) sends the event. Every parent handler bound to the output receives value as $event.
The child emits ratings, and the parent stores them
import { Component, EventEmitter, Input, Output } from '@angular/core';
@Component({
selector: 'app-star-rating',
template: `
<span>{{ label }}: </span>
@for (star of [1, 2, 3, 4, 5]; track star) {
<button (click)="rated.emit(star)">{{ star }}★</button>
}
`,
})
export class StarRating {
@Input() label = 'Rate';
@Output() rated = new EventEmitter<number>();
}
@Component({
selector: 'app-root',
imports: [StarRating],
template: `
<app-star-rating label="Delivery" (rated)="onRated('Delivery', $event)" />
<app-star-rating label="Product" (rated)="onRated('Product', $event)" />
<p>{{ summary }}</p>
`,
})
export class App {
summary = 'No ratings yet';
onRated(area: string, stars: number) {
this.summary = area + ' rated ' + stars + ' out of 5';
}
}
The same StarRating component is used twice. The parent knows which one was clicked because each binding passes its own label.
| Point | @Input() | @Output() |
|---|---|---|
| Direction | Parent → child | Child → parent |
| Parent syntax | [name]="value" | (name)="handler($event)" |
| Child property holds | A value | An EventEmitter |
| Child sends with | Nothing. It only receives | this.name.emit(value) |
| Typical use | Data to display | User actions inside the child |
Two-Way Binding Between Components
When a child has an input x and an output named xChange, the parent can use two-way binding: [(x)]="value". This is the same pattern ngModel uses.
[(size)]="fontSize"in the parent[size]value goes down(sizeChange)new value comes upfontSize updatedboth stay in sync[( )] is a short form for one @Input() and one @Output() named ...Change.
Two-way binding = property binding + event binding.
An input and an xChange output enable [(x)]
import { Component, EventEmitter, Input, Output } from '@angular/core';
@Component({
selector: 'app-counter',
template: `
<button (click)="change(-1)">−</button>
<strong> {{ count }} </strong>
<button (click)="change(1)">+</button>
`,
})
export class Counter {
@Input() count = 0;
@Output() countChange = new EventEmitter<number>();
change(step: number) {
this.countChange.emit(this.count + step);
}
}
@Component({
selector: 'app-root',
imports: [Counter],
template: `
<p>Tickets: <app-counter [(count)]="tickets" /></p>
<p>Total: ₹{{ tickets * 250 }}</p>
`,
})
export class App {
tickets = 2;
}
Component Communication Best Practices
- Keep data ownership in one place. The parent owns and changes the data. The child receives it through inputs and reports changes through outputs.
- Do not change input objects inside the child. Emit an event and let the parent create the updated object.
- Name outputs after what happened, such as
rated,deletedoraddToCart. Do not name them after a handler, such asonRated. - Mark essential inputs as required with
@Input({ required: true }), so a missing binding fails at build time. - Keep the chain short. Passing data through more than two or three levels of components becomes hard to follow. Use a shared service instead, which is covered in the Services lesson.
- Emit only what the parent needs, such as an id or a value, instead of the whole child component.
Inputs go down, events go up. A child never changes its parent's data directly. It asks the parent to change it by emitting an event.
Common Mistakes
| Mistake | What you see | Fix |
|---|---|---|
Binding to a property without @Input() | NG0303: Can't bind to 'product' since it isn't a known property | Add @Input() to the child property |
Text without quotes: [label]="Delivery" | The child receives undefined | Use [label]="'Delivery'" or label="Delivery" |
Output not initialised: @Output() saved; | Cannot read properties of undefined (reading 'emit') | Assign new EventEmitter() |
Forgetting $event in the parent handler | The handler receives no value | Pass it: (rated)="onRated($event)" |
| Changing an input object inside the child | The parent's data changes unexpectedly | Emit an event and let the parent update the data |
Output named countchange for [(count)] | Two-way binding does not update the parent | Name it exactly countChange |
Check Yourself
Exercises
What to Remember
- A parent passes data down with
[input]="value"to a child property marked@Input(). - A child sends events up with an
@Output()EventEmitter, and the parent listens with(output)="handler($event)". - The parent owns the data. Inputs go down, events go up.
Next: Component Lifecycle Hook, the hooks Angular calls as a component is created, updated and destroyed.