Signal Inputs and model()
Angular 17.1 introduced signal inputs — a function-based API, input() and input.required(), that replaces @Input() with a value exposed as a readonly Signal rather than a plain class property. Angular 17.2 followed with model(), which builds on signal inputs to provide a two-way-bindable signal, replacing the older @Input()/@Output() pair pattern used for banana-in-a-box syntax ([(ngModel)]-style custom two-way bindings). These APIs are part of Angular's broader shift toward signals as the primary reactivity primitive, and as of Angular 17+ they are the recommended default for new component inputs, offering better type safety, explicit reactivity, and tighter integration with computed() and effect().
Cricket analogy: The shift from @Input() to input() is like a cricket board moving from a paper team sheet a manager could quietly scribble changes onto, to a tamper-evident digital lineup that locks once submitted and can only be read, not silently altered mid-innings.
input() and input.required()
Where @Input() decorates a mutable class property, input() is called as a class field initializer that returns a read-only Signal<T>. You read the current value by calling it as a function, e.g. this.title(), rather than accessing it directly. input() accepts an optional default value and options object (alias, transform); input.required<T>() omits the default and instead requires the parent to bind it, enforced at compile time exactly like @Input({ required: true }) but as the primary, more concise spelling. Because the returned value is a signal, it composes directly with computed() to derive values and automatically participates in Angular's fine-grained, signal-based change detection without needing OnPush boilerplate reasoning.
Cricket analogy: Calling this.title() to read a signal input is like a scorer having to actually check today's official scorecard rather than assuming yesterday's total, since input.required<T>() is like a match that cannot start until the toss result is formally recorded.
@Component({
selector: 'app-price-tag',
standalone: true,
template: `
<span class="price">{{ formatted() }}</span>
`
})
export class PriceTagComponent {
amount = input.required<number>();
currency = input<string>('USD');
formatted = computed(() =>
new Intl.NumberFormat('en-US', { style: 'currency', currency: this.currency() })
.format(this.amount())
);
}model() for Two-Way Binding
model() creates a signal that is both readable and writable from inside the component, and automatically generates a paired output (named propertyNameChange) so the parent can use Angular's banana-in-a-box [(syntax))="parentSignal" for two-way binding — the same sugar that powers [(ngModel)]. Calling .set() or .update() on a model signal from within the child both updates the local value and emits the change event, so the parent's bound signal or property stays in sync without you manually wiring an @Output(). This collapses what previously required a paired @Input() + @Output() with a manually-emitted change event into a single declaration.
Cricket analogy: model() is like a shared scoreboard that both the scorer and the broadcaster can update, where the scorer changing the run tally automatically pushes the update out to the broadcast feed without a separate manual radio call.
@Component({
selector: 'app-quantity-stepper',
standalone: true,
template: `
<button (click)="decrement()">-</button>
<span>{{ quantity() }}</span>
<button (click)="increment()">+</button>
`
})
export class QuantityStepperComponent {
quantity = model<number>(1);
increment(): void {
this.quantity.update(q => q + 1);
}
decrement(): void {
this.quantity.update(q => Math.max(0, q - 1));
}
}
// Parent template:
// <app-quantity-stepper [(quantity)]="cartQty" />model() is conceptually close to Vue's defineModel() macro or a controlled/uncontrolled hybrid in React with a value + onChange pair — but Angular generates the paired 'change' output for you automatically, so [(quantity)] in the parent template desugars to [quantity]="cartQty" (quantityChange)="cartQty = $event", exactly mirroring how [(ngModel)] itself works.
A signal returned by input() is read-only from inside the component that declares it — calling .set() on a plain input() signal is a compile error. Only model() signals are writable locally. If you need to derive local mutable state seeded from an input, create a separate local signal initialized from the input's value (often inside an effect() or constructor), rather than trying to mutate the input signal itself.
Because input(), input.required(), and model() are functions rather than decorators, they can be used in any class field context without needing experimental decorator support enabled, and they read naturally alongside signal(), computed(), and effect(). Angular's migration schematics (ng generate @angular/core:signal-input-migration) can automatically convert existing @Input() properties to input() across a codebase.
Cricket analogy: Migration schematics converting @Input() to input() automatically across a codebase is like a cricket board retrofitting every stadium's old manual scoreboard to a new digital system in one coordinated rollout rather than replacing each ground one at a time by hand.
- input() and input.required() are function-based, signal-returning replacements for @Input(), read by calling the property as a function (e.g. title()).
- Signal inputs are read-only from within the component; mutating them requires deriving a separate local signal.
- model() creates a writable, two-way-bindable signal and automatically generates a matching '<name>Change' output.
- [(propName)]="parentSignal" on a model() input desugars to the same property+event binding pattern as [(ngModel)].
- Signal inputs compose naturally with computed() and effect(), integrating tightly with Angular's fine-grained reactivity model.
- Angular provides automated migration schematics to convert existing @Input()/@Output() pairs to the signal-based equivalents.
Practice what you learned
1. How do you read the current value of a property declared with `title = input<string>('default')`?
2. What does model() automatically generate alongside the writable signal?
3. Can you call .set() on a signal created with plain input() (not model())?
4. What does [(quantity)]="cartQty" desugar to when `quantity` is a model() input?
5. Which Angular version range introduced input()/input.required() and model()?
Was this page helpful?
You May Also Like
Introduction to Signals
Understand Angular Signals — the reactive primitive that wraps a value and notifies dependents on change, forming the foundation of Angular's modern change-detection model.
Input and Output Decorators
Understand how @Input() and @Output() enable parent-child component communication in Angular, including binding syntax, EventEmitter, and input transforms.
computed() and effect()
Learn how computed() derives memoized reactive values from signals, and how effect() runs side effects automatically whenever the signals it reads change.
The New Control Flow Syntax (@if, @for, @switch)
Explore Angular's built-in @if, @for, and @switch template control-flow blocks, introduced to replace *ngIf/*ngFor/*ngSwitch with faster, more ergonomic syntax.
Related Reading
Related Study Notes in Web Development
Browse all study notesWebSockets Study Notes
Web Development · 30 topics
Web DevelopmentWebAssembly Study Notes
WebAssembly · 30 topics
Web DevelopmentgRPC Study Notes
Protocol Buffers · 30 topics
Web DevelopmentSpring Boot Study Notes
Java · 30 topics
Web DevelopmentFlask Study Notes
Python · 30 topics
Web DevelopmentDjango Study Notes
Python · 30 topics