Using Model Signals for Two-Way Binding in Angular
Problem Statement
Angular developers transitioning to signals often encounter a common scenario: attempting to modify an input signal (input()
) from within a component. Many expect syntax like:
this.selected.set(true); // Doesn't work!
this.selected().set(true); // Also doesn't work!
These attempts fail, leaving developers confused about how to manage component-internal updates that should propagate to parent components. This occurs due to a fundamental characteristic of Angular's signal-based inputs.
Why Input Signals Are Immutable
Angular's input()
creates a read-only signal designed only to receive values from parent components. Its value cannot be modified from the child component, as this would violate the unidirectional data flow principle.
Solution Overview
For two-way data binding where a child component needs to both:
- Receive initial values from a parent
- Update that value and notify the parent
Use Angular's model()
signal instead of input()
. Model signals provide readable and writable signals that automatically synchronize with parent components using two-way binding syntax.
How Model Signals Work
Defining a Model Signal in a Child Component
import { model } from '@angular/core';
export class MyComponent {
// Define a model signal with default value
selected = model(false);
toggleSelection() {
// Update the model signal's value
this.selected.update(current => !current);
}
}
Binding in Parent Template
<app-child [(selected)]="isChildSelected" />
In the parent component:
isChildSelected = signal(true);
Key Characteristics
model()
signals are writable (unlikeinput()
signals)- Changes propagate to both parent and child components
- Uses familiar banana-in-the-box
[( )]
syntax
Why Model Signals (Not Input Signals)
Feature | input() | model() |
---|---|---|
Writable from child | ❌ | ✅ |
Parent binding | One-way ([prop] ) | Two-way ([(prop)] ) |
Initial value | Required/defaultable | Required/defaultable |
Value propagation | Parent → Child | Parent ↔ Child |
Full Example Implementation
Child Component (counter.component.ts
):
import { Component, model } from '@angular/core';
@Component({
selector: 'app-counter',
template: `
<button (click)="increment()">Count: {{ value() }}</button>
`,
standalone: true
})
export class CounterComponent {
value = model(0); // Writable model signal
increment() {
this.value.update(current => current + 1);
}
}
Parent Component (app.component.ts
):
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<p>Current count: {{ count() }}</p>
<app-counter [(value)]="count" />
`
})
export class AppComponent {
count = signal(5); // Initialized with 5
}
Common Use Cases
- Form controls (toggles, inputs, selects)
- Interactive widgets (sliders, rating controls)
- Expand/collapse components
- Any scenario needing child-initiated value updates
When to Use Input Signals Instead
Stick with input()
when:
- The value should only be set by the parent
- You need a read-only value inside the component
- You're using transform functions for input processing
- You require input validation using
input.required()
Additional Consideration
If you cannot use model()
(Angular versions < 17.2), implement a two-way binding manually:
// Child Component
@Input() selected = false;
@Output() selectedChange = new EventEmitter<boolean>();
updateSelection(newValue: boolean) {
this.selected = newValue;
this.selectedChange.emit(newValue);
}
Best Practice
For modern Angular projects (v17.2+), prefer model()
over manual input/output pairs for cleaner implementation of two-way binding scenarios.
The model()
API provides an elegant solution for child-to-parent value synchronization using Angular's signals paradigm. By understanding the distinction between input()
(read-only) and model()
(writable) signals, you can architect cleaner component communication patterns.