Component Communication in Angular: @Input, @Output, @ViewChild and ng-content
How components talk in Angular 12: data from parent to child with @Input, notifications back with @Output and EventEmitter, your own two-way binding, @ViewChild and content projection with ng-content.
Version note: The code and commands in this post are for Angular 12.
How Does Data Flow Through the Component Tree?
Remember the component tree we saw earlier: AppComponent at the top, pages below it, lists inside the pages, cards inside the lists. So far every component has kept its own data to itself. In a real app these pieces need to talk to each other. The list has to tell the card which product to show, and the card has to tell the list when "add to cart" is clicked.
Think of a manager and their team. Tasks go down: the manager tells the team what to do. Reports go up: the team lets the manager know when the work is done. In Angular the road down is called @Input() and the road up is called @Output(). In this post we will see both, plus two more ways for a parent to reach its child.
Our example is a small product list. First, let's define the shape of a product:
export interface Product {
id: number;
name: string;
price: number;
}
@Input: Data From Parent to Child
Use the CLI to generate two components, product-list and product-item. The child, ProductItemComponent, will show a single product:
import { Component, Input } from '@angular/core';
import { Product } from '../product';
@Component({
selector: 'app-product-item',
templateUrl: './product-item.component.html',
styleUrls: ['./product-item.component.css']
})
export class ProductItemComponent {
@Input() product!: Product;
}
<div class="product">
<h4>{{ product.name }}</h4>
<p>{{ product.price }} TL</p>
</div>
The @Input() decorator makes the product property fillable from the outside. The component no longer produces its own data; it shows whatever it's given. Here's the parent side:
export class ProductListComponent {
products: Product[] = [
{ id: 1, name: 'Mekanik Klavye', price: 1450 },
{ id: 2, name: 'Kablosuz Mouse', price: 420 }
];
}
<app-product-item *ngFor="let p of products" [product]="p"></app-product-item>
[product]="p" is exactly the property binding we saw earlier. The only difference is that this time we bind to a property of our own component instead of an HTML element. If the product in the parent changes, the child's view changes too.
Strict Mode and the Exclamation Mark
You probably noticed the exclamation mark in product!: Product. Remove it and Angular 12's strict mode gives you this error:
error TS2564: Property 'product' has no initializer and is not definitely assigned in the constructor.
TypeScript is asking a fair question: who gives this property its first value? Angular will assign it from the outside, but TypeScript can't know that. The exclamation mark is how you say "don't worry, this field will be filled." If there's a sensible default, giving it one is even better than the exclamation mark, as in @Input() discount = 0;.
Strict mode has another nice side. Bindings in the template are type-checked too. Write [product]="'Klavye'" by mistake and the build stops:
error TS2322: Type 'string' is not assignable to type 'Product'.
@Output and EventEmitter: Notifying the Parent
Now the other direction. The card has to tell the list that the add-to-cart button was clicked. Let's add an output to the child:
import { Component, EventEmitter, Input, Output } from '@angular/core';
export class ProductItemComponent {
@Input() product!: Product;
@Output() addToCart = new EventEmitter<Product>();
onAdd(): void {
this.addToCart.emit(this.product);
}
}
<button (click)="onAdd()">Sepete ekle</button>
EventEmitter is an object that lets a component publish its own event. The generic Product type says what kind of data travels with that event. The moment emit() is called, the event is fired outward.
The parent listens to it just like it would listen to a button's click:
<p>Sepette {{ cart.length }} ürün var.</p>
<app-product-item
*ngFor="let p of products"
[product]="p"
(addToCart)="onAddToCart($event)">
</app-product-item>
cart: Product[] = [];
onAddToCart(product: Product): void {
this.cart.push(product);
}
Here $event is not a DOM event; it's the product the child sent with emit(). Angular knows its type, so it goes straight into onAddToCart as a Product. Notice that the child knows nothing about the cart. It only says "I was clicked, here's the product"; the parent decides what to do. That separation is what makes the card reusable anywhere.
Our Own [( )] Binding
The banana-in-a-box syntax we saw earlier was nothing more than an input and an output meeting under a naming convention. Now that we know both, we can write our own two-way binding:
export class QuantityPickerComponent {
@Input() quantity = 1;
@Output() quantityChange = new EventEmitter<number>();
change(delta: number): void {
this.quantity = Math.max(1, this.quantity + delta);
this.quantityChange.emit(this.quantity);
}
}
<app-quantity-picker [(quantity)]="amount"></app-quantity-picker>
The rule is simple: if the input is called quantity, the output must be called quantityChange. When those two sit side by side, Angular accepts [(quantity)]; the parent's amount flows down to the child and is updated with every change in the child.
@ViewChild: Reaching the Child Directly
Sometimes the parent doesn't need to send the child data; it needs the child to do something. Reset a counter, clear a form, pause a video. Let's take a simple counter:
export class CounterComponent {
count = 0;
increment(): void {
this.count++;
}
reset(): void {
this.count = 0;
}
}
The parent can reach the counter component in its template with @ViewChild:
import { Component, ViewChild } from '@angular/core';
import { CounterComponent } from '../counter/counter.component';
export class ScoreboardComponent {
@ViewChild(CounterComponent) counter!: CounterComponent;
resetCounter(): void {
this.counter.reset();
}
}
<app-counter></app-counter>
<button (click)="resetCounter()">Sıfırla</button>
@ViewChild(CounterComponent) means "find the first CounterComponent in my template and put it in this property." Now the parent can call the child's public methods directly. If you only need it inside the template, there's a shorter way: write <app-counter #counterRef> and call counterRef.reset() from the button.
A warning: this.counter doesn't exist yet while the constructor runs. Angular fills it only after the component's view has been created. Clicking the button is fine, because by then the screen is long ready. We will come back to this timing in a moment.
Content Projection: ng-content
With inputs we sent data to a child. What if we want to send it a chunk of HTML? Think of a card that uses the same frame everywhere: the border, shadow and padding never change, but the content is different every time. Generate a component called card and put this in its template:
<div class="card">
<ng-content></ng-content>
</div>
<app-card>
<h3>Mekanik Klavye</h3>
<p>Kırmızı switch, Türkçe Q klavye.</p>
</app-card>
Everything you write between the <app-card> tags is placed wherever <ng-content> sits. This is called content projection. Think of a picture frame: the frame stays the same, the photo inside is different on every wall.
One important detail: projected content belongs to the parent. If you read a value with {{ }} inside it, that value comes from the parent's class, not the card's. The card only provides the spot.
Multiple Slots with select
If you want the card's title on top, its buttons at the bottom and the rest in the middle, you use more than one ng-content:
<div class="card">
<header class="card-header">
<ng-content select="[card-title]"></ng-content>
</header>
<div class="card-body">
<ng-content></ng-content>
</div>
<footer class="card-footer">
<ng-content select=".card-actions"></ng-content>
</footer>
</div>
<app-card>
<h3 card-title>Mekanik Klavye</h3>
<p>Kırmızı switch, Türkçe Q klavye.</p>
<div class="card-actions">
<button>Sepete ekle</button>
</div>
</app-card>
select takes a CSS selector. [card-title] catches the element carrying that attribute, and .card-actions catches the element with that class. Anything that matches no selector falls into the default slot, the one without select. It's like a bento box: each food goes into its own compartment, and the rest goes into the big one in the middle.
What About Sibling Components?
Inputs and outputs only work between a parent and its child. So how do two siblings under the same parent talk to each other? For example, when a product card is clicked, what if the cart summary next to it needs to update?
For close siblings you can go through the shared parent: the first sibling notifies the parent with an output, and the parent passes the data to the second sibling with an input. The cart array above sits in exactly that kind of shared spot; if you added a cart summary component, you could hand it the array with [items]="cart". But as the tree gets deeper, carrying data five levels up and five levels down gets tiring. At that point, keeping the data outside the tree, in a service, is a much cleaner solution. We will cover services and dependency injection in upcoming posts.
A Small Experiment
Write a task card component. It should take the task's title through an @Input(), and when its "Done" button is clicked, send the task's id to the parent through an @Output(). The parent should remove completed tasks from the list. Also add a slot at the bottom of the card with ng-content select, where the parent can put any extra buttons it wants.
Then go back to the @ViewChild example and add console.log(this.counter) to the parent's constructor. Put the same line inside the resetCounter() method too, and compare the two outputs in the console.
You will see undefined in the constructor and a fully formed component on the click. So what happened in between, and which steps does Angular take a component through to bring it to the screen? Our next topic is lifecycle hooks, which let us run code at each of those steps.