input(), output() and model() between components
Key takeaway —
input()receives data from the parent as a signal,output()emits events to the parent, andmodel()creates a two-way binding[(valeur)]. Communication between components becomes fully reactive.
So far, our signals lived inside a single component. Let's now see how to make a parent and a child communicate with the signal-based APIs, which advantageously replace the historical @Input() and @Output() decorators.
input(): receiving data as a signal
The input() function declares a component input. Unlike @Input(), it returns a read-only signal: you read the value received from the parent by calling it.
import { Component, input } from '@angular/core';
@Component({
selector: 'app-badge',
standalone: true,
template: `<span>{{ libelle() }} ({{ niveau() }})</span>`,
})
export class BadgeComponent {
readonly libelle = input.required<string>(); // REQUIRED input
readonly niveau = input(1); // input with a default value
}
On the parent side:
<app-badge libelle="Expert" [niveau]="3" />
Two useful variants:
input.required<T>(): the input is required. If the parent forgets it, Angular reports it at compile time (withstrictTemplates).- Transformation:
input(0, { transform: ... })to convert the received value. A common example, acceptingdisabledwithout a value:
import { booleanAttribute, numberAttribute } from '@angular/core';
readonly desactive = input(false, { transform: booleanAttribute }); // <cmp desactive>
readonly age = input(0, { transform: numberAttribute }); // age="42" → 42
Since input() returns a signal, you can derive it directly with computed():
readonly prix = input.required<number>();
readonly prixTTC = computed(() => this.prix() * 1.2);
output(): emitting events to the parent
output() declares a typed output event. You fire it with .emit(value):
import { Component, input, output } from '@angular/core';
@Component({
selector: 'app-note',
standalone: true,
template: `
<p>Rating: {{ valeur() }} / 5</p>
<button (click)="noter.emit(valeur() + 1)" [disabled]="valeur() >= 5">
Increase
</button>
`,
})
export class NoteComponent {
readonly valeur = input(0);
readonly noter = output<number>(); // OutputEmitterRef<number>
}
On the parent side, you listen to it like a classic event:
<app-note [valeur]="qualite()" (noter)="qualite.set($event)" />
output() is simpler than an EventEmitter: no RxJS inheritance, just emit(). Here is the full, runnable parent-child example:
import { Component, input, output, signal, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-note',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<article>
<h3>{{ titre() }}</h3>
<p>Rating: {{ valeur() }} / 5</p>
<button (click)="noter.emit(valeur() + 1)" [disabled]="valeur() >= 5">
Increase
</button>
</article>
`,
})
export class NoteComponent {
readonly titre = input.required<string>();
readonly valeur = input(0);
readonly noter = output<number>();
}
@Component({
selector: 'app-root',
standalone: true,
imports: [NoteComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<h1>Product review</h1>
<app-note titre="Qualité" [valeur]="qualite()" (noter)="qualite.set($event)" />
<p>Saved rating: {{ qualite() }} / 5</p>
`,
})
export class AppComponent {
readonly qualite = signal(3);
}
Try it live
input-output-note
model(): two-way binding
model() combines an input and an output into a single signal modifiable from both sides. It is the modern equivalent of the @Input() + @Output() pair that made the [(banana-in-a-box)] possible.
Inside the component, a model is a writable signal: you read it and apply set/update to it. On the parent side, you bind it with the [(...)] syntax:
import { Component, input, model, signal, ChangeDetectionStrategy } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-champ',
standalone: true,
imports: [FormsModule],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<label>
{{ libelle() }}:
<input [(ngModel)]="valeur" />
</label>
<button (click)="valeur.set('')">Clear</button>
`,
})
export class ChampComponent {
readonly libelle = input('Champ');
readonly valeur = model(''); // ModelSignal<string>
}
@Component({
selector: 'app-root',
standalone: true,
imports: [ChampComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<h1>Form</h1>
<app-champ libelle="Pseudo" [(valeur)]="pseudo" />
<p>Hello {{ pseudo() || 'inconnu' }} 👋</p>
`,
})
export class AppComponent {
readonly pseudo = signal('');
}
When the user types in the field, valeur changes in the child, which propagates the new value to the parent's pseudo — and vice versa. One signal, both directions.
Try it live
model-champ
input/output/model: which one to choose?
| Need | API | Parent binding |
|---|---|---|
| Receive data from the parent | input() |
[valeur] |
| Notify the parent of an event | output() |
(evenement) |
| Data modifiable from both sides | model() |
[(valeur)] |
Simple rule: start with input() + output(). Only move to model() when one same value truly needs to be driven by the parent AND modified by the child (reusable form fields, toggle, slider, date-picker style components).
Key points
input()/input.required(): inputs as signals, derivable withcomputed, transformable viatransform.output(): typed events emitted with.emit(), without RxJS.model(): two-way binding[(...)], a writable signal shared between parent and child.
In chapter 7, we connect signals to the asynchronous world with RxJS interop: toSignal(), toObservable() and the resource() pattern.