On this page

Inputs, outputs, and model signals

12 min read TextCh. 2 — Reactivity

Communication between components

Angular provides three primitives for parent-child communication:

  • input() — The parent sends data to the child (unidirectional)
  • output() — The child emits events to the parent
  • model() — Bidirectional binding between parent and child

Input signals

input() replaces the @Input decorator. They are read-only signals:

import { input } from '@angular/core';

// Optional input with default value
readonly title = input('Untitled');

// Required input (mandatory in the template)
readonly userId = input.required<number>();

// Input with transformation
readonly disabled = input(false, {
  transform: (value: string | boolean) => typeof value === 'string' ? value !== 'false' : value,
});

// Input with alias
readonly data = input<string[]>([], { alias: 'data' });

Using inputs in the parent template

<!-- String literal (no brackets) -->
<app-card title="Welcome" />

<!-- Binding to expression (with brackets) -->
<app-card [title]="titleSignal()" [userId]="42" />

Output signals

output() replaces the @Output decorator. They emit events to the parent component:

import { output } from '@angular/core';

// Simple output
readonly click = output<void>();

// Output with data
readonly selected = output<number>();

// Output with alias
readonly close = output<void>({ alias: 'close' });

Emitting an event

// In the component method
this.selected.emit(42);
this.click.emit();

Listening in the parent

<app-list (selected)="handleSelection($event)" />

Model signals

model() creates a signal that supports two-way binding. Internally it generates an input and an output with the Change suffix:

import { model } from '@angular/core';

// model() === input value + output valueChange
readonly value = model(0);

// Required model
readonly selection = model.required<string>();

Two-way binding

<!-- The parent can read and write the child's model -->
<app-slider [(value)]="myValue" />

<!-- Long-form equivalent -->
<app-slider [value]="myValue()" (valueChange)="myValue.set($event)" />

Container-presentational pattern

A common pattern is to separate logic (container) from presentation (pure UI):

// Presentational component: only inputs + outputs
@Component({ selector: 'app-product-card', /* ... */ })
export class ProductCard {
  readonly product = input.required<Product>();
  readonly addToCart = output<number>();
}

// Container component: manages state and logic
@Component({ selector: 'app-store', /* ... */ })
export class Store {
  readonly products = signal<Product[]>([]);

  addProduct(id: number): void {
    // business logic
  }
}

API summary

API Read Write Two-way Usage
input() Yes No No Receive data from parent
output() No Emit No Notify parent
model() Yes Yes Yes Bidirectional binding

Practice

  1. Create a component with model(): Implement a RangeSlider component with a model() for the value and use [(value)] in the parent component to achieve two-way binding.
  2. Use input with transform: Create an input that receives a string "true" or "false" and automatically transforms it to a boolean using the transform option.
  3. Container-presentational pattern: Separate an existing component into a container component (with logic and state) and a presentational component (only input() and output()).

In the next lesson, we will learn about services and dependency injection, the backbone of Angular.

model() vs input() + output()
Use model() when you need two-way binding ([(value)]). It is equivalent to having an input() + an output named valueChange, but with less boilerplate.
Inputs with alias
You can rename an input for the external template: input({ alias: 'title' }). The name in the class will be different from the name in the HTML.
import {
  Component, input, output, model,
  computed, ChangeDetectionStrategy,
} from '@angular/core';

// ---- Child component: Slider ----
@Component({
  selector: 'app-slider',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="slider">
      <label>{{ label() }}</label>
      <input
        type="range"
        [min]="min()"
        [max]="max()"
        [value]="value()"
        (input)="value.set(+$any($event.target).value)"
      />
      <span>{{ value() }}</span>
    </div>
  `,
})
export class Slider {
  // Required input
  readonly label = input.required<string>();

  // Inputs with default values
  readonly min = input(0);
  readonly max = input(100);

  // Model: automatic two-way binding
  readonly value = model(50);
}

// ---- Parent component ----
@Component({
  selector: 'app-color-editor',
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [Slider],
  template: `
    <h2>Color editor</h2>
    <app-slider label="Red"   [max]="255" [(value)]="red" />
    <app-slider label="Green" [max]="255" [(value)]="green" />
    <app-slider label="Blue"  [max]="255" [(value)]="blue" />

    <div
      class="preview"
      [style.background-color]="colorRGB()"
    >
      {{ colorRGB() }}
    </div>

    <button (click)="copy.emit(colorRGB())">Copy color</button>
  `,
})
export class ColorEditor {
  readonly red = model(128);
  readonly green = model(128);
  readonly blue = model(64);

  readonly colorRGB = computed(
    () => `rgb(${this.red()}, ${this.green()}, ${this.blue()})`
  );

  readonly copy = output<string>();
}