Two-way Binding
Basic Two-way Binding
Section intitulée « 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
Section intitulée « How it Works »<!-- Two-way binding --><input [(ngModel)]="name">
<!-- Équivalent à : --><input [ngModel]="name" (ngModelChange)="name = $event">Custom Two-way Binding (Signal-based with model())
Section intitulée « 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
Section intitulée « 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
Section intitulée « 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
Section intitulée « 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)
Section intitulée « 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() pour le binding bidirectionnel avec le 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
Section intitulée « 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
Section intitulée « Checkbox Two-way Binding »@Component({ template: ` <input type="checkbox" [(ngModel)]="isChecked"> <p>{{ isChecked ? 'Checked' : 'Unchecked' }}</p> `})export class CheckboxComponent { isChecked = false;}Radio Buttons
Section intitulée « 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
Section intitulée « 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)
Section intitulée « 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>Bonnes Pratiques
Section intitulée « Bonnes Pratiques »- Utiliser ngModel pour les formulaires simples
- Utiliser Reactive Forms pour les formulaires complexes
- Utiliser model() pour les bindings bidirectionnels personnalisés basés sur les signaux (approche moderne)
- Pour les composants non-signal, suivre la convention de nommage :
value+valueChange - Préférer les mises à jour immutables pour les objets avec
model.update() - Utiliser
ControlValueAccessorpour les contrôles de formulaire réutilisables qui fonctionnent avec ngModel/FormControl - Toujours utiliser des patterns immutables, pas de mutations
- Utiliser
computed()pour les valeurs dérivées dans les transformations

