EthicsComputerCourses
Join training +91 90532 10052

Inputs, Outputs & model()

Angular · Lesson 10 of 41 Intermediate 6 min read Updated

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:

TypeScript
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 plain Dish, never undefined.
  • 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. booleanAttribute lets the parent write the attribute on its own, like disabled in HTML, and turns it into true.

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:

TypeScript
@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:

Output
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.

TypeScript
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:

HTML
<app-qty-stepper [(value)]="plates" [max]="5" />
<p>Plates: {{ plates() }} (₹{{ plates() * 60 }})</p>

Starting from plates = signal(1), two clicks on "+" give:

Output
- 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?

UseWhen
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 use model() 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; a computed or 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 or ngOnInit.

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 calls emit(), 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
TypeScript
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?

2. A child declares deleted = output<number>(). How does the parent listen?

3. What does [(value)]="plates" expand to for a child with value = model(1)?

4. Why can't a child call set() on an input signal?

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