Cours Angular · Module 8: Formulaires
Les formulaires typés (Angular 14+)
Dans cette leçon2
Les formulaires typés offrent une sécurité de type complète - comme les traits précis d'un maître calligraphe !
Définir les types
1import { FormControl, FormGroup, NonNullableFormBuilder } from '@angular/forms';
2
3// Interface du formulaire
4interface SamuraiForm {
5 name: FormControl<string>;
6 level: FormControl<number>;
7 clan: FormControl<string>;
8 skills: FormControl<string[]>;
9}
10
11@Component({...})
12export class TypedFormComponent {
13 // NonNullableFormBuilder - les contrôles ne peuvent pas être null
14 private fb = inject(NonNullableFormBuilder);
15
16 samuraiForm: FormGroup<SamuraiForm> = this.fb.group({
17 name: ['', Validators.required],
18 level: [1, Validators.min(1)],
19 clan: ['Ronin'],
20 skills: [['Kendo']]
21 });
22
23 onSubmit(): void {
24 // la valeur est entièrement typée !
25 const formValue = this.samuraiForm.getRawValue();
26 // Type : { name: string, level: number, clan: string, skills: string[] }
27
28 console.log(formValue.name); // string, pas string | null
29 }
30}FormControl vs FormGroup vs FormArray
1import { FormControl, FormGroup, FormArray } from '@angular/forms';
2
3// FormControl - valeur unique
4const nameControl = new FormControl<string>('');
5
6// FormGroup - objet avec des champs
7const samuraiGroup = new FormGroup({
8 name: new FormControl(''),
9 level: new FormControl(1)
10});
11
12// FormArray - tableau de contrôles
13const skillsArray = new FormArray([
14 new FormControl('Kendo'),
15 new FormControl('Iaido')
16]);
17
18// Accès aux valeurs
19console.log(nameControl.value); // string
20console.log(samuraiGroup.value); // { name: string, level: number }
21console.log(skillsArray.value); // string[]Code de cette leçon : src/app/app.component.ts
1import { Component, signal } from '@angular/core';
2import { CommonModule } from '@angular/common';
3import { ReactiveFormsModule, FormBuilder, FormGroup, Validators, AbstractControl, ValidationErrors, AsyncValidatorFn } from '@angular/forms';
4import { Observable, of, delay, map } from 'rxjs';
5
6@Component({
7 selector: 'app-root',
8 standalone: true,
9 imports: [CommonModule, ReactiveFormsModule],
10 template: `
11 <div class="registration-dojo">
12 <h1>Inscription à l’Académie des samouraïs</h1>
13
14 <form [formGroup]="registrationForm" (ngSubmit)="onSubmit()">
15
16 <div class="form-field">
17 <label>Nom du guerrier *</label>
18 <input formControlName="username" placeholder="Nom unique...">
19
20 @if (registrationForm.get('username')?.pending) {
21 <span class="checking">Vérification de la disponibilité...</span>
22 }
23
24 @if (registrationForm.get('username')?.touched && registrationForm.get('username')?.errors) {
25 <div class="error-box">
26 @if (registrationForm.get('username')?.errors?.['required']) {
27 <span>Le nom est obligatoire</span>
28 }
29 @if (registrationForm.get('username')?.errors?.['minlength']) {
30 <span>4 caractères minimum</span>
31 }
32 @if (registrationForm.get('username')?.errors?.['usernameTaken']) {
33 <span>Ce nom est déjà pris !</span>
34 }
35 </div>
36 }
37
38 @if (registrationForm.get('username')?.valid) {
39 <span class="valid-mark">Nom disponible</span>
40 }
41 </div>
42
43 <div class="form-field">
44 <label>E-mail *</label>
45 <input type="email" formControlName="email" placeholder="ton@email.com">
46
47 @if (registrationForm.get('email')?.touched && registrationForm.get('email')?.errors) {
48 <div class="error-box">
49 @if (registrationForm.get('email')?.errors?.['required']) {
50 <span>L’e-mail est obligatoire</span>
51 }
52 @if (registrationForm.get('email')?.errors?.['email']) {
53 <span>Format d’e-mail invalide</span>
54 }
55 </div>
56 }
57 </div>
58
59 <div class="form-field" formGroupName="passwords">
60 <label>Mot de passe *</label>
61 <input type="password" formControlName="password" placeholder="Mot de passe robuste...">
62
63 @if (registrationForm.get('passwords.password')?.touched) {
64 <div class="password-strength">
65 <div class="strength-bar" [style.width.%]="passwordStrength()">
66 </div>
67 <span>Robustesse : {{ passwordStrengthLabel() }}</span>
68 </div>
69 }
70
71 <label>Répète le mot de passe *</label>
72 <input type="password" formControlName="confirmPassword" placeholder="Répète le mot de passe...">
73
74 @if (registrationForm.get('passwords')?.errors?.['passwordMismatch'] &&
75 registrationForm.get('passwords.confirmPassword')?.touched) {
76 <div class="error-box">
77 <span>Les mots de passe ne correspondent pas !</span>
78 </div>
79 }
80 </div>
81
82 <div class="form-field">
83 <label class="checkbox-label">
84 <input type="checkbox" formControlName="acceptTerms">
85 J’accepte le code du Bushido (conditions d’utilisation) *
86 </label>
87
88 @if (registrationForm.get('acceptTerms')?.touched &&
89 registrationForm.get('acceptTerms')?.errors?.['requiredTrue']) {
90 <div class="error-box">
91 <span>Tu dois accepter les conditions d’utilisation</span>
92 </div>
93 }
94 </div>
95
96 <div class="form-summary">
97 <h3>État du formulaire :</h3>
98 <ul>
99 <li [class.valid]="registrationForm.get('username')?.valid">
100 Nom : {{ registrationForm.get('username')?.valid ? 'OK' : 'X' }}
101 </li>
102 <li [class.valid]="registrationForm.get('email')?.valid">
103 E-mail : {{ registrationForm.get('email')?.valid ? 'OK' : 'X' }}
104 </li>
105 <li [class.valid]="registrationForm.get('passwords')?.valid">
106 Mots de passe : {{ registrationForm.get('passwords')?.valid ? 'OK' : 'X' }}
107 </li>
108 <li [class.valid]="registrationForm.get('acceptTerms')?.valid">
109 Conditions : {{ registrationForm.get('acceptTerms')?.valid ? 'OK' : 'X' }}
110 </li>
111 </ul>
112 </div>
113
114 <button type="submit" [disabled]="registrationForm.invalid || registrationForm.pending">
115 {{ registrationForm.pending ? 'Vérification...' : 'Rejoindre l’Académie' }}
116 </button>
117 </form>
118
119 @if (registered()) {
120 <div class="success-message">
121 <h2>Bienvenue à l’Académie, {{ registrationForm.get('username')?.value }} !</h2>
122 <p>Ton aventure de samouraï commence !</p>
123 </div>
124 }
125 </div>
126 `,
127 styles: [`
128 .registration-dojo {
129 font-family: 'Segoe UI', sans-serif;
130 padding: 20px;
131 background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
132 min-height: 100vh;
133 color: #eee;
134 }
135
136 h1 {
137 text-align: center;
138 color: #e94560;
139 }
140
141 form {
142 max-width: 500px;
143 margin: 0 auto;
144 background: rgba(255,255,255,0.1);
145 padding: 30px;
146 border-radius: 15px;
147 }
148
149 .form-field {
150 margin-bottom: 20px;
151 }
152
153 .form-field label {
154 display: block;
155 margin-bottom: 8px;
156 color: #e94560;
157 font-weight: bold;
158 }
159
160 .form-field input[type="text"],
161 .form-field input[type="email"],
162 .form-field input[type="password"] {
163 width: 100%;
164 padding: 12px;
165 border: 2px solid #0f3460;
166 border-radius: 8px;
167 background: #0f3460;
168 color: white;
169 font-size: 16px;
170 box-sizing: border-box;
171 margin-bottom: 5px;
172 }
173
174 .form-field input:focus {
175 border-color: #e94560;
176 outline: none;
177 }
178
179 .form-field input.ng-valid.ng-touched {
180 border-color: #28a745;
181 }
182
183 .form-field input.ng-invalid.ng-touched {
184 border-color: #dc3545;
185 }
186
187 .checking {
188 color: #ffc107;
189 font-size: 0.9em;
190 }
191
192 .valid-mark {
193 color: #28a745;
194 font-size: 0.9em;
195 }
196
197 .error-box {
198 background: rgba(220, 53, 69, 0.2);
199 border: 1px solid #dc3545;
200 border-radius: 5px;
201 padding: 8px;
202 margin-top: 5px;
203 }
204
205 .error-box span {
206 display: block;
207 color: #fca5a5;
208 font-size: 0.85em;
209 }
210
211 .password-strength {
212 margin: 10px 0;
213 }
214
215 .strength-bar {
216 height: 8px;
217 background: linear-gradient(90deg, #dc3545 0%, #ffc107 50%, #28a745 100%);
218 border-radius: 4px;
219 transition: width 0.3s;
220 }
221
222 .checkbox-label {
223 display: flex;
224 align-items: center;
225 gap: 10px;
226 cursor: pointer;
227 }
228
229 .checkbox-label input {
230 width: 20px;
231 height: 20px;
232 }
233
234 .form-summary {
235 background: rgba(0,0,0,0.3);
236 padding: 15px;
237 border-radius: 8px;
238 margin: 20px 0;
239 }
240
241 .form-summary h3 {
242 margin-top: 0;
243 color: #e94560;
244 }
245
246 .form-summary ul {
247 list-style: none;
248 padding: 0;
249 }
250
251 .form-summary li {
252 padding: 5px 0;
253 color: #888;
254 }
255
256 .form-summary li.valid {
257 color: #28a745;
258 }
259
260 button[type="submit"] {
261 width: 100%;
262 padding: 15px;
263 background: #e94560;
264 color: white;
265 border: none;
266 border-radius: 8px;
267 font-size: 18px;
268 cursor: pointer;
269 transition: background 0.3s;
270 }
271
272 button[type="submit"]:hover:not(:disabled) {
273 background: #ff6b6b;
274 }
275
276 button[type="submit"]:disabled {
277 opacity: 0.5;
278 cursor: not-allowed;
279 }
280
281 .success-message {
282 text-align: center;
283 background: rgba(40, 167, 69, 0.2);
284 border: 2px solid #28a745;
285 border-radius: 15px;
286 padding: 30px;
287 margin-top: 20px;
288 max-width: 500px;
289 margin-left: auto;
290 margin-right: auto;
291 }
292
293 .success-message h2 {
294 color: #28a745;
295 }
296 `]
297})
298export class AppComponent {
299 registrationForm: FormGroup;
300 registered = signal(false);
301
302 // Simulation des noms déjà pris
303 private takenUsernames = ['musashi', 'kojiro', 'yagyu', 'admin', 'samurai'];
304
305 constructor(private fb: FormBuilder) {
306 this.registrationForm = this.fb.group({
307 username: ['',
308 [Validators.required, Validators.minLength(4)],
309 [this.usernameAvailableValidator()]
310 ],
311 email: ['', [Validators.required, Validators.email]],
312 passwords: this.fb.group({
313 password: ['', [Validators.required, Validators.minLength(8)]],
314 confirmPassword: ['', Validators.required]
315 }, { validators: this.passwordMatchValidator }),
316 acceptTerms: [false, Validators.requiredTrue]
317 });
318 }
319
320 // Validateur asynchrone - vérifie si le nom est disponible
321 usernameAvailableValidator(): AsyncValidatorFn {
322 return (control: AbstractControl): Observable<ValidationErrors | null> => {
323 if (!control.value) {
324 return of(null);
325 }
326
327 // Simulation d’une requête API
328 return of(control.value).pipe(
329 delay(1000),
330 map(username => {
331 const isTaken = this.takenUsernames.includes(username.toLowerCase());
332 return isTaken ? { usernameTaken: true } : null;
333 })
334 );
335 };
336 }
337
338 // Validateur inter-champs
339 passwordMatchValidator(control: AbstractControl): ValidationErrors | null {
340 const password = control.get('password')?.value;
341 const confirmPassword = control.get('confirmPassword')?.value;
342
343 if (password && confirmPassword && password !== confirmPassword) {
344 return { passwordMismatch: true };
345 }
346 return null;
347 }
348
349 passwordStrength(): number {
350 const password = this.registrationForm.get('passwords.password')?.value || '';
351 let strength = 0;
352
353 if (password.length >= 8) strength += 25;
354 if (password.length >= 12) strength += 15;
355 if (/[A-Z]/.test(password)) strength += 20;
356 if (/[a-z]/.test(password)) strength += 10;
357 if (/[0-9]/.test(password)) strength += 15;
358 if (/[^A-Za-z0-9]/.test(password)) strength += 15;
359
360 return Math.min(strength, 100);
361 }
362
363 passwordStrengthLabel(): string {
364 const strength = this.passwordStrength();
365 if (strength < 30) return 'Faible';
366 if (strength < 60) return 'Moyenne';
367 if (strength < 80) return 'Bonne';
368 return 'Très élevée';
369 }
370
371 onSubmit(): void {
372 if (this.registrationForm.valid) {
373 this.registered.set(true);
374 }
375 }
376}Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Que fournissent les Typed Forms (Angular 14+) ?
2. Que garantit l'utilisation de NonNullableFormBuilder ?
Voici 2 des 6 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Tri vertical
Ordonne les blocs de construction d'un formulaire du plus petit au plus grand (d'une seule valeur à un tableau de groupes).
- Éditeur de code
Complète le template qui affiche les erreurs de validation seulement quand le champ a été touché.
- Clique dans l'ordre
Construis la définition du champ avec des validateurs : ['', [Validators.required, Validators.email]]. Clique sur les éléments dans l'ordre.