@Input and @Output in Angular
@Input and @Output are the primary mechanism for parent-child component communication in Angular. @Input lets a parent pass data down to a child; @Output lets a child send events up to the parent. Together they form the component API.
@Input — Passing Data Down
Decorate a child component property with @Input() to allow the parent to bind to it. The parent uses property binding [inputName]="value" to supply the value.
// user-card.component.ts (child)
import { Component, Input } from '@angular/core';
interface User {
id: number;
name: string;
avatar: string;
role: string;
}
@Component({
selector: 'app-user-card',
standalone: true,
template: `
<div class="card">
<img [src]="user.avatar" [alt]="user.name" />
<h3>{{ user.name }}</h3>
<span class="badge">{{ user.role }}</span>
</div>
`,
})
export class UserCardComponent {
@Input() user!: User; // required input
@Input() compact = false; // optional with default
}
// parent component
import { Component } from '@angular/core';
import { UserCardComponent } from './user-card.component';
@Component({
selector: 'app-team',
standalone: true,
imports: [UserCardComponent],
template: `
@for (member of team; track member.id) {
<app-user-card [user]="member" [compact]="true" />
}
`,
})
export class TeamComponent {
team = [
{ id: 1, name: 'Alice', avatar: '/img/alice.jpg', role: 'Developer' },
{ id: 2, name: 'Bob', avatar: '/img/bob.jpg', role: 'Designer' },
];
}
Required Inputs (Angular 16+)
From Angular 16 you can mark an input as required. Angular will throw a compile-time error if the parent forgets to provide it — no more runtime crashes from undefined inputs.
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-button',
standalone: true,
template: `<button [class]="variant">{{ label }}</button>`,
})
export class ButtonComponent {
@Input({ required: true }) label!: string; // must be provided
@Input() variant = 'primary'; // optional, has default
}
Input Aliases
You can expose an input under a different public name than the internal property name using an alias.
@Component({ selector: 'app-item', standalone: true, template: '' })
export class ItemComponent {
// Public API uses 'itemData', internally stored as 'data'
@Input('itemData') data!: { id: number; title: string };
}
<!-- Parent uses the alias --> <app-item [itemData]="selectedItem" />
Input Transform (Angular 16.1+)
The transform option lets you coerce or convert the value before it is assigned to the property. A common use case is accepting a boolean-like string attribute.
import { Component, Input, booleanAttribute, numberAttribute } from '@angular/core';
@Component({
selector: 'app-chip',
standalone: true,
template: `<span [class.dismissible]="dismissible">{{ label }}</span>`,
})
export class ChipComponent {
@Input({ required: true }) label!: string;
// Accepts: [dismissible]="true", [dismissible]="false", or just the attribute presence
@Input({ transform: booleanAttribute }) dismissible = false;
// Coerces string "42" → number 42
@Input({ transform: numberAttribute }) maxLength = 100;
}
<!-- These three are now equivalent --> <app-chip label="Angular" [dismissible]="true" /> <app-chip label="Angular" dismissible="true" /> <app-chip label="Angular" dismissible />
@Output — Emitting Events Up
@Output() combined with EventEmitter lets a child component notify its parent that something happened. The parent listens using event binding (eventName)="handler($event)".
// rating.component.ts (child)
import { Component, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-rating',
standalone: true,
template: `
@for (star of stars; track star) {
<button
(click)="select(star)"
[class.filled]="star <= current"
>★</button>
}
`,
})
export class RatingComponent {
@Input() current = 0;
@Output() ratingChange = new EventEmitter<number>();
stars = [1, 2, 3, 4, 5];
select(value: number) {
this.ratingChange.emit(value);
}
}
// parent component
import { Component } from '@angular/core';
import { RatingComponent } from './rating.component';
@Component({
selector: 'app-product',
standalone: true,
imports: [RatingComponent],
template: `
<h2>{{ product.name }}</h2>
<app-rating [current]="product.rating" (ratingChange)="onRate($event)" />
<p>Your rating: {{ product.rating }} / 5</p>
`,
})
export class ProductComponent {
product = { name: 'Angular Course', rating: 4 };
onRate(value: number) {
this.product.rating = value;
console.log('Saved rating:', value);
}
}
Output Aliases
Like @Input, @Output supports aliases for separating the public event name from the internal property name.
export class SearchComponent {
// Public event is named 'search', internally it's the 'searchEvent' property
@Output('search') searchEvent = new EventEmitter<string>();
emit(query: string) {
this.searchEvent.emit(query);
}
}
<!-- Parent uses the alias --> <app-search (search)="onSearch($event)" />
Signal-Based Inputs and Outputs (Angular 17.1+)
Angular 17.1 introduced input() and output() functions as the modern, signal-based alternative to @Input and @Output decorators. They provide better type safety and integrate with Angular's Signals reactivity system.
import { Component, input, output, computed } from '@angular/core';
@Component({
selector: 'app-product-card',
standalone: true,
template: `
<div [class.featured]="featured()">
<h3>{{ title() }}</h3>
<p>{{ formattedPrice() }}</p>
<button (click)="addToCart()">Add to Cart</button>
</div>
`,
})
export class ProductCardComponent {
// input() — required
title = input.required<string>();
// input() — optional with default
price = input(0);
featured = input(false);
// Computed from input signal
formattedPrice = computed(() =>
new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' })
.format(this.price())
);
// output() — replaces @Output + EventEmitter
addedToCart = output<{ title: string }>();
addToCart() {
this.addedToCart.emit({ title: this.title() });
}
}
Comparison: Decorator vs Signal API
Feature | @Input / @Output | input() / output() (17.1+) |
|---|---|---|
Import | @angular/core decorators | input, output functions |
Type inference | Manual with ! | Automatic from generics |
Reactivity | Zone.js / OnPush | Fine-grained signal tracking |
Required inputs | @Input({ required: true }) | input.required<Type>() |
Default value | @Input() prop = default | input(defaultValue) |
Alias | @Input("alias") | input({ alias: "name" }) |
Transform | @Input({ transform }) | input({ transform }) |
Event emitter | new EventEmitter<T>() | output<T>() |
Complete Example: Shopping Cart Item
// cart-item.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';
export interface CartItem {
id: number;
name: string;
price: number;
quantity: number;
}
@Component({
selector: 'app-cart-item',
standalone: true,
template: `
<div class="cart-item">
<span class="name">{{ item.name }}</span>
<span class="price">{{ item.price | currency }}</span>
<div class="qty-controls">
<button (click)="changeQty(-1)" [disabled]="item.quantity <= 1">-</button>
<span>{{ item.quantity }}</span>
<button (click)="changeQty(1)">+</button>
</div>
<span class="subtotal">{{ item.price * item.quantity | currency }}</span>
<button class="remove" (click)="remove()">Remove</button>
</div>
`,
})
export class CartItemComponent {
@Input({ required: true }) item!: CartItem;
@Output() quantityChanged = new EventEmitter<{ id: number; quantity: number }>();
@Output() removed = new EventEmitter<number>();
changeQty(delta: number) {
const newQty = this.item.quantity + delta;
if (newQty >= 1) {
this.quantityChanged.emit({ id: this.item.id, quantity: newQty });
}
}
remove() {
this.removed.emit(this.item.id);
}
}
// cart.component.ts (parent)
@Component({
selector: 'app-cart',
standalone: true,
imports: [CartItemComponent, CurrencyPipe],
template: `
@for (item of items; track item.id) {
<app-cart-item
[item]="item"
(quantityChanged)="updateQty($event)"
(removed)="removeItem($event)"
/>
}
<p>Total: {{ total | currency }}</p>
`,
})
export class CartComponent {
items: CartItem[] = [
{ id: 1, name: 'Laptop Stand', price: 49.99, quantity: 1 },
{ id: 2, name: 'Keyboard', price: 89.99, quantity: 2 },
];
get total() {
return this.items.reduce((sum, i) => sum + i.price * i.quantity, 0);
}
updateQty(event: { id: number; quantity: number }) {
const item = this.items.find(i => i.id === event.id);
if (item) item.quantity = event.quantity;
}
removeItem(id: number) {
this.items = this.items.filter(i => i.id !== id);
}
}
Summary
@Input() — marks a property as receivable from a parent via [property]="value".
@Input({ required: true }) — Angular 16+ compile-time enforcement.
@Input({ transform }) — coerce input values (booleanAttribute, numberAttribute).
@Output() + EventEmitter<T> — emit typed events to the parent.
Parent listens with (outputName)="handler($event)".
Signal API: input() / input.required<T>() and output<T>() are the modern Angular 17.1+ equivalents.
@Input alias and @Output alias decouple public API names from internal property names.