@dsivd/prestations-ng 19.0.8 → 19.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1223 @@
1
+ # Utilisation technique (DEV)
2
+
3
+ > Ce guide décrit les bonnes pratiques pour développer une prestation avec `prestations-ng`.
4
+ > Il a été mis à jour pour la version **v19+** : composants **standalone**, **signals** et
5
+ > fonction **`inject`** (plus de modules, plus d'injection par constructeur).
6
+ >
7
+ > 📚 Pour aller plus loin :
8
+ > [ANGULAR_SIGNALS.md](ANGULAR_SIGNALS.md) · [COMPONENTS.md](COMPONENTS.md) · [UPGRADING_V19.md](UPGRADING_V19.md)
9
+
10
+ ## Sommaire
11
+
12
+ - [Documentation](good-practices#documentation)
13
+ - [Mise à jour de la librairie](good-practices#mise___jour_de_la_librairie)
14
+ - [Accessibilité](good-practices#accessibilit_)
15
+ - [Structure HTML](good-practices#structure_html)
16
+ - [Le focus](good-practices#le_focus)
17
+ - [La balise `<label>`](good-practices#la_balise__code__lt_label_gt___code_)
18
+ - [La balise `<ul>` ou `<ol>`](good-practices#la_balise__code__lt_ul_gt___code__ou__code__lt_ol_gt___code_)
19
+ - [La balise `<fieldset>` et `<legend>`](good-practices#la_balise__code__lt_fieldset_gt___code__et__code__lt_legend_gt___code_)
20
+ - [Liseuse d'écran (afficher / masquer)](good-practices#liseuse_d__cran__afficher___masquer_)
21
+ - [Une page prestations-ng](good-practices#une_page_prestations-ng)
22
+ - [Le routage de vos pages](good-practices#le_routage_de_vos_pages)
23
+ - [Formulaire simple](good-practices#formulaire_simple)
24
+ - [Formulaire complexe (sous-routes)](good-practices#formulaire_complexe__sous-routes_)
25
+ - [Création d'un composant réutilisable](good-practices#cr_ation_d_un_composant_r_utilisable)
26
+ - [Exemple avec foehn-input-date de prestations-ng](good-practices#exemple_avec_foehn-input-date_de_prestations-ng)
27
+ - [Afficher la validation globale du composant](good-practices#afficher_la_validation_globale_du_composant)
28
+ - [Éviter des problèmes d'affichage d'erreurs dans une boucle `@for`](good-practices#_viter_des_probl_mes_d_affichage_d_erreurs_dans_une_boucle__code__for__code_)
29
+ - [Validation du formulaire](good-practices#validation_du_formulaire)
30
+ - [Gestion standard](good-practices#gestion_standard)
31
+ - [Gestion manuelle](good-practices#gestion_manuelle)
32
+ - [Back-end](good-practices#back-end)
33
+ - [Front-end](good-practices#front-end)
34
+ - [Validation manuelle d'un composant](good-practices#validation_manuelle_d_un_composant)
35
+ - [Cacher une erreur](good-practices#cacher_une_erreur)
36
+ - [Afficher une erreur](good-practices#afficher_une_erreur)
37
+
38
+ > ⚠️ Dans toutes vos pages HTML vous devez avoir un `<foehn-form>` et celui-ci **ne peut pas être
39
+ > conditionnel** (pas de `@if` autour de `<foehn-form>`).
40
+
41
+ ## Documentation
42
+
43
+ - [PrestaKit](https://www.vd.ch/toutes-les-autorites/departements/departement-des-infrastructures-et-des-ressources-humaines-dirh/direction-generale-du-numerique-et-des-systemes-dinformation-dgnsi/prestakit-sdk-pour-le-developpement-des-prestations-en-ligne/)
44
+ - [prestations-ng](https://dsi-vd.github.io/prestations-ng/)
45
+ - [prestations-ng (doc autogénérée)](https://dsi-vd.github.io/prestations-ng/generateddoc/)
46
+ - [foehn](https://dsi-vd.github.io/foehn-design-system/components/detail/formulaire-prestation--default.html) (Charte graphique)
47
+ - [CSS](https://www.w3schools.com/css/default.asp)
48
+ - [Bootstrap](https://getbootstrap.com/docs/5.3/layout/grid/)
49
+ - [Flex - Bootstrap](https://getbootstrap.com/docs/5.3/utilities/flex/)
50
+ - [Flex](https://css-tricks.com/snippets/css/a-guide-to-flexbox/)
51
+ - [HTML](https://www.w3schools.com/html/default.asp)
52
+ - [Éléments de section](https://www.alsacreations.com/article/lire/1376-html5-section-article-nav-header-footer-aside.html)
53
+ - [Description list](https://www.w3schools.com/tags/tag_dl.asp)
54
+ - [RXJS](https://rxjs-dev.firebaseapp.com/)
55
+
56
+ ## Mise à jour de la librairie
57
+
58
+ > ℹ️ Maintenez à jour vos librairies en vous inscrivant sur la page [PrestaKit](https://www.vd.ch/toutes-les-autorites/departements/departement-des-infrastructures-et-des-ressources-humaines-dirh/direction-generale-du-numerique-et-des-systemes-dinformation-dgnsi/prestakit-sdk-pour-le-developpement-des-prestations-en-ligne/)
59
+ > et vous serez ainsi notifié des nouvelles releases. Cela vous permettra de profiter des corrections
60
+ > de bug, améliorations et nouvelles fonctionnalités.
61
+ >
62
+ > Prenez le temps de lire le [changelog](CHANGELOG.md) de **prestations-ng** à chaque nouvelle release.
63
+ >
64
+ > N'hésitez pas à ouvrir des tickets JIRA (PRESTAKIT-xxx) dans [PrestaKit (anc SDK Cyber)](https://issuetracker.etat-de-vaud.ch/outils/issuetracker/browse/PRESTAKIT)
65
+ > pour signaler des bugs ou même à faire des propositions d'améliorations afin de
66
+ > [contribuer](CONTRIBUTING.md) au développement de prestations-ng.
67
+
68
+ Mettre à jour la librairie prestations-ng (npm) :
69
+
70
+ ```bash
71
+ ng update @dsivd/prestations-ng
72
+ ```
73
+
74
+ ## Accessibilité
75
+
76
+ > ℹ️ En développant des prestations pour l'État, nous devons faire tout notre possible pour rendre nos
77
+ > pages web les plus accessibles possibles au grand public.
78
+ >
79
+ > Prestations-ng fait une bonne partie du boulot pour nous mais c'est au développeur de prendre le
80
+ > soin de mettre les efforts qu'il faut pour que la prestation reste accessible.
81
+
82
+ ### Structure HTML
83
+
84
+ > ⚠️ Nous recommandons :
85
+ >
86
+ > - une seule balise h1 (`<h1>`)
87
+ > - respecter la hiérarchie h1, h2, h3..
88
+ > - regrouper vos informations dans des sections (`<section>`) si vous avez plusieurs h2 (`<h2>`)
89
+ > - utiliser la classe CSS `col-md-8` de Bootstrap pour définir la largeur de votre formulaire
90
+ > - organisez votre texte avec des paragraphes (`<p>`)
91
+ > - utiliser les balises `<dl>`, `<dt>` et `<dd>` pour afficher des clés / valeurs (voir [foehn-recap-section](https://dsi-vd.github.io/prestations-ng/recap))
92
+ > - utiliser [foehn-abbr](https://dsi-vd.github.io/prestations-ng/misc) pour les abréviations tant que possible et sinon la balise abbr (`<abbr>`)
93
+
94
+ Ce qu'il ne faut **pas** faire :
95
+
96
+ ```html
97
+ <h1>...</h1>
98
+ <h3>...</h3>
99
+ <h2>...</h2>
100
+ <h6>...</h6>
101
+ ...
102
+ ...
103
+ ...
104
+ ```
105
+
106
+ Ce qu'il **faut** faire :
107
+
108
+ ```html
109
+ <h1>...</h1>
110
+ <section>
111
+ <h2>...</h2>
112
+ <h3>...</h3>
113
+ <h4>...</h4>
114
+ ...
115
+ ...
116
+ </section>
117
+ <h2>...</h2>
118
+ ...
119
+ <section>
120
+ ...
121
+ ```
122
+
123
+ Quelques attributs HTML utiles :
124
+
125
+ - Pour les images (`<img>`)
126
+ - `title="[texte explicite ici]"`
127
+ - `alt="[texte explicite ici]"`
128
+ - Pour les liens (`<a>`)
129
+ - `title="[texte explicite ici]"`
130
+ - `target="_blank"`
131
+ - [Rendre les balises accessibles](https://www.w3.org/TR/wai-aria-1.2/#intro_ria_accessibility)
132
+ - [`aria-*`](https://w3c.github.io/using-aria/#aria-states-and-properties-aria-attributes)
133
+ - [`role`](https://www.w3.org/WAI/PF/HTML/wiki/RoleAttribute)
134
+
135
+ ### Le focus
136
+
137
+ > ⚠️ Il est important que le focus dans une page HTML soit maîtrisé. Si le focus est perdu, alors la
138
+ > liseuse d'écran le sera aussi.
139
+ >
140
+ > Si, par exemple, on ouvre une modale en cliquant sur un bouton, alors à la fermeture de la modale,
141
+ > le focus doit être remis sur le bouton déclencheur.
142
+
143
+ Exemple :
144
+
145
+ ```html
146
+ <button
147
+ type="button"
148
+ class="btn btn-secondary"
149
+ (click)="openModal()"
150
+ #modalTrigger
151
+ >
152
+ <!-- on déclare le déclencheur de la modale -->
153
+ Open Modal
154
+ </button>
155
+ <foehn-modal
156
+ modalHeaderText="Modal dialog"
157
+ modalSize="modal-lg"
158
+ [isModalVisible]="isModalVisible"
159
+ (isModalVisibleChange)="updateVisibilityStatus($event)"
160
+ [modalTriggerHtmlElement]="modalTrigger"
161
+ >
162
+ <!-- on utilise le déclencheur -->
163
+ <div>Here is the dialog</div>
164
+ </foehn-modal>
165
+ ```
166
+
167
+ ### La balise `<label>`
168
+
169
+ > ⚠️ Tous nos composants `foehn-input-*` disposent d'une balise label ([`<label>`](https://developer.mozilla.org/fr/docs/Web/HTML/Element/label))
170
+ > qui décrit le champ de saisie. Il est important que celle-ci soit remplie avec un contenu explicite.
171
+ >
172
+ > Si pour une raison quelconque, le métier ne souhaite pas l'afficher, vous devez tout de même la
173
+ > renseigner et ajouter un attribut `[isLabelSrOnly]="true"`. Le label ne sera pas affiché mais
174
+ > uniquement lu par la liseuse d'écran.
175
+
176
+ Exemple :
177
+
178
+ ```html
179
+ <foehn-input-number
180
+ name="..."
181
+ [(model)]="..."
182
+ [label]="'Le texte que la liseuse va lire'"
183
+ [isLabelSrOnly]="true"
184
+ [required]="..."
185
+ [maxlength]="..."
186
+ />
187
+ ```
188
+
189
+ > 💡 `label`, `isLabelSrOnly`, `required`, ... sont désormais des **input signals** côté composant.
190
+ > Vous continuez à les alimenter depuis le template parent via un binding `[label]="..."`.
191
+ > À l'intérieur du composant, vous les lisez en les invoquant : `label()`, `required()`, ...
192
+
193
+ ### La balise `<ul>` ou `<ol>`
194
+
195
+ > ⚠️
196
+ >
197
+ > - L'élément [`<ul>`](https://developer.mozilla.org/fr/docs/Web/HTML/Element/ul) doit être utilisé
198
+ > pour regrouper plusieurs éléments qui n'ont pas de relation d'ordre. Si on hésite entre
199
+ > [`<ol>`](https://developer.mozilla.org/fr/docs/Web/HTML/Element/ol) et
200
+ > [`<ul>`](https://developer.mozilla.org/fr/docs/Web/HTML/Element/ul), on se demandera si changer
201
+ > l'ordre des éléments de la liste a un impact : si le déplacement d'un élément change la
202
+ > signification, cela signifie que l'ordre est important et qu'il faudra utiliser
203
+ > [`<ol>`](https://developer.mozilla.org/fr/docs/Web/HTML/Element/ol), sinon l'ordre n'importe pas
204
+ > et [`<ul>`](https://developer.mozilla.org/fr/docs/Web/HTML/Element/ul) peut être utilisé.
205
+ > - Si [`<ol>`](https://developer.mozilla.org/fr/docs/Web/HTML/Element/ol) ou
206
+ > [`<ul>`](https://developer.mozilla.org/fr/docs/Web/HTML/Element/ul) ne contient qu'un seul élément
207
+ > [`<li>`](https://developer.mozilla.org/fr/docs/Web/HTML/Element/li) (non dynamique), alors on se
208
+ > demandera si l'utilisation d'une liste est nécessaire et si un paragraphe
209
+ > ([`<p>`](https://developer.mozilla.org/fr/docs/Web/HTML/Element/p)) ne serait pas une meilleure
210
+ > option.
211
+ > - Ne **JAMAIS** mettre une autre balise que [`<li>`](https://developer.mozilla.org/fr/docs/Web/HTML/Element/li)
212
+ > entre deux balises [`<ol>`](https://developer.mozilla.org/fr/docs/Web/HTML/Element/ol) ou
213
+ > [`<ul>`](https://developer.mozilla.org/fr/docs/Web/HTML/Element/ul).
214
+
215
+ Exemple :
216
+
217
+ ```html
218
+ <ul>
219
+ <li>un artichaut</li>
220
+ <li>Les trucs pour le gâteau
221
+ <!-- On voit que </li> n'est pas là -->
222
+ <ul>
223
+ <li>trois œufs</li>
224
+ <li>La génoise
225
+ <!-- Là on ouvre une autre liste -->
226
+ <ul>
227
+ <li>100g de sucre</li>
228
+ <li>un œuf</li>
229
+ <li>150g de farine</li>
230
+ </ul>
231
+ </li> <!-- On ferme la liste la plus imbriquée -->
232
+ <li>200g de chocolat</li>
233
+ </ul>
234
+ <!-- On ferme la liste imbriquée avec </li> -->
235
+ </li>
236
+ <li>De l'essuie-tout</li>
237
+ <li>A faire dans l'ordre pour une belle vaisselle
238
+ <ol>
239
+ <li>Remplir le lave vaisselle</li>
240
+ <li>Mettre une pastille pour lave vaisselle</li>
241
+ <li>Fermer la porte du lave vaisselle</li>
242
+ <li>Lancer le lave vaisselle</li>
243
+ </ol>
244
+ </li>
245
+ </ul>
246
+ ```
247
+
248
+ ### La balise `<fieldset>` et `<legend>`
249
+
250
+ > ⚠️ L'élément HTML [`<fieldset>`](https://developer.mozilla.org/fr/docs/Web/HTML/Element/fieldset) est
251
+ > utilisé afin de regrouper plusieurs contrôles interactifs ainsi que des étiquettes
252
+ > ([`<label>`](https://developer.mozilla.org/fr/docs/Web/HTML/Element/label)) dans un formulaire HTML
253
+ > et on doit lui associer une légende
254
+ > ([`<legend>`](https://developer.mozilla.org/fr/docs/Web/HTML/Element/legend)) décrivant ce groupe.
255
+ >
256
+ > L'élément HTML [`<legend>`](https://developer.mozilla.org/fr/docs/Web/HTML/Element/legend) représente
257
+ > une légende pour le contenu de son élément parent
258
+ > [`<fieldset>`](https://developer.mozilla.org/fr/docs/Web/HTML/Element/fieldset). Il ne peut pas être
259
+ > utilisé tout seul.
260
+
261
+ Exemple :
262
+
263
+ ```html
264
+ <form>
265
+ <fieldset>
266
+ <legend>Choose your favorite monster</legend>
267
+
268
+ <input type="radio" id="kraken" name="monster" value="K" />
269
+ <label for="kraken">Kraken</label><br />
270
+
271
+ <input type="radio" id="sasquatch" name="monster" value="S" />
272
+ <label for="sasquatch">Sasquatch</label><br />
273
+
274
+ <input type="radio" id="mothman" name="monster" value="M" />
275
+ <label for="mothman">Mothman</label>
276
+ </fieldset>
277
+ </form>
278
+ ```
279
+
280
+ ### Liseuse d'écran (afficher / masquer)
281
+
282
+ > ⚠️ Il est important de garder en tête qu'une liseuse d'écran va parcourir notre code HTML. De ce
283
+ > fait, nous pouvons à tout moment décider de lui afficher explicitement du contenu à lire ou de lui
284
+ > masquer du contenu pour ne pas surcharger l'information.
285
+
286
+ - Afficher uniquement pour la liseuse : utiliser la classe CSS `sr-only`
287
+ - Masquer du contenu à la liseuse : utiliser l'attribut `aria-hidden="true"`
288
+
289
+ Exemple :
290
+
291
+ ```html
292
+ <a href="https://www.vd.ch">
293
+ <span class="sr-only">
294
+ <!-- Afficher uniquement pour la liseuse d'écran -->
295
+ Retour à la page d'accueil
296
+ </span>
297
+ <img
298
+ aria-hidden="true"
299
+ class="img-fluid footer-logo"
300
+ src="[url_vers_mon_image]"
301
+ alt="Canton de Vaud"
302
+ />
303
+ <!-- Masquer uniquement pour la liseuse d'écran -->
304
+ </a>
305
+ ```
306
+
307
+ Le résultat de l'exemple ci-dessus sera un lien représenté par une image pour les voyants et un lien
308
+ avec du texte pour les non-voyants.
309
+
310
+ ### Une page prestations-ng
311
+
312
+ 1. Étendre la classe abstraite `AbstractPageComponent` en spécifiant un modèle
313
+ 2. Ajouter un titre h1 en utilisant `setPageTitle`
314
+ 3. Implémenter la fonction `newForm`
315
+ 4. Ajouter le `<foehn-form>` dans la page (⚠️ **il ne doit pas être conditionnel**)
316
+ 5. La première balise d'en-tête de votre page doit être un h2 (`<h2>`)
317
+ 6. Ajouter `<foehn-navigation>`
318
+ 7. Si vous disposez de plusieurs pages, utiliser le composant `<foehn-page-counter>`
319
+
320
+ `page-one.component.ts`
321
+
322
+ ```ts
323
+ @Component({
324
+ templateUrl: './page-one.component.html',
325
+ imports: [
326
+ // chaque composant importe ce dont il a besoin dans son template
327
+ FoehnFormComponent,
328
+ FoehnNavigationComponent,
329
+ FoehnPageCounterComponent,
330
+ SdkDictionaryPipe,
331
+ // ... vos composants foehn-* utilisés dans le template
332
+ ],
333
+ })
334
+ export class PageOneComponent extends AbstractPageComponent<BusinessForm> implements OnInit {
335
+ ngOnInit(): void {
336
+ super.ngOnInit();
337
+ // point (2) : ajoutera le titre h1
338
+ this.setPageTitle(this._dictionaryService.getKeySync('page.one.title'));
339
+ }
340
+
341
+ // point (3)
342
+ newForm(): BusinessForm {
343
+ return new BusinessForm();
344
+ }
345
+ }
346
+ ```
347
+
348
+ > 💡 Plus de `constructor` : les dépendances de `AbstractPageComponent` (dont `_dictionaryService`)
349
+ > sont injectées via `inject()` dans la classe abstraite. `activatedRoute` n'est donc plus un
350
+ > paramètre de constructeur à passer.
351
+
352
+ `page-one.component.html`
353
+
354
+ ```html
355
+ <div class="container mt-5">
356
+ <foehn-page-counter />
357
+ <!-- point (7) : si besoin -->
358
+
359
+ <!-- point (4) : doit toujours être présent dans la page et pas conditionnel (pas de @if) -->
360
+ <foehn-form>
361
+ <div class="row">
362
+ <div class="col-md-8">
363
+ <!-- point (5) : toujours avoir un <h2> en premier dans votre page -->
364
+ <h2>{{ 'La_clé_du_dictionnaire_de_votre_titre_H2_ici' | fromDictionary }}</h2>
365
+
366
+ <!-- [VOS COMPOSANTS FOEHN ICI] -->
367
+
368
+ <foehn-navigation
369
+ id="navigation"
370
+ (onPrevious)="previous()"
371
+ (onNext)="send()"
372
+ />
373
+ <!-- point (6) -->
374
+ </div>
375
+ </div>
376
+ </foehn-form>
377
+ </div>
378
+ ```
379
+
380
+ ## Le routage de vos pages
381
+
382
+ ### Formulaire simple
383
+
384
+ `app.routes.ts`
385
+
386
+ ```ts
387
+ export const routes: Routes = [
388
+ {
389
+ path: '',
390
+ component: PageWrapperComponent,
391
+ data: { root: true },
392
+ children: [
393
+ { path: '', component: PageOneComponent, canActivate: [GesdemLoaderGuard] },
394
+ { path: '404', component: FoehnNotfoundComponent },
395
+ { path: 'erreur', component: GesdemErrorComponent },
396
+ {
397
+ path: ':reference',
398
+ children: [
399
+ { path: 'page-1', component: PageOneComponent, data: { order: 1 } },
400
+ // {...},
401
+ // ...
402
+ { path: 'erreur', component: GesdemErrorComponent },
403
+ { path: '404', component: FoehnNotfoundComponent },
404
+ { path: '', redirectTo: 'page-1', pathMatch: 'full' },
405
+ ],
406
+ canActivate: [GesdemLoaderGuard],
407
+ },
408
+ { path: '**', redirectTo: '/404', pathMatch: 'full' },
409
+ ],
410
+ },
411
+ ];
412
+ ```
413
+
414
+ ### Formulaire complexe (sous-routes)
415
+
416
+ > ℹ️ Préférez l'utilisation de **sous-routes** (`*.routes.ts`) pour regrouper vos routes et ainsi
417
+ > mieux structurer votre projet.
418
+ >
419
+ > Comme il n'y a plus de modules (composants **standalone**), on ne charge plus un `*.module.ts` mais
420
+ > un fichier de routes qui exporte une constante `Routes`. Voir [UPGRADING_V19.md](UPGRADING_V19.md)
421
+ > pour le détail de la migration.
422
+
423
+ `app.routes.ts`
424
+
425
+ ```ts
426
+ export const routes: Routes = [
427
+ {
428
+ path: '',
429
+ component: PageWrapperComponent,
430
+ children: [
431
+ {
432
+ path: 'welcome',
433
+ loadChildren: () =>
434
+ import('./modules/welcome/welcome.routes').then((m) => m.welcome_routes),
435
+ },
436
+ {
437
+ path: 'inscription',
438
+ loadChildren: () =>
439
+ import('./modules/inscription/inscription.routes').then(
440
+ (m) => m.inscription_routes,
441
+ ),
442
+ },
443
+ // {...},
444
+ // ...
445
+ // recovery link should redirect to /inscription/reference
446
+ { path: ':reference', redirectTo: 'inscription/:reference', pathMatch: 'full' },
447
+ { path: '', redirectTo: 'welcome', pathMatch: 'full' },
448
+ { path: '404', component: FoehnNotfoundComponent },
449
+ { path: '**', redirectTo: '/404', pathMatch: 'full' },
450
+ ],
451
+ },
452
+ ];
453
+ ```
454
+
455
+ > 💡 On charge donc `welcome.routes.ts` (et non plus `welcome.module.ts`) :
456
+ > `loadChildren: () => import('./modules/welcome/welcome.routes').then((m) => m.welcome_routes)`
457
+ > au lieu de `import('./modules/welcome/welcome.module').then((m) => m.WelcomeModule)`.
458
+
459
+ `welcome.routes.ts`
460
+
461
+ ```ts
462
+ export const welcome_routes: Routes = [
463
+ {
464
+ path: '',
465
+ children: [{ path: '', component: WelcomePageComponent }],
466
+ },
467
+ ];
468
+ ```
469
+
470
+ `inscription.routes.ts`
471
+
472
+ ```ts
473
+ export const inscription_routes: Routes = [
474
+ {
475
+ path: '',
476
+ data: { root: true },
477
+ children: [
478
+ { path: '', component: PageOneComponent, canActivate: [GesdemLoaderGuard] },
479
+ { path: 'erreur', component: GesdemErrorComponent },
480
+ {
481
+ path: ':reference',
482
+ children: [
483
+ { path: 'page-1', component: PageOneComponent, data: { order: 1 } },
484
+ // {...},
485
+ // ...
486
+ { path: 'erreur', component: GesdemErrorComponent },
487
+ { path: '404', component: FoehnNotfoundComponent },
488
+ { path: '', redirectTo: 'page-1', pathMatch: 'full' },
489
+ ],
490
+ canActivate: [GesdemLoaderGuard],
491
+ },
492
+ ],
493
+ },
494
+ ];
495
+ ```
496
+
497
+ > 💡 Vous pouvez aussi charger paresseusement (`lazy-load`) un composant :
498
+ >
499
+ > ```ts
500
+ > { path: 'list', loadComponent: () => import('./demande-list-page/demande-list-page.component').then((m) => m.DemandeListPageComponent) },
501
+ > ```
502
+
503
+ ## Création d'un composant réutilisable
504
+
505
+ > ℹ️ Créer un composant réutilisable en Angular n'est pas difficile mais pour qu'il soit adapté à
506
+ > prestations-ng, c'est une autre affaire. Il va falloir respecter certaines règles, sans quoi, ça ne
507
+ > fonctionnera pas correctement.
508
+
509
+ ### Exemple avec foehn-input-date de prestations-ng
510
+
511
+ Votre composant doit :
512
+
513
+ 1. `selector` devrait commencer par `"app-..."` pour éviter d'éventuels conflits de nommage
514
+ 2. avoir un `providers`
515
+ 3. étendre `FoehnInputComponent` et vous devez lui préciser votre modèle de données
516
+ 4. répartir les attributs de votre modèle de données dans des variables publiques utilisées dans
517
+ votre HTML
518
+ 5. implémenter une fonction `update` (ici ce sera `updateDate`) qui fera appel à `updateNgModel` pour
519
+ maintenir la synchronisation de votre modèle de données avec votre page
520
+ 6. implémenter une fonction `handleUserInput` qui fera appel à `handleChange` pour continuer à
521
+ notifier le parent des changements de comportement de votre composant
522
+ 7. implémenter une fonction `onModelChange` qui vous permettra d'initialiser correctement les
523
+ variables publiques correspondant à votre modèle
524
+ 8. implémenter une fonction `getValidValue` afin de reconstruire votre modèle de données avant de le
525
+ retransmettre à votre page et retourner `null` si tous les attributs de votre objet sont vides (ça
526
+ c'est pour que Java soit content)
527
+
528
+ > 🚨 Points **standalone / signals** à respecter :
529
+ >
530
+ > - **Pas de `multi: true`** dans le `providers` (retiré depuis la migration standalone).
531
+ > - Les `imports` du composant se déclarent **directement** dans le décorateur `@Component`. Chaque
532
+ > composant charge ce dont il a besoin (voir `foehn-input-date` dans ce projet).
533
+
534
+ `input-date.component.ts`
535
+
536
+ ```ts
537
+ import { Component, forwardRef } from '@angular/core';
538
+
539
+ import { FoehnInputComponent } from '../foehn-input/foehn-input.component';
540
+ import { FoehnInputNumberComponent } from '../foehn-input/foehn-input-number.component';
541
+ import { FoehnValidationAlertsComponent } from '../foehn-validation-alerts/foehn-validation-alerts.component';
542
+ import { SdkDictionaryPipe } from '../sdk-dictionary/sdk-dictionary.pipe';
543
+
544
+ @Component({
545
+ // point (1) : dans votre prestation, le selector sera plutôt 'app-...'
546
+ selector: 'app-input-date',
547
+ templateUrl: './input-date.component.html',
548
+ // point (2)
549
+ providers: [
550
+ {
551
+ provide: FoehnInputComponent,
552
+ useExisting: forwardRef(() => InputDateComponent),
553
+ // ⚠️ plus de `multi: true` en standalone
554
+ },
555
+ ],
556
+ // les imports se déclarent directement ici : chaque composant charge ce dont il a besoin
557
+ imports: [
558
+ FoehnValidationAlertsComponent,
559
+ FoehnInputNumberComponent,
560
+ SdkDictionaryPipe,
561
+ ],
562
+ })
563
+ export class InputDateComponent extends FoehnInputComponent<number[]> {
564
+ // point (3) : extends FoehnInputComponent<number[]>
565
+
566
+ // point (4) : les attributs de votre modèle répartis en variables publiques
567
+ day: string;
568
+ month: string;
569
+ year: string;
570
+
571
+ // point (5)
572
+ updateDate(): void {
573
+ const validValue = this.getValidValue();
574
+ if (typeof validValue !== 'undefined') {
575
+ this.updateNgModel(validValue);
576
+ }
577
+ }
578
+
579
+ // point (6)
580
+ handleUserInput(): void {
581
+ const validValue = this.getValidValue();
582
+ if (typeof validValue !== 'undefined') {
583
+ this.handleChange(validValue);
584
+ }
585
+ }
586
+
587
+ // point (7)
588
+ override onModelChange(value: number[]): void {
589
+ if (value && value.length > 2) {
590
+ this.year = this.toString(value[0]);
591
+ this.month = this.toString(value[1]);
592
+ this.day = this.toString(value[2]);
593
+ } else if (!value) {
594
+ this.year = null;
595
+ this.month = null;
596
+ this.day = null;
597
+ }
598
+ }
599
+
600
+ // point (8)
601
+ getValidValue(): number[] | null | undefined {
602
+ if (this.isEmpty(this.day) && this.isEmpty(this.month) && this.isEmpty(this.year)) {
603
+ if (this.model_ !== undefined) {
604
+ return null;
605
+ }
606
+
607
+ return undefined;
608
+ }
609
+
610
+ return [this.toNumber(this.year), this.toNumber(this.month), this.toNumber(this.day)];
611
+ }
612
+
613
+ // ...code
614
+ }
615
+ ```
616
+
617
+ Chaque `foehn-input-*` utilisé dans votre vue doit :
618
+
619
+ 1. avoir un `id` qui concatène l'appel à la fonction `buildId()` et le nom de l'attribut de votre objet
620
+ 2. avoir un `name` qui concatène l'input **signal** `name()` et le nom de l'attribut de votre objet
621
+ 3. avoir un `label`
622
+ 4. utiliser votre variable publique comme model en double-binding (banana in the box)
623
+ 5. faire appel à `updateDate()` lors du déclenchement de l'événement `(modelChange)`
624
+ 6. faire appel à `handleUserInput()` lors du déclenchement de l'événement `(userInput)`
625
+
626
+ > 💡 `name` est désormais un **input signal**. On l'invoque : `name() + '_day'` (et non plus
627
+ > `name + '_day'`).
628
+
629
+ `input-date.component.html`
630
+
631
+ ```html
632
+ <foehn-input-number
633
+ [id]="buildId() + '_day'"
634
+ [name]="name() + '_day'"
635
+ [label]="'input-date.day.label' | fromDictionary"
636
+ [(model)]="day"
637
+ (modelChange)="updateDate()"
638
+ (userInput)="handleUserInput()"
639
+ [required]="required()"
640
+ [hideNotRequiredExtraLabel]="true"
641
+ [maxlength]="2"
642
+ />
643
+ <!-- ... -->
644
+ <foehn-input-number
645
+ [id]="buildId() + '_month'"
646
+ [name]="name() + '_month'"
647
+ [label]="'input-date.month.label' | fromDictionary"
648
+ [(model)]="month"
649
+ (modelChange)="updateDate()"
650
+ (userInput)="handleUserInput()"
651
+ [required]="required()"
652
+ [hideNotRequiredExtraLabel]="true"
653
+ [maxlength]="2"
654
+ />
655
+ <!-- ... -->
656
+ <foehn-input-number
657
+ [id]="buildId() + '_year'"
658
+ [name]="name() + '_year'"
659
+ [label]="'input-date.year.label' | fromDictionary"
660
+ [(model)]="year"
661
+ (modelChange)="updateDate()"
662
+ (userInput)="handleUserInput()"
663
+ [required]="required()"
664
+ [hideNotRequiredExtraLabel]="true"
665
+ [maxlength]="4"
666
+ />
667
+ ```
668
+
669
+ ### Afficher la validation globale du composant
670
+
671
+ - Il faut ajouter dans la partie HTML de votre composant le code décrit ci-dessous
672
+ - Remonter une erreur correspondant au `name` de votre composant lorsque celui-ci est utilisé dans
673
+ votre page de formulaire.
674
+
675
+ > 💡 Points **signals** à noter dans ce template :
676
+ >
677
+ > - `@if` remplace `*ngIf` et les attributs du composant sont des signaux : on les invoque
678
+ > (`label()`, `required()`, `isLabelSrOnly()`, `hideNotRequiredExtraLabel()`, `helpText()`).
679
+ > - `[ngClass]` est remplacé par des bindings `[class.xxx]="..."`.
680
+
681
+ ```html
682
+ <!-- (obligatoire) Affichera une barre verticale rouge sur l'entier du composant -->
683
+ <div
684
+ class="form-group"
685
+ [class.has-danger]="hasErrorsToDisplay()"
686
+ [class.vd-form-group-danger]="hasErrorsToDisplay()"
687
+ [attr.id]="buildId('Container')"
688
+ tabindex="-1"
689
+ >
690
+ <!-- (obligatoire) Permet de regrouper vos champs pour une validation globale -->
691
+ <fieldset
692
+ [attr.aria-describedby]="getDescribedBy()"
693
+ [attr.aria-invalid]="hasErrorsToDisplay() || null"
694
+ >
695
+ <!-- (facultatif) Seulement si vous souhaitez afficher un label/legend global à votre composant -->
696
+ @if (!!label()) {
697
+ <legend
698
+ [class.visually-hidden]="isLabelSrOnly()"
699
+ [class.vd-p]="!isLabelSrOnly()"
700
+ >
701
+ <span [innerHTML]="label()"></span>
702
+ @if (!required() && !hideNotRequiredExtraLabel()) {
703
+ <span aria-hidden="true">{{ 'foehn-input.optional' | fromDictionary }}</span>
704
+ }
705
+ </legend>
706
+ }
707
+
708
+ <!-- (obligatoire) -->
709
+ <foehn-validation-alerts [component]="this" />
710
+
711
+ <!-- (facultatif) Seulement si vous souhaitez pouvoir afficher un helpText global au composant -->
712
+ @if (helpText()) {
713
+ <small
714
+ [attr.id]="buildId() + 'Help'"
715
+ class="text-secondary"
716
+ [innerHTML]="helpText()"
717
+ ></small>
718
+ }
719
+
720
+ <!-- ...vos foehn-input-* ici -->
721
+ </fieldset>
722
+ </div>
723
+ ```
724
+
725
+ ### Éviter des problèmes d'affichage d'erreurs dans une boucle `@for`
726
+
727
+ > ⚠️ Ne pas oublier de mettre un `track` avec un identifiant unique dans la boucle `@for` sinon la
728
+ > validation ne s'affichera pas. Quand un identifiant unique tel que `id` n'est pas possible, tracker
729
+ > par objet ou, en dernier recours, `track $index`.
730
+
731
+ > ℹ️ Dans une boucle `@for` d'Angular avec un composant `FoehnInputComponent`, si vous validez votre
732
+ > formulaire, les erreurs remontées sont synchronisées avec l'index de vos composants répétés dans la
733
+ > boucle.
734
+ >
735
+ > **Scénario :** Dans votre liste, lorsque vous supprimez un composant qui contient des erreurs à
736
+ > l'index 0 (par ex.) et que le composant suivant (index 1) n'a pas d'erreur ou pas les mêmes
737
+ > erreurs, alors ce dernier se retrouve avec les erreurs du composant fraîchement supprimé à
738
+ > l'index 0.
739
+ >
740
+ > **Explication :** Ce phénomène est dû au fait que les erreurs remontées par GESDEM ne se
741
+ > rafraîchissent que lors de la demande de sauvegarde des données. Du coup, le composant supprimé à
742
+ > l'index 0 est remplacé par celui à l'index 1 mais les erreurs à l'index 0, elles, existent toujours
743
+ > et se reportent donc sur le composant venant de l'index 1.
744
+
745
+ Voici la bonne pratique à mettre en place pour palier à ces problèmes d'affichage d'erreurs dans une
746
+ boucle `@for`.
747
+
748
+ Lorsque vous supprimez un élément dans votre liste, vous devez appeler une méthode se trouvant dans
749
+ votre `AbstractPageComponent`. Cette méthode contiendra le code suivant :
750
+
751
+ ```ts
752
+ /*
753
+ * if an inventory item that was in error is removed, this prevents the error to be displayed
754
+ * to the next item that has taken it's index
755
+ */
756
+ this._gesdemService.validate(this.form).pipe(first()).subscribe();
757
+ ```
758
+
759
+ Voici un exemple de page de formulaire contenant cette méthode (1) :
760
+
761
+ > 💡 Plus de `constructor` : on fait de l'**inject**. Les dépendances de `AbstractPageComponent`
762
+ > (dont `_dictionaryService` et `_gesdemService`) sont déjà injectées dans la classe abstraite. Pour
763
+ > vos propres dépendances, utilisez `inject()`.
764
+
765
+ ```ts
766
+ @Component({
767
+ templateUrl: './votre-page.component.html',
768
+ imports: [
769
+ // ... vos composants foehn-*
770
+ ],
771
+ })
772
+ export class VotrePageComponent extends AbstractPageComponent<BusinessForm> implements OnInit {
773
+ // vos propres dépendances : inject() au lieu du constructor
774
+ private readonly metaService = inject(MetaService);
775
+
776
+ ngOnInit(): void {
777
+ super.ngOnInit();
778
+ this.setPageTitle(this._dictionaryService.getKeySync('votre-page.title'));
779
+ }
780
+
781
+ newForm(): BusinessForm {
782
+ return new BusinessForm();
783
+ }
784
+
785
+ handleRemovedVehicule(): void {
786
+ // (1)
787
+ /*
788
+ * if an inventory item that was in error is removed, this prevents the error to be displayed
789
+ * to the next item that has taken it's index
790
+ */
791
+ this._gesdemService.validate(this.form).pipe(first()).subscribe();
792
+ }
793
+ }
794
+ ```
795
+
796
+ Il ne tient qu'à vous de décider comment appeler cette méthode lors de la suppression de l'élément de
797
+ votre liste.
798
+
799
+ Voici une manière élégante de gérer vos listes d'éléments :
800
+
801
+ - Structure des dossiers et fichiers
802
+
803
+ ```text
804
+ demande-page/
805
+ ├── vehicules-list/
806
+ │ ├── vehicule-item/
807
+ │ │ ├── vehicule-item.component.css
808
+ │ │ ├── vehicule-item.component.html
809
+ │ │ └── vehicule-item.component.ts
810
+ │ ├── vehicules-list.component.html
811
+ │ └── vehicules-list.component.ts
812
+ ├── demande-page.component.html
813
+ └── demande-page.component.ts
814
+ ```
815
+
816
+ **vehicule-item**
817
+
818
+ `vehicule-item.component.html` contient le bouton pour la suppression de l'élément dans la liste (1).
819
+
820
+ ```html
821
+ <div
822
+ class="form-group clearable-input-form-group"
823
+ [class.has-danger]="hasErrorsToDisplay()"
824
+ [class.vd-form-group-danger]="hasErrorsToDisplay()"
825
+ [attr.id]="buildId('Container')"
826
+ tabindex="-1"
827
+ >
828
+ <foehn-validation-alerts [component]="this" />
829
+
830
+ <div class="row d-flex align-items-end vehicule-infos">
831
+ <!-- ... vos composants foehn-input-* -->
832
+
833
+ <div class="col-md-2 mt-md-0 mt-xs-2">
834
+ <button class="btn btn-danger" (click)="removeVehicule()">
835
+ <!-- (1) -->
836
+ {{ 'demande-page.vehicule-remove' | fromDictionary }}
837
+ </button>
838
+ </div>
839
+ </div>
840
+ </div>
841
+ ```
842
+
843
+ `vehicule-item.component.ts` contient la fonction de suppression qui va remonter l'événement à son
844
+ parent (la liste des éléments).
845
+
846
+ > 💡 `@Output() + EventEmitter` sont remplacés par la fonction `output()`.
847
+
848
+ ```ts
849
+ @Component({
850
+ selector: 'app-vehicule-item',
851
+ templateUrl: './vehicule-item.component.html',
852
+ providers: [
853
+ {
854
+ provide: FoehnInputComponent,
855
+ useExisting: forwardRef(() => VehiculeItemComponent),
856
+ },
857
+ ],
858
+ imports: [
859
+ FoehnValidationAlertsComponent,
860
+ // ... vos foehn-input-*
861
+ SdkDictionaryPipe,
862
+ ],
863
+ })
864
+ export class VehiculeItemComponent extends FoehnInputComponent<Vehicule> {
865
+ readonly removedVehicule = output<void>();
866
+
867
+ removeVehicule(): void {
868
+ this.removedVehicule.emit();
869
+ }
870
+
871
+ // ...code
872
+ }
873
+ ```
874
+
875
+ **vehicules-list**
876
+
877
+ `vehicules-list.component.html` contient votre boucle `@for` (1) et votre composant répétable (2) qui
878
+ devra lui aussi remonter l'événement.
879
+
880
+ > ⚠️ Dans la boucle `@for` ne pas oublier de mettre un `track` (identifiant unique) sinon la
881
+ > validation ne s'affichera pas. Ici nous trackons par exemple avec `vehicule.id`.
882
+
883
+ ```html
884
+ <div
885
+ class="form-group clearable-input-form-group"
886
+ [class.has-danger]="hasErrorsToDisplay()"
887
+ [class.vd-form-group-danger]="hasErrorsToDisplay()"
888
+ [attr.id]="buildId('Container')"
889
+ tabindex="-1"
890
+ >
891
+ @if (label() && type() !== 'hidden') {
892
+ <label
893
+ [attr.for]="buildChildId()"
894
+ [class]="'form-label ' + (isLabelSrOnly() ? 'visually-hidden' : (labelStyleModifier() ?? ''))"
895
+ >
896
+ {{ label() }}
897
+ @if (!required() && !hideNotRequiredExtraLabel()) {
898
+ <span aria-hidden="true">{{ 'foehn-input.optional' | fromDictionary }}</span>
899
+ }
900
+ </label>
901
+ }
902
+
903
+ <foehn-validation-alerts [component]="this" />
904
+
905
+ @if (helpText()) {
906
+ <small
907
+ [attr.id]="buildChildId() + 'Help'"
908
+ class="form-text text-secondary"
909
+ [innerHTML]="helpText()"
910
+ ></small>
911
+ }
912
+
913
+ <!-- (1) votre boucle @for + track (identifiant unique) -->
914
+ @for (vehicule of vehicules; track vehicule.id; let i = $index) {
915
+ <!-- (2) votre composant réutilisable -->
916
+ <app-vehicule-item
917
+ [name]="name() + '[' + i + ']'"
918
+ [(model)]="vehicule"
919
+ (modelChange)="updateVehiculesList($event, i)"
920
+ (userInput)="handleUserInput($event, i)"
921
+ (removedVehicule)="handleRemovedVehicule(i)"
922
+ />
923
+ }
924
+
925
+ <button class="btn btn-primary mt-2" (click)="addVehicule()">
926
+ {{ 'demande-page.vehicule-add' | fromDictionary }}
927
+ </button>
928
+ </div>
929
+ ```
930
+
931
+ > 💡 Le `name` est un input signal : `[name]="name() + '[' + i + ']'"` (et non plus
932
+ > `[name]="name + '[' + i + ']'"`).
933
+
934
+ `vehicules-list.component.ts` remonte à son tour l'événement (1) au parent (`AbstractPageComponent`)
935
+ ainsi que supprimer l'élément de la liste (2). C'est aussi ici que vous pouvez gérer l'ajout d'un
936
+ élément (3).
937
+
938
+ ```ts
939
+ @Component({
940
+ selector: 'app-vehicules-list',
941
+ templateUrl: './vehicules-list.component.html',
942
+ providers: [
943
+ {
944
+ provide: FoehnInputComponent,
945
+ useExisting: forwardRef(() => VehiculesListComponent),
946
+ },
947
+ ],
948
+ imports: [
949
+ VehiculeItemComponent,
950
+ FoehnValidationAlertsComponent,
951
+ SdkDictionaryPipe,
952
+ ],
953
+ })
954
+ export class VehiculesListComponent extends FoehnInputComponent<Vehicule[]> {
955
+ readonly removedVehicule = output<void>(); // (1) événement à remonter au parent
956
+
957
+ vehicules: Vehicule[] = []; // Tableau contenant vos éléments répétables
958
+
959
+ handleRemovedVehicule(index: number): void {
960
+ // (2) votre fonction de suppression de l'élément ainsi que la transmission de l'évènement
961
+ this.vehicules.splice(index, 1); // suppression de l'élément
962
+ this.getValidValueAndUpdateNgModel(); // Mise à jour de votre modèle
963
+ this.removedVehicule.emit(); // (1) on remonte l'évènement
964
+ }
965
+
966
+ addVehicule(): void {
967
+ // (3) Gestion de l'ajout d'un élément
968
+ // ...code
969
+ }
970
+ }
971
+ ```
972
+
973
+ > 💡 Plus besoin de définir une `trackByFn` dédiée : la nouvelle syntaxe `@for` utilise directement
974
+ > l'expression `track` (ici `vehicule.id`).
975
+
976
+ **demande-page**
977
+
978
+ `demande-page.component.html` utilisera votre composant de liste de véhicule (1) et écoutera pour
979
+ l'événement de suppression (2).
980
+
981
+ ```html
982
+ <foehn-form>
983
+ <!-- ... -->
984
+ <h2>{{ 'demande-page.vehicules' | fromDictionary }}</h2>
985
+
986
+ <app-vehicules-list
987
+ [(model)]="form.vehicules"
988
+ name="vehicules"
989
+ [label]="'demande-page.vehicules-list' | fromDictionary"
990
+ [required]="true"
991
+ (removedVehicule)="handleRemovedVehicule()"
992
+ />
993
+ <!-- (1) Utilisation de votre liste - (2) Appel de la méthode qui sauvegardera les données dans GESDEM afin de rafraîchir les erreurs -->
994
+
995
+ <foehn-navigation id="navigation" (onPrevious)="previous()" (onNext)="send()" />
996
+ </foehn-form>
997
+ ```
998
+
999
+ `demande-page.component.ts` se chargera de l'appel à GESDEM afin de re-générer les erreurs du
1000
+ formulaire et ainsi rafraîchir l'index.
1001
+
1002
+ ```ts
1003
+ @Component({
1004
+ // ...
1005
+ })
1006
+ export class DemandePageComponent extends AbstractPageComponent<BusinessForm> implements OnInit {
1007
+ // ...code
1008
+
1009
+ handleRemovedVehicule(): void {
1010
+ /*
1011
+ * if an inventory item that was in error is removed, this prevents the error to be displayed
1012
+ * to the next item that has taken it's index
1013
+ */
1014
+ this._gesdemService.save(this.form).pipe(first()).subscribe();
1015
+ }
1016
+ }
1017
+ ```
1018
+
1019
+ ## Validation du formulaire
1020
+
1021
+ > ℹ️ Lors de la sauvegarde de votre formulaire dans GESDEM, l'`AbstractPageComponent` gère pour vous
1022
+ > l'affichage éventuel des erreurs.
1023
+ >
1024
+ > Si par contre vous souhaitez sauvegarder les données ailleurs que dans GESDEM, vous serez confronté
1025
+ > à la gestion manuelle de l'affichage des erreurs.
1026
+
1027
+ ### Gestion standard
1028
+
1029
+ > ℹ️ Lors de l'enregistrement de vos données dans GESDEM, l'affichage des erreurs est automatiquement
1030
+ > géré pour vous. Cependant, vous devez impérativement respecter certaines règles.
1031
+
1032
+ Prenons un exemple de modèle simple côté front à deux niveaux de profondeur :
1033
+
1034
+ `identification.ts`
1035
+
1036
+ ```ts
1037
+ export class Identification {
1038
+ firstName: string;
1039
+ lastName: string;
1040
+ }
1041
+ ```
1042
+
1043
+ `business-form.ts`
1044
+
1045
+ ```ts
1046
+ export class BusinessForm {
1047
+ acceptCookies: boolean;
1048
+ identification: Identification = new Identification();
1049
+ }
1050
+ ```
1051
+
1052
+ > ⚠️ Le modèle ci-dessus devra impérativement être reflété dans le `name` de vos composants
1053
+ > `foehn-input-*` de votre page.
1054
+
1055
+ `page-1.html`
1056
+
1057
+ ```html
1058
+ <foehn-input-text
1059
+ [(model)]="form.identification.firstName"
1060
+ name="identification.firstName"
1061
+ />
1062
+ <!-- 2 niveaux de profondeur -->
1063
+
1064
+ <foehn-input-text
1065
+ [(model)]="form.identification.lastName"
1066
+ name="identification.lastName"
1067
+ />
1068
+
1069
+ <foehn-boolean-checkbox
1070
+ [(model)]="form.acceptCookies"
1071
+ name="acceptCookies"
1072
+ />
1073
+ <!-- 1 niveau de profondeur -->
1074
+ ```
1075
+
1076
+ - Côté back-end, vous pouvez gérer vos erreurs grâce aux annotations (`@NotEmpty`, `@NotNull`, ...),
1077
+ et **prestations-be** fera le reste pour vous.
1078
+ - Si vous souhaitez remonter une erreur spécifique à un champ de votre formulaire, voici la syntaxe :
1079
+
1080
+ ```java
1081
+ new AbstractSdkController.FormError("identification.lastName", "ErrorCodeHere", "Le nom est obligatoire si vous ne spécifiez pas de prénom")
1082
+ ```
1083
+
1084
+ > ⚠️ Le nom de l'erreur doit impérativement correspondre au nom de votre composant dans votre page
1085
+ > HTML.
1086
+
1087
+ ### Gestion manuelle
1088
+
1089
+ #### Back-end
1090
+
1091
+ Côté back-end JAVA, le serveur devra retourner un objet contenant les erreurs du formulaire afin
1092
+ d'être compatible avec prestations-ng.
1093
+
1094
+ ```java
1095
+ @Getter
1096
+ @Setter
1097
+ @Builder
1098
+ @AllArgsConstructor
1099
+ @NoArgsConstructor
1100
+ public class PostResponse<T> {
1101
+
1102
+ private T dto;
1103
+
1104
+ @Builder.Default
1105
+ private List<AbstractSdkController.FormError> errors = new ArrayList<>();
1106
+ // AbstractSdkController.FormError vient de prestations-be qui est la partie backend de prestations-ng
1107
+ }
1108
+ ```
1109
+
1110
+ Ensuite, ajoutez vos erreurs dans votre modèle avant de le retourner au front-end :
1111
+
1112
+ ```java
1113
+ List<AbstractSdkController.FormError> errors = new ArrayList<>();
1114
+ errors.add(new AbstractSdkController.FormError("identification.lastName", "ErrorCodeHere", "Le nom est obligatoire si vous ne spécifiez pas de prénom"));
1115
+
1116
+ PostResponse.<MyObject>builder().dto(myObject).errors(errors).build();
1117
+ ```
1118
+
1119
+ #### Front-end
1120
+
1121
+ Côté front-end, comme la validation standard, les noms de vos composants `foehn-input-*` de votre
1122
+ page HTML et le nom de vos erreurs devront correspondre.
1123
+
1124
+ ```html
1125
+ <foehn-input-text
1126
+ [(model)]="form.identification.lastName"
1127
+ name="identification.lastName"
1128
+ />
1129
+ ```
1130
+
1131
+ > ⚠️ Au retour de la sauvegarde de vos données, vous allez devoir déclencher manuellement la
1132
+ > validation.
1133
+
1134
+ 1. Dans votre `page.ts`, créez une fonction `handleValidationErrors` pour gérer l'affichage des
1135
+ erreurs.
1136
+ 2. Cette fonction sera appelée au retour du back-end.
1137
+
1138
+ > 💡 Point **signals** : plus de `@ViewChild('formWithModels')`. Pour utiliser un composant dans le
1139
+ > TypeScript, on utilise un **signal** `viewChild(...)`. On accède ensuite à l'instance en l'invoquant :
1140
+ > `this.formWithModels()`.
1141
+
1142
+ `page.ts`
1143
+
1144
+ ```ts
1145
+ import {
1146
+ ErrorTemplate,
1147
+ GrowlBrokerService,
1148
+ GrowlType,
1149
+ ValidationHandlerService,
1150
+ } from '@dsivd/prestations-ng';
1151
+
1152
+ @Component({
1153
+ // ...
1154
+ })
1155
+ export class ... extends AbstractPageComponent<YOUR_MODEL> implements OnInit {
1156
+ // point (7 des signaux) : un signal viewChild au lieu de @ViewChild
1157
+ readonly formWithModels = viewChild(FoehnFormComponent);
1158
+
1159
+ // vos propres dépendances via inject()
1160
+ private readonly httpClient = inject(HttpClient);
1161
+
1162
+ ngOnInit(): void {
1163
+ super.ngOnInit();
1164
+ // Activer l'affichage des erreurs
1165
+ this._validationHandlerService.shouldDisplayErrors(true);
1166
+ }
1167
+
1168
+ // Fonction de sauvegarde
1169
+ save(): void {
1170
+ // Activer l'affichage des erreurs
1171
+ this._validationHandlerService.shouldDisplayErrors(true);
1172
+ // Appel au back pour faire votre validation
1173
+ this.httpClient
1174
+ .post<PostResponse>('api_url_ici', votre_objet)
1175
+ .subscribe((response: PostResponse) => {
1176
+ if (!response.errors.length) {
1177
+ // On prévient l'utilisateur que tout s'est bien passé
1178
+ this._growlService.addWithType(
1179
+ GrowlType.SUCCESS,
1180
+ 'Enregistrement effectué avec succès',
1181
+ );
1182
+ }
1183
+ // Gestion des erreurs au retour du back-end
1184
+ this.handleValidationErrors(response.errors); // point (2)
1185
+ });
1186
+ }
1187
+
1188
+ // Affichage des erreurs
1189
+ private handleValidationErrors(errors: ErrorTemplate[]): void {
1190
+ // point (1)
1191
+ const form = this.formWithModels();
1192
+ // Need to markAsUntouched and markAsPristine so error can be shown
1193
+ form.reset();
1194
+ // Feed errors to validation service
1195
+ this._validationHandlerService.updateErrors(errors);
1196
+ // Manually set the focus. On a real application the abstract-page-controller will do it.
1197
+ form.focusErrorSummary();
1198
+ }
1199
+ }
1200
+ ```
1201
+
1202
+ `page.html`
1203
+
1204
+ ```html
1205
+ <foehn-form>
1206
+ <!-- ... vos composants foehn-input-* -->
1207
+ </foehn-form>
1208
+ ```
1209
+
1210
+ ## Validation manuelle d'un composant
1211
+
1212
+ ### Cacher une erreur
1213
+
1214
+ > ✅ Utiliser la fonction `markAsDirty()` permet de simuler une interaction avec le composant.
1215
+ >
1216
+ > ℹ️ Permet de cacher l'erreur d'un composant foehn-input en erreur.
1217
+
1218
+ ### Afficher une erreur
1219
+
1220
+ > ✅ Utiliser la fonction `markAsPristine()` remet le composant dans son état d'origine comme si
1221
+ > aucune interaction n'avait été effectuée.
1222
+ >
1223
+ > ℹ️ Permet d'afficher l'erreur d'un composant foehn-input en erreur.