Two-way Binding
Basic Two-way Binding
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-demo',
standalone: true,
imports: [FormsModule],
template: `
<input [(ngModel)]="name">
<p>Hello {{ name }}!</p>
`
})
export class DemoComponent {
name = 'John';
}
How it Works
<!-- Two-way binding -->
<input [(ngModel)]="name">
<!-- Equivalent to: -->
<input
[ngModel]="name"
(ngModelChange)="name = $event">
Custom Two-way Binding (Signal-based with model())
import { Component, model, signal } from '@angular/core';
@Component({
selector: 'app-counter',
standalone: true,
template: `
<button (click)="decrement()">-</button>
<span>{{ value() }}</span>
<button (click)="increment()">+</button>
`
})
export class CounterComponent {
// model() creates a writable signal with two-way binding
value = model<number>(0);
increment() {
this.value.update(v => v + 1);
}
decrement() {
this.value.update(v => v - 1);
}
}
// Usage
@Component({
standalone: true,
imports: [CounterComponent],
template: `
<app-counter [(value)]="count"></app-counter>
<p>Count: {{ count() }}</p>
`
})
export class ParentComponent {
count = signal(0);
}
Signal-based Two-way Binding
import { Component, model } from '@angular/core';
@Component({
selector: 'app-input',
standalone: true,
template: `
<input
[value]="value()"
(input)="value.set($any($event.target).value)">
`
})
export class InputComponent {
// model() creates a writable signal with two-way binding
value = model('');
}
// Usage
@Component({
template: `
<app-input [(value)]="text"></app-input>
<p>{{ text() }}</p>
`
})
export class ParentComponent {
text = signal('');
}
Multiple Two-way Bindings
import { Component, model, signal } from '@angular/core';
@Component({
selector: 'app-size-picker',
standalone: true,
template: `
<input
type="range"
[value]="width()"
(input)="width.set(+($event.target as HTMLInputElement).value)">
<input
type="range"
[value]="height()"
(input)="height.set(+($event.target as HTMLInputElement).value)">
`
})
export class SizePickerComponent {
// Multiple model() signals for independent two-way bindings
width = model<number>(100);
height = model<number>(100);
}
// Usage
@Component({
standalone: true,
imports: [SizePickerComponent],
template: `
<app-size-picker
[(width)]="boxWidth"
[(height)]="boxHeight">
</app-size-picker>
<p>Width: {{ boxWidth() }}, Height: {{ boxHeight() }}</p>
`
})
export class ParentComponent {
boxWidth = signal(100);
boxHeight = signal(100);
}
Two-way Binding with Objects
import { Component, model, signal } from '@angular/core';
interface User {
name: string;
email: string;
}
@Component({
selector: 'app-user-form',
standalone: true,
template: `
<input
[value]="user().name"
(input)="onNameChange($event)">
<input
[value]="user().email"
(input)="onEmailChange($event)">
`
})
export class UserFormComponent {
// model() for object two-way binding
user = model.required<User>();
onNameChange(event: Event) {
const name = (event.target as HTMLInputElement).value;
// Update with immutable pattern
this.user.update(u => ({ ...u, name }));
}
onEmailChange(event: Event) {
const email = (event.target as HTMLInputElement).value;
// Update with immutable pattern
this.user.update(u => ({ ...u, email }));
}
}
// Usage
@Component({
standalone: true,
imports: [UserFormComponent],
template: `
<app-user-form [(user)]="currentUser"></app-user-form>
<p>Name: {{ currentUser().name }}, Email: {{ currentUser().email }}</p>
`
})
export class ParentComponent {
currentUser = signal<User>({ name: 'John', email: 'john@example.com' });
}
Transformation Pattern (Signal-based)
import { Component, model, computed } from '@angular/core';
@Component({
selector: 'app-temperature',
standalone: true,
template: `
<p>°C: {{ celsius() }}</p>
<input
type="number"
[value]="fahrenheit()"
(input)="setFahrenheit(+($event.target as HTMLInputElement).value)">
<p>°F: {{ fahrenheit() }}</p>
`
})
export class TemperatureComponent {
// model() for two-way binding with parent
celsius = model<number>(0);
// Computed signal for derived value
fahrenheit = computed(() => this.celsius() * 9/5 + 32);
setFahrenheit(value: number) {
// Convert and update celsius
this.celsius.set((value - 32) * 5/9);
}
}
// Usage
@Component({
standalone: true,
imports: [TemperatureComponent],
template: `
<app-temperature [(celsius)]="temp"></app-temperature>
<p>Temperature: {{ temp() }}°C</p>
`
})
export class ParentComponent {
temp = signal(20);
}
Form Controls
import { Component } from '@angular/core';
import { ReactiveFormsModule, FormControl } from '@angular/forms';
@Component({
standalone: true,
imports: [ReactiveFormsModule],
template: `
<input [formControl]="nameControl">
<p>{{ nameControl.value }}</p>
`
})
export class FormComponent {
nameControl = new FormControl('');
ngOnInit() {
this.nameControl.valueChanges.subscribe(value => {
console.log('Value changed:', value);
});
}
}
Checkbox Two-way Binding
@Component({
template: `
<input
type="checkbox"
[(ngModel)]="isChecked">
<p>{{ isChecked ? 'Checked' : 'Unchecked' }}</p>
`
})
export class CheckboxComponent {
isChecked = false;
}
Radio Buttons
@Component({
template: `
<label>
<input type="radio" [(ngModel)]="size" value="small">
Small
</label>
<label>
<input type="radio" [(ngModel)]="size" value="medium">
Medium
</label>
<label>
<input type="radio" [(ngModel)]="size" value="large">
Large
</label>
<p>Selected: {{ size }}</p>
`
})
export class RadioComponent {
size = 'medium';
}
Select Dropdown
@Component({
template: `
<select [(ngModel)]="selectedCity">
<option *ngFor="let city of cities" [value]="city">
{{ city }}
</option>
</select>
<p>Selected: {{ selectedCity }}</p>
`
})
export class SelectComponent {
cities = ['Paris', 'London', 'New York'];
selectedCity = 'Paris';
}
Custom Control (ControlValueAccessor)
import { Component, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
@Component({
selector: 'app-star-rating',
template: `
<span *ngFor="let star of stars; let i = index"
(click)="rate(i + 1)"
[class.filled]="i < rating">
★
</span>
`,
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => StarRatingComponent),
multi: true
}]
})
export class StarRatingComponent implements ControlValueAccessor {
rating = 0;
stars = [1, 2, 3, 4, 5];
onChange: any = () => {};
onTouched: any = () => {};
rate(rating: number) {
this.rating = rating;
this.onChange(rating);
this.onTouched();
}
writeValue(value: number): void {
this.rating = value;
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
}
// Usage with ngModel
<app-star-rating [(ngModel)]="userRating"></app-star-rating>
// Usage with FormControl
<app-star-rating [formControl]="ratingControl"></app-star-rating>
Best Practices
- Use ngModel for simple forms
- Use Reactive Forms for complex forms
- Use model() for signal-based custom two-way bindings (modern approach)
- For non-signal components, follow naming convention:
value+valueChange - Prefer immutable updates for objects with
model.update() - Use
ControlValueAccessorfor reusable form controls that work with ngModel/FormControl - Always use immutable patterns, not mutations
- Use
computed()for derived values in transformations