On this page
Inputs, outputs, and model signals
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 parentmodel()— 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
- Create a component with model(): Implement a
RangeSlidercomponent with amodel()for the value and use[(value)]in the parent component to achieve two-way binding. - Use input with transform: Create an input that receives a string
"true"or"false"and automatically transforms it to a boolean using thetransformoption. - Container-presentational pattern: Separate an existing component into a container component (with logic and state) and a presentational component (only
input()andoutput()).
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>();
}
Sign in to track your progress