Inputs, Outputs & model()
Components become truly reusable when a parent can configure them. A dish card should show any dish, not one hard-coded item, and it should tell the menu when the user clicks "Add". Angular gives you three functions for this: input() to receive data from the parent, output() to send events to the parent, and model() for values that both sides can change.
Inputs: data flows down
An input is a property that the parent sets. Declare it with input(); the result is a read-only signal, so you read it by calling it:
import { Component, booleanAttribute, computed, input, output } from '@angular/core';
export interface Dish {
id: number;
name: string;
price: number;
veg: boolean;
}
@Component({
selector: 'app-dish-card',
template: `
<h3>{{ dish().name }} @if (dish().veg) { <span>(Veg)</span> }</h3>
<p>₹{{ priceWithGst() }} incl. GST</p>
@if (!soldOut()) {
<button (click)="added.emit(dish())">{{ buttonText() }}</button>
} @else {
<p>Sold out</p>
}
`,
})
export class DishCard {
readonly dish = input.required<Dish>();
readonly buttonText = input('Add to cart');
readonly soldOut = input(false, { transform: booleanAttribute });
readonly added = output<Dish>();
protected priceWithGst = computed(() => Math.round(this.dish().price * 1.05));
}There are three kinds of input here:
input.required<Dish>(): the parent must provide it. Forgetting it is a compile error, and inside the component the type is plainDish, neverundefined.input('Add to cart'): optional, with a default value. The type (string) is inferred from the default.input(false, { transform: booleanAttribute }): a transform converts the incoming value.booleanAttributelets the parent write the attribute on its own, likedisabledin HTML, and turns it intotrue.
Because inputs are signals, computed works with them directly: priceWithGst updates whenever the parent passes a different dish.
Passing inputs from the parent
The parent binds inputs with square brackets, exactly like DOM properties. A plain attribute without brackets passes a fixed string:
@Component({
selector: 'app-menu',
imports: [DishCard],
template: `
@for (d of dishes(); track d.id) {
<app-dish-card [dish]="d" (added)="addToCart($event)" />
}
<app-dish-card [dish]="special" buttonText="Grab it!" soldOut />
<p>Cart: {{ cart().length }} items</p>
`,
})
export class Menu {
protected dishes = signal<Dish[]>([
{ id: 1, name: 'Poha', price: 30, veg: true },
{ id: 2, name: 'Egg Bhurji', price: 50, veg: false },
]);
protected special: Dish = { id: 9, name: 'Kesar Halwa', price: 70, veg: true };
protected cart = signal<Dish[]>([]);
protected addToCart(dish: Dish) {
this.cart.update((list) => [...list, dish]);
}
}After clicking "Add to cart" on Poha twice and on Egg Bhurji once, the page shows:
Poha (Veg) ₹32 incl. GST [Add to cart] Egg Bhurji ₹53 incl. GST [Add to cart] Kesar Halwa (Veg) ₹74 incl. GST Sold out Cart: 3 items
Inputs are read-only inside the child. An InputSignal has no set(). This is deliberate: the parent owns the data, and the child only displays it. If the child needs to change something, it tells the parent through an output.
Outputs: events flow up
An output is a custom event. Declare it with output<T>(), where T is the type of data the event carries, and fire it with emit(). In DishCard, the button calls added.emit(dish()).
The parent listens with round brackets, exactly like a click: (added)="addToCart($event)". Here $event is the emitted Dish, not a browser event. This pattern, inputs down, outputs up, keeps data flow easy to follow: the menu owns the cart, the cards only report clicks.
model(): two-way binding
Some components edit a value that belongs to the parent: a quantity stepper, a star rating, a date picker. For these, use model(). A model is an input that the child is also allowed to change; each change is sent back to the parent automatically.
import { Component, input, model } from '@angular/core';
@Component({
selector: 'app-qty-stepper',
template: `
<button (click)="value.update((v) => v - 1)" [disabled]="value() <= 1">-</button>
<span>{{ value() }}</span>
<button (click)="value.update((v) => v + 1)" [disabled]="value() >= max()">+</button>
`,
})
export class QtyStepper {
readonly value = model(1);
readonly max = input(10);
}The parent uses the "banana in a box" syntax [( )] with a writable signal:
<app-qty-stepper [(value)]="plates" [max]="5" />
<p>Plates: {{ plates() }} (₹{{ plates() * 60 }})</p>Starting from plates = signal(1), two clicks on "+" give:
- 3 + Plates: 3 (₹180)
[(value)]="plates" is shorthand for [value]="plates()" plus (valueChange)="plates.set($event)". Every model automatically has an output named after it with Change added.
Inputs vs model: which one?
| Use | When |
|---|---|
input() | The child only displays the value |
input.required() | The child cannot work without it |
output() | The child reports an action: clicked, deleted, submitted |
model() | The child edits a value owned by the parent |
Common mistakes
- Trying to
set()an input. Inputs are read-only. Emit an output and let the parent change its data, or usemodel()if two-way editing is really what you need. - Forgetting the brackets.
<app-dish-card dish="d" />passes the string"d". Write[dish]="d". - Two-way binding to a plain value.
[(value)]needs a writable signal (or a plain field) on the parent; acomputedor an input cannot be written. - Reading an input in the constructor. Inputs are not set yet when the constructor runs; reading a required input there throws an error. Read inputs in templates,
computed, effects orngOnInit.
Key points
input()declares a read-only signal set by the parent;input.required()makes it mandatory.- Parents pass inputs with
[name]="expression", or a fixed string with a plain attribute. output<T>()declares a custom event; the child callsemit(), the parent listens with(name)="...".model()creates a two-way bindable value; the parent uses[(name)]="someSignal".- Data flows down through inputs and up through outputs, which keeps components predictable.
Exercise
Build a StarRating component with a rating model (default 0) that shows five star buttons. Clicking a star sets the rating; filled stars (★) are shown up to the rating and empty stars (☆) after it. Show "Not rated" or "X / 5" next to the stars. Use it in a ReviewForm parent with two-way binding and show "You gave X stars." under it.
Show solution
import { Component, computed, model, signal } from '@angular/core';
@Component({
selector: 'app-star-rating',
template: `
@for (star of stars; track star) {
<button (click)="rating.set(star)" [attr.aria-label]="star + ' stars'">
{{ star <= rating() ? '★' : '☆' }}
</button>
}
<span>{{ label() }}</span>
`,
})
export class StarRating {
readonly rating = model(0);
protected readonly stars = [1, 2, 3, 4, 5];
protected label = computed(() => (this.rating() === 0 ? 'Not rated' : `${this.rating()} / 5`));
}
@Component({
selector: 'app-review-form',
imports: [StarRating],
template: `
<p>Rate Sharma Ji's samosa:</p>
<app-star-rating [(rating)]="samosaRating" />
<p>You gave {{ samosaRating() }} stars.</p>
`,
})
export class ReviewForm {
protected samosaRating = signal(0);
}Clicking the fourth star shows ★ ★ ★ ★ ☆ 4 / 5 and "You gave 4 stars." The child changes rating with set(), and the model sends the new value back into the parent's samosaRating signal.
Quick Quiz
1. How does a child component declare an input that the parent must always provide?
input.required() makes the input mandatory and removes undefined from its type.
2. A child declares deleted = output<number>(). How does the parent listen?
Outputs are events, so the parent uses round brackets; $event is the emitted number.
3. What does [(value)]="plates" expand to for a child with value = model(1)?
Two-way binding combines an input binding and the automatic valueChange output.
4. Why can't a child call set() on an input signal?
Inputs are read-only signals; use an output or model() to send changes up.
Learn Angular with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
