Appearance
Utilser model() pour du double data-binding
Le double data-binding est un concept puissant qui permet de synchroniser automatiquement des données entre différents composants. Voyons comment l'implémenter efficacement avec les nouvelles fonctionnalités d'Angular.
Avec Angular, nous avons maintenant une façon plus moderne de gérer le double data-binding grâce aux model inputs.
Utiliser les composants avec des signaux
Depuis Angular 17.x, les signaux sont introduits en tant que réactifs comme alternative aux décorateurs @Input()
, @Output()
, etc.
Donc, pour utiliser input()
, output()
, model()
, contentChild()
, contentChildren()
, viewChild()
et viewChildren()
, il est essentiel de comprendre la notion des signaux.
Lisez l'article sur les signaux pour en savoir plus.
Voici un exemple simple avec un composant de gestion d'utilisateur :
ts
import { Component, model } from '@angular/core';
import { User } from './user.interface';
@Component({
selector: 'app-user-form',
standalone: true,
template: `
<div>
<input
[value]="username()"
(input)="username.set($event.target.value)"
>
<button (click)="updateUsername()">Mettre à jour</button>
</div>
`
})
export class UserFormComponent {
username = model(''); // Création d'un model input
updateUsername() {
this.username.update(value => value.toUpperCase());
}
}
ts
import { Component, signal } from '@angular/core';
import { UserFormComponent } from './user-form.component';
@Component({
selector: 'app-root',
standalone: true,
imports: [UserFormComponent],
template: `
<div>
<h2>Nom d'utilisateur: {{ username() }}</h2>
<app-user-form [(username)]="username" />
</div>
`
})
export class AppComponent {
username = signal('');
}
Le model()
fournit :
- Une fonction getter
username()
pour lire la valeur - Une méthode
set()
pour modifier la valeur - Une méthode
update()
pour transformer la valeur (comme dansupdateUsername()
)
La syntaxe [(username)]
(banana-in-a-box) permet de :
- Passer la valeur du signal au composant enfant
- Recevoir automatiquement les mises à jour quand la valeur change dans l'enfant
SYNTAXE BANANA-IN-A-BOX
La syntaxe [(volume)]
est souvent appelée "banana-in-a-box" en raison de sa ressemblance avec une banane dans une boîte. C'est un raccourci pour combiner la liaison de propriété [volume]
et l'événement (volumeChange)
.
Quand utiliser model() ?
model()
est idéal quand :
- Vous avez besoin d'un double data-binding simple
- Vous voulez éviter d'écrire manuellement
@Input()
et@Output()
- Vous travaillez avec des formulaires ou des composants interactifs
Événements de changement implicites
Lorsque vous déclarez un model input, Angular crée automatiquement un événement de sortie correspondant. Le nom de cet événement est le nom du model input suivi de "Change".
Voici un exemple avec une case à cocher personnalisée :
ts
import { Component, model } from '@angular/core';
@Component({
selector: 'app-custom-checkbox',
standalone: true,
template: `
<div
class="checkbox"
[class.checked]="checked()"
(click)="toggle()"
>
<span>{{ checked() ? '✓' : '' }}</span>
</div>
`
})
export class CustomCheckboxComponent {
// Crée automatiquement un événement checkedChange
checked = model(false);
toggle() {
this.checked.update(value => !value);
}
}
ts
import { Component, signal } from '@angular/core';
import { CustomCheckboxComponent } from './custom-checkbox.component';
@Component({
selector: 'app-user-settings',
standalone: true,
imports: [CustomCheckboxComponent],
template: `
<div>
<h3>Paramètres utilisateur</h3>
<app-custom-checkbox
[checked]="receiveNotifications()"
(checkedChange)="receiveNotifications.set($event)"
/>
<span>Recevoir les notifications</span>
</div>
`
})
export class UserSettingsComponent {
receiveNotifications = signal(false);
}
AVANTAGE
L'utilisation de model()
réduit considérablement le code boilerplate par rapport à l'approche traditionnelle avec @Input()
et @Output()
. Plus besoin de déclarer explicitement l'EventEmitter !
Transformation des valeurs
Si vous avez besoin de transformer les valeurs avant qu'elles ne soient mises à jour, vous pouvez utiliser la méthode update
du model input :
ts
import { Component, model } from '@angular/core';
@Component({
selector: 'app-user-input',
standalone: true,
template: `
<input
[value]="username()"
(input)="formatUsername($event)"
>
`
})
export class UserInputComponent {
username = model('');
formatUsername(event: Event) {
const value = (event.target as HTMLInputElement).value;
// Transformation : suppression des espaces et mise en minuscules
this.username.update(() => value.trim().toLowerCase());
}
}
Utiliser les décorateurs @Input et @Output
Avant Angular 17
Les décorateurs @Input()
et @Output()
sont la façon traditionnelle de définir des inputs et outputs dans Angular avant la version 17.
Le double data-binding consiste à mettre à jour une variable peu importe où l'information a été modifiée.
Par exemple, si nous avons le nom de l'utilisateur affiché sur les deux composants A et B. Si on modifie le nom sur A, il le sera aussi sur B et réciproquement.
En fait, nous avons une entrée et une sortie.
ts
import { Component, EventEmitter, Input, Output } from '@angular/core';
@Component({
selector: 'app-count',
standalone: true,
template: `
<p>{{n}}</p>
<button (click)="up()">Up Counter</button>
`
})
export class CountComponent {
@Input() n = 0;
@Output() nChange: EventEmitter<number> = new EventEmitter();
up() {
this.n++;
this.nChange.emit(this.n);
}
}
Le composant du compteur ne change pas vraiment. Nous avons juste ajouté le décorateur Input
pour récupérer la valeur en entrée.
Lorsque vous faites du data-binding, la propriété de sortie est le même nom que la propriété d'entrée suffixée de Change
. Par exemple, ici :
- Propriété d'entrée :
n
- Propriété de sortie :
nChange
Pourquoi ? Car nous n'indiquerons pas l'attribut nChange
dans le template. Cela se fera automatiquement par Angular. Voyez sur le composant parent :
ts
import { Component } from '@angular/core';
import { CountComponent } from './count.component';
@Component({
selector: 'app',
imports: [CountComponent],
template: `
<div>
<p>{{val}}</p>
<button (click)="up()">Up Parent</button>
<app-count [(n)]="val"></app-count>
</div>
`
})
export class AppComponent {
val: number = 0;
up() {
this.val++;
}
}
Remarquez que nous retrouvons le bouton qui incrémente la propriété val
. Cette dernière est propre au composant AppComponent
. Regardez que nous faisons du data-binding en combinant l'entrée et la sortie : [(n)]
. Ainsi, si vous cliquez sur l'un des boutons, la valeur se synchronise.
Cela revient à faire :
html
<app-count (n)="val" [nChange]="up()"></app-count>