Aller au contenu principal

Lab 17 : Signal Forms

📖 Ressources​

🚀 Code de départ​

step-15-album-wholesale-v21-post-signal-computed

Continuez depuis le résultat du Lab 16. Si vous avez besoin d'une copie fraîche, utilisez step-15-album-wholesale-v21-post-signal-computed comme base et complétez d'abord les Labs 15 et 16.

Dans ce lab, vous adopterez la nouvelle API de formulaires basée sur les signaux d'Angular. Vous créerez des formulaires avec la fonction form() adossée à un signal accessible en écriture, lierez les inputs avec la directive [field], implémenterez des validateurs de manière déclarative et sauvegarderez en lisant directement depuis le signal.

Note : Signal Forms est expérimental dans Angular 21. Ajoutez "skipLibCheck": true à votre tsconfig.json pour éviter un problème connu de définitions de types dans @angular/forms/signals.

📝 Instructions​

Étape 1 : Activer skipLibCheck​

Dans tsconfig.json, ajoutez "skipLibCheck": true dans compilerOptions :

{
"compilerOptions": {
"skipLibCheck": true
}
}

Étape 2 : Créer un Signal Form​

form() requiert un signal accessible en écriture comme modèle. Le signal est la source de vérité — la directive [field] le met à jour directement au fur et à mesure que l'utilisateur saisit.

import { Component, inject, signal } from '@angular/core';
import { form, required, min, validate, Field } from '@angular/forms/signals';
import { Album } from '../model/album.model';

function prixTerminePar9(value: number) {
const dernierChiffre = value.toString().slice(-1);
return dernierChiffre !== '9'
? { kind: 'prixTerminantPar9' as const, message: 'Le prix doit se terminer par 9' }
: null;
}

@Component({
selector: 'app-album-add',
templateUrl: './album-add.component.html',
standalone: true,
imports: [Field],
})
export class AlbumAddComponent {
// Le signal est le modèle — form() l'enveloppe avec la validation
private albumModel = signal<Album>({ id: 0, name: '', artist: '', description: '', price: 0, tags: [] });

albumForm = form(this.albumModel, (path) => {
required(path.name);
required(path.artist);
min(path.price, 0);
validate(path.price, (ctx) => prixTerminePar9(ctx.value()));
});

saveAlbum() {
// Lire la valeur actuelle directement depuis le signal — pas besoin de .getRawValue()
const album = this.albumModel();
inject(AlbumService).save(album).subscribe(...);
}
}

Étape 3 : Utiliser la directive [field]​

Liez chaque input Ă  son champ correspondant avec [field] :

<mat-form-field>
<mat-label>Nom</mat-label>
<input matInput [field]="albumForm.name">
@for (error of albumForm.name().errors(); track error.kind) {
@if (error.kind === 'required') {
<mat-error>Le nom est obligatoire</mat-error>
}
}
</mat-form-field>

<mat-form-field>
<mat-label>Artiste</mat-label>
<input matInput [field]="albumForm.artist">
</mat-form-field>

<mat-form-field>
<mat-label>Prix</mat-label>
<input matInput type="number" [field]="albumForm.price">
@for (error of albumForm.price().errors(); track error.kind) {
@if (error.kind === 'min') {
<mat-error>Le prix doit être supérieur ou égal à 0</mat-error>
}
@if (error.kind === 'prixTerminantPar9') {
<mat-error>Le prix doit se terminer par 9</mat-error>
}
}
</mat-form-field>

<button mat-raised-button (click)="saveAlbum()">Enregistrer</button>

Étape 4 : Validateur personnalisé​

Un validateur personnalisé est une fonction qui reçoit la valeur courante et retourne un objet d'erreur ou null :

function prixTerminePar9(value: number) {
const dernierChiffre = value.toString().slice(-1);
return dernierChiffre !== '9'
? { kind: 'prixTerminantPar9' as const, message: 'Le prix doit se terminer par 9' }
: null;
}

// Appliquer avec validate() :
validate(path.price, (ctx) => prixTerminePar9(ctx.value()));

La propriété kind est utilisée dans le template @if (error.kind === 'prixTerminantPar9') pour afficher le bon message d'erreur.