@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.
- package/CHANGELOG.md +27 -0
- package/GOOD_PRACTICES.md +1223 -0
- package/UPGRADING_V19.md +17 -1
- package/dsivd-prestations-ng-19.1.0.tgz +0 -0
- package/fesm2022/dsivd-prestations-ng.mjs +95 -12
- package/fesm2022/dsivd-prestations-ng.mjs.map +1 -1
- package/package.json +1 -1
- package/types/dsivd-prestations-ng.d.ts +29 -1
- package/dsivd-prestations-ng-19.0.8.tgz +0 -0
|
@@ -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.
|