#Parent to child with input()
Components form a tree (see Parent and child components), and data moves through it along the edges. The basic rule is data down, events up: a parent hands values to its child through inputs, and the child reports back through outputs. A child never reaches up and edits its parent's state, and a parent never pokes at a child's fields directly. That one-directional flow is what makes it possible to look at a template and know where a value comes from.
An input is a public property of the child that the parent can bind to with [name]="expression". In modern Angular you declare it with the input() function, which creates an InputSignal: a read-only signal whose value Angular sets from the parent's binding. You read it by calling it, name(), in the template or in TypeScript, and because it's a signal, anything derived from it (computed, the template) updates automatically when the parent sends a new value.
A mental model: an input is a one-way pipe from the parent's expression into a read-only signal in the child. The parent owns the tap. The child can drink from it but can't pour anything back up it.
import { Component, Input, input, signal } from '@angular/core';
// Modern: signal inputs
@Component({
selector: 'app-user-card',
template: `<h3>{{ name() }}</h3><p class="role">{{ role() }}</p>`,
})
export class UserCard {
name = input.required<string>();
role = input('guest');
}
// Old: decorator inputs
@Component({
selector: 'app-legacy-user-card',
template: `<h3>{{ name }}</h3><p class="role">{{ role }}</p>`,
})
export class LegacyUserCard {
@Input({ required: true }) name!: string;
@Input() role = 'guest';
}
@Component({
selector: 'app-team',
imports: [UserCard, LegacyUserCard],
template: `
<app-user-card [name]="lead().name" [role]="lead().role" />
<app-user-card name="Linus" />
<app-legacy-user-card [name]="lead().name" />
`,
})
export class Team {
lead = signal({ name: 'Ada', role: 'admin' });
}Team to UserCardconst f = TestBed.createComponent(Team);
await f.whenStable();
expect(all(el, 'h3')).toEqual(['Ada', 'Linus', 'Ada']);
expect(all(el, '.role')).toEqual(['admin', 'guest', 'guest']);
f.componentInstance.lead.set({ name: 'Grace', role: 'owner' });
await f.whenStable();
expect(all(el, 'h3')).toEqual(['Grace', 'Linus', 'Grace']);
expect(all(el, '.role')).toEqual(['owner', 'guest', 'guest']);name = input.required<string>()declares an input with no default that the parent must bind (next topic).role = input('guest')declares an optional input whose value is'guest'until a parent binds something; its type,InputSignal<string>, is inferred from the default.Teammust listUserCardin itsimports, or<app-user-card>is an unknown element. When Angular createsTeam's view, it creates oneUserCardinstance per<app-user-card>tag — two separate instances with separate inputs.- On the first check Angular evaluates
lead().namein the parent's context ('Ada') and writes it into the first child'snameinput. The second child usesname="Linus", a plain attribute: that's a static string input, set once. Itsroleisn't bound, so it keeps'guest'. - The child's template reads
name()androle(). Reading the input signals is what ties the child's view to them. lead.set({ name: 'Grace', role: 'owner' })changes the parent's signal. On the next check Angular re-evaluates the bindingslead().nameandlead().role, sees new values, sets the children's inputs, and the children re-render:Grace/owner. The second card is untouched —Linus/guest— because nothing it's bound to changed.- The legacy card does the same with
@Input(): Angular assigns the field (this.name = 'Ada') instead of setting a signal, and the template reads the plain field{{ name }}.name!is there because TypeScript can't know Angular will assign it before use. It works, but nothing in TypeScript can react to the change exceptngOnChangesor a setter — that's the main reason signal inputs replaced it.
Inputs are read-only from the inside. This is deliberate — the parent owns the value — and the compiler enforces it:
export class StrictPage {
page = input(1);
rename() {
this.page.set(2); // ❌ TS2339: Property 'set' does not exist on type 'InputSignal<number>'.
}
}setInputIf the child needs to change the value and tell the parent, that's model(). If it needs a local copy that starts from the input but can then diverge (an editable draft), that's linkedSignal.
Strict templates also type-check the binding itself: <app-user-card [name]="42" /> fails with TS2322: Type 'number' is not assignable to type 'string'.
In tests you don't have a parent template, so you set inputs on the component ref. This is the API Angular wants you to use (it goes through the same path a template binding does):
const f = TestBed.createComponent(UserCard);
f.componentRef.setInput('name', 'Barbara');
await f.whenStable();
// <h3>Barbara</h3><p class="role">guest</p>@Input() vs input() reference tableOld: @Input() | Modern: input() | |
|---|---|---|
| Declaration | @Input() role = 'guest'; | role = input('guest'); |
| Required | @Input({ required: true }) name!: string; | name = input.required<string>(); |
| Read | this.role | this.role() |
| React to changes | ngOnChanges or a setter | computed() / effect() |
| Writable inside the child | Yes (and that's a bug magnet) | No — compile error |
| Transform | @Input({ transform: numberAttribute }) | input(0, { transform: numberAttribute }) |
| Rename | @Input('alias') / { alias } | input('', { alias: 'label' }) |