@dsivd/prestations-ng 19.1.2 → 19.2.0-beta.10
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 +186 -7
- package/CONTRIBUTING.md +43 -3
- package/GOOD_PRACTICES.md +329 -124
- package/UPGRADING_V18.md +1 -0
- package/UPGRADING_V19.md +224 -3
- package/dsivd-prestations-ng-19.2.0-beta.10.tgz +0 -0
- package/eslint/configs/spec-ts-recommended.mjs +34 -0
- package/eslint/configs/template-recommended.mjs +2 -0
- package/eslint/configs/template-rules.mjs +6 -2
- package/eslint/configs/ts-recommended.mjs +2 -0
- package/eslint/configs/ts-rules.mjs +6 -2
- package/eslint/index.mjs +17 -0
- package/eslint/rules/no-direct-signal-mutation.mjs +1 -1
- package/eslint/signal-names.mjs +66 -35
- package/fesm2022/dsivd-prestations-ng.mjs +5013 -4196
- package/fesm2022/dsivd-prestations-ng.mjs.map +1 -1
- package/package.json +1 -2
- package/schematics/ng-add/index.js +2 -4
- package/schematics/ng-update/migrations/update-13_2_0/index.js +7 -2
- package/schematics/utility/add-allow-synthetic-default-imports.js +2 -2
- package/schematics/utility/add-allowed-common-js-dependencies.js +39 -29
- package/schematics/utility/foehn2-bootstrap5-migration.js +20 -20
- package/schematics/utility/migrate-to-karma-coverage.js +2 -2
- package/schematics/utility/remove-emit-decorator-metadata.js +8 -9
- package/src/eslint/__tests__/signal-names.test.mjs +129 -0
- package/src/eslint/configs/__tests__/configs.test.mjs +46 -2
- package/src/eslint/configs/spec-ts-recommended.mjs +34 -0
- package/src/eslint/configs/template-recommended.mjs +2 -0
- package/src/eslint/configs/template-rules.mjs +6 -2
- package/src/eslint/configs/ts-recommended.mjs +2 -0
- package/src/eslint/configs/ts-rules.mjs +6 -2
- package/src/eslint/index.mjs +17 -0
- package/src/eslint/rules/no-direct-signal-mutation.mjs +1 -1
- package/src/eslint/signal-names.mjs +66 -35
- package/types/dsivd-prestations-ng.d.ts +1184 -998
- package/dsivd-prestations-ng-19.1.2.tgz +0 -0
package/GOOD_PRACTICES.md
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
> Ce guide décrit les bonnes pratiques pour développer une prestation avec `prestations-ng`.
|
|
4
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).
|
|
5
|
+
> fonction **`inject`** (plus de modules, plus d'injection par constructeur). Depuis la **v19.2**,
|
|
6
|
+
> prestations-ng est **zoneless** : les exemples tiennent l'état de vos composants dans des signaux,
|
|
7
|
+
> ce qui fonctionne avec ou sans zone.js (voir [Application zoneless](good-practices#application_zoneless)).
|
|
6
8
|
>
|
|
7
9
|
> 📚 Pour aller plus loin :
|
|
8
10
|
> [ANGULAR_SIGNALS.md](ANGULAR_SIGNALS.md) · [COMPONENTS.md](COMPONENTS.md) · [UPGRADING_V19.md](UPGRADING_V19.md)
|
|
@@ -19,6 +21,10 @@
|
|
|
19
21
|
- [La balise `<fieldset>` et `<legend>`](good-practices#la_balise__code__lt_fieldset_gt___code__et__code__lt_legend_gt___code_)
|
|
20
22
|
- [Liseuse d'écran (afficher / masquer)](good-practices#liseuse_d__cran__afficher___masquer_)
|
|
21
23
|
- [Une page prestations-ng](good-practices#une_page_prestations-ng)
|
|
24
|
+
- [Application zoneless](good-practices#application_zoneless)
|
|
25
|
+
- [Afficher un état asynchrone](good-practices#afficher_un__tat_asynchrone)
|
|
26
|
+
- [Agir sur le DOM après le rendu](good-practices#agir_sur_le_dom_apr_s_le_rendu)
|
|
27
|
+
- [Tester un composant zoneless](good-practices#tester_un_composant_zoneless)
|
|
22
28
|
- [Le routage de vos pages](good-practices#le_routage_de_vos_pages)
|
|
23
29
|
- [Formulaire simple](good-practices#formulaire_simple)
|
|
24
30
|
- [Formulaire complexe (sous-routes)](good-practices#formulaire_complexe__sous-routes_)
|
|
@@ -95,29 +101,25 @@ Ce qu'il ne faut **pas** faire :
|
|
|
95
101
|
|
|
96
102
|
```html
|
|
97
103
|
<h1>...</h1>
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
...
|
|
103
|
-
...
|
|
104
|
+
<h3>...</h3>
|
|
105
|
+
<h2>...</h2>
|
|
106
|
+
<h6>...</h6>
|
|
107
|
+
... ... ...
|
|
104
108
|
```
|
|
105
109
|
|
|
106
110
|
Ce qu'il **faut** faire :
|
|
107
111
|
|
|
108
112
|
```html
|
|
109
113
|
<h1>...</h1>
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
<section>
|
|
120
|
-
...
|
|
114
|
+
<section>
|
|
115
|
+
<h2>...</h2>
|
|
116
|
+
<h3>...</h3>
|
|
117
|
+
<h4>...</h4>
|
|
118
|
+
... ...
|
|
119
|
+
</section>
|
|
120
|
+
<h2>...</h2>
|
|
121
|
+
...
|
|
122
|
+
<section>...</section>
|
|
121
123
|
```
|
|
122
124
|
|
|
123
125
|
Quelques attributs HTML utiles :
|
|
@@ -155,8 +157,7 @@ Exemple :
|
|
|
155
157
|
<foehn-modal
|
|
156
158
|
modalHeaderText="Modal dialog"
|
|
157
159
|
modalSize="modal-lg"
|
|
158
|
-
[isModalVisible]="isModalVisible"
|
|
159
|
-
(isModalVisibleChange)="updateVisibilityStatus($event)"
|
|
160
|
+
[(isModalVisible)]="isModalVisible"
|
|
160
161
|
[modalTriggerHtmlElement]="modalTrigger"
|
|
161
162
|
>
|
|
162
163
|
<!-- on utilise le déclencheur -->
|
|
@@ -164,6 +165,15 @@ Exemple :
|
|
|
164
165
|
</foehn-modal>
|
|
165
166
|
```
|
|
166
167
|
|
|
168
|
+
```ts
|
|
169
|
+
// Un signal : la modale s'affiche et se masque aussi sans zone.js
|
|
170
|
+
readonly isModalVisible = signal(false);
|
|
171
|
+
|
|
172
|
+
openModal(): void {
|
|
173
|
+
this.isModalVisible.set(true);
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
167
177
|
### La balise `<label>`
|
|
168
178
|
|
|
169
179
|
> ⚠️ Tous nos composants `foehn-input-*` disposent d'une balise label ([`<label>`](https://developer.mozilla.org/fr/docs/Web/HTML/Element/label))
|
|
@@ -216,32 +226,36 @@ Exemple :
|
|
|
216
226
|
|
|
217
227
|
```html
|
|
218
228
|
<ul>
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
<li>trois œufs</li>
|
|
224
|
-
<li>La génoise
|
|
225
|
-
<!-- Là on ouvre une autre liste -->
|
|
229
|
+
<li>un artichaut</li>
|
|
230
|
+
<li>
|
|
231
|
+
Les trucs pour le gâteau
|
|
232
|
+
<!-- On voit que </li> n'est pas là -->
|
|
226
233
|
<ul>
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
234
|
+
<li>trois œufs</li>
|
|
235
|
+
<li>
|
|
236
|
+
La génoise
|
|
237
|
+
<!-- Là on ouvre une autre liste -->
|
|
238
|
+
<ul>
|
|
239
|
+
<li>100g de sucre</li>
|
|
240
|
+
<li>un œuf</li>
|
|
241
|
+
<li>150g de farine</li>
|
|
242
|
+
</ul>
|
|
243
|
+
</li>
|
|
244
|
+
<!-- On ferme la liste la plus imbriquée -->
|
|
245
|
+
<li>200g de chocolat</li>
|
|
230
246
|
</ul>
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
</
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
</ol>
|
|
244
|
-
</li>
|
|
247
|
+
<!-- On ferme la liste imbriquée avec </li> -->
|
|
248
|
+
</li>
|
|
249
|
+
<li>De l'essuie-tout</li>
|
|
250
|
+
<li>
|
|
251
|
+
A faire dans l'ordre pour une belle vaisselle
|
|
252
|
+
<ol>
|
|
253
|
+
<li>Remplir le lave vaisselle</li>
|
|
254
|
+
<li>Mettre une pastille pour lave vaisselle</li>
|
|
255
|
+
<li>Fermer la porte du lave vaisselle</li>
|
|
256
|
+
<li>Lancer le lave vaisselle</li>
|
|
257
|
+
</ol>
|
|
258
|
+
</li>
|
|
245
259
|
</ul>
|
|
246
260
|
```
|
|
247
261
|
|
|
@@ -262,18 +276,18 @@ Exemple :
|
|
|
262
276
|
|
|
263
277
|
```html
|
|
264
278
|
<form>
|
|
265
|
-
|
|
266
|
-
|
|
279
|
+
<fieldset>
|
|
280
|
+
<legend>Choose your favorite monster</legend>
|
|
267
281
|
|
|
268
|
-
|
|
269
|
-
|
|
282
|
+
<input type="radio" id="kraken" name="monster" value="K" />
|
|
283
|
+
<label for="kraken">Kraken</label><br />
|
|
270
284
|
|
|
271
|
-
|
|
272
|
-
|
|
285
|
+
<input type="radio" id="sasquatch" name="monster" value="S" />
|
|
286
|
+
<label for="sasquatch">Sasquatch</label><br />
|
|
273
287
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
288
|
+
<input type="radio" id="mothman" name="monster" value="M" />
|
|
289
|
+
<label for="mothman">Mothman</label>
|
|
290
|
+
</fieldset>
|
|
277
291
|
</form>
|
|
278
292
|
```
|
|
279
293
|
|
|
@@ -331,7 +345,10 @@ avec du texte pour les non-voyants.
|
|
|
331
345
|
// ... vos composants foehn-* utilisés dans le template
|
|
332
346
|
],
|
|
333
347
|
})
|
|
334
|
-
export class PageOneComponent
|
|
348
|
+
export class PageOneComponent
|
|
349
|
+
extends AbstractPageComponent<BusinessForm>
|
|
350
|
+
implements OnInit
|
|
351
|
+
{
|
|
335
352
|
ngOnInit(): void {
|
|
336
353
|
super.ngOnInit();
|
|
337
354
|
// point (2) : ajoutera le titre h1
|
|
@@ -361,7 +378,10 @@ export class PageOneComponent extends AbstractPageComponent<BusinessForm> implem
|
|
|
361
378
|
<div class="row">
|
|
362
379
|
<div class="col-md-8">
|
|
363
380
|
<!-- point (5) : toujours avoir un <h2> en premier dans votre page -->
|
|
364
|
-
<h2>
|
|
381
|
+
<h2>
|
|
382
|
+
{{ 'La_clé_du_dictionnaire_de_votre_titre_H2_ici' |
|
|
383
|
+
fromDictionary }}
|
|
384
|
+
</h2>
|
|
365
385
|
|
|
366
386
|
<!-- [VOS COMPOSANTS FOEHN ICI] -->
|
|
367
387
|
|
|
@@ -377,6 +397,126 @@ export class PageOneComponent extends AbstractPageComponent<BusinessForm> implem
|
|
|
377
397
|
</div>
|
|
378
398
|
```
|
|
379
399
|
|
|
400
|
+
## Application zoneless
|
|
401
|
+
|
|
402
|
+
> ℹ️ Depuis la v19.2, prestations-ng n'a plus besoin de zone.js et nous vous recommandons de rendre
|
|
403
|
+
> votre application zoneless (`provideZonelessChangeDetection()`, voir
|
|
404
|
+
> [UPGRADING_V19.md](UPGRADING_V19.md#zoneless-optional-but-recommended)).
|
|
405
|
+
>
|
|
406
|
+
> Sans zone.js, Angular ne rafraîchit un composant que lorsqu'il est prévenu d'un changement :
|
|
407
|
+
>
|
|
408
|
+
> - un listener de son template a été appelé (`(click)`, `(modelChange)`, `(userInput)`, ...), y
|
|
409
|
+
> compris les double-bindings `[(model)]`
|
|
410
|
+
> - un signal lu par son template a changé (`signal()`, `computed()`, `input()`, `model()`,
|
|
411
|
+
> `toSignal()`, ...)
|
|
412
|
+
> - `ChangeDetectorRef.markForCheck()` a été appelé, ce que fait le pipe `async` lorsqu'un observable
|
|
413
|
+
> émet
|
|
414
|
+
|
|
415
|
+
### Afficher un état asynchrone
|
|
416
|
+
|
|
417
|
+
> ⚠️ Un champ modifié ailleurs (dans un `subscribe()`, un `setTimeout()`, une `Promise`, un
|
|
418
|
+
> callback, ...) n'est plus affiché tant que rien d'autre ne rafraîchit le composant. Tenez-le dans
|
|
419
|
+
> un **signal**.
|
|
420
|
+
|
|
421
|
+
Ce qu'il ne faut **pas** faire :
|
|
422
|
+
|
|
423
|
+
```ts
|
|
424
|
+
isLoading = true;
|
|
425
|
+
|
|
426
|
+
ngOnInit(): void {
|
|
427
|
+
this.myService.load().subscribe(() => {
|
|
428
|
+
this.isLoading = false; // pas affiché sans zone.js
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
Ce qu'il **faut** faire :
|
|
434
|
+
|
|
435
|
+
```ts
|
|
436
|
+
readonly isLoading = signal(true);
|
|
437
|
+
|
|
438
|
+
ngOnInit(): void {
|
|
439
|
+
this.myService.load().subscribe(() => {
|
|
440
|
+
this.isLoading.set(false);
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
```html
|
|
446
|
+
@if (isLoading()) {
|
|
447
|
+
<p>{{ 'page.loading' | fromDictionary }}</p>
|
|
448
|
+
}
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
> 💡 Pour un observable affiché tel quel, `toSignal(this.myService.load())` ou le pipe `async` font
|
|
452
|
+
> le travail pour vous.
|
|
453
|
+
|
|
454
|
+
Dans vos pages (`AbstractPageComponent`), prestations-ng rafraîchit la page lorsqu'il charge `form`
|
|
455
|
+
et `reference`. Si vous modifiez vous-même le formulaire dans un callback asynchrone, appelez
|
|
456
|
+
`markForCheck()` ensuite :
|
|
457
|
+
|
|
458
|
+
```ts
|
|
459
|
+
private readonly changeDetectorRef = inject(ChangeDetectorRef);
|
|
460
|
+
|
|
461
|
+
ngOnInit(): void {
|
|
462
|
+
super.ngOnInit();
|
|
463
|
+
this.sessionInfoWithApplicationService.prefillForm((info) => {
|
|
464
|
+
this.form.email = info?.email ?? null;
|
|
465
|
+
this.changeDetectorRef.markForCheck();
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
### Agir sur le DOM après le rendu
|
|
471
|
+
|
|
472
|
+
> ⚠️ `NgZone` (`onStable`, `onMicrotaskEmpty`, `run()`, ...) ne fonctionne plus sans zone.js, et un
|
|
473
|
+
> `setTimeout()` n'attend pas le rendu. Pour agir sur le DOM rendu (focus, scroll, ...), utilisez
|
|
474
|
+
> `afterNextRender()`.
|
|
475
|
+
|
|
476
|
+
```ts
|
|
477
|
+
readonly detailsVisible = signal(false);
|
|
478
|
+
readonly detailsField = viewChild<ElementRef<HTMLElement>>('detailsField');
|
|
479
|
+
|
|
480
|
+
private readonly injector = inject(Injector);
|
|
481
|
+
|
|
482
|
+
showDetails(): void {
|
|
483
|
+
this.detailsVisible.set(true);
|
|
484
|
+
// Le champ n'existe qu'une fois le détail rendu
|
|
485
|
+
afterNextRender(() => this.detailsField()?.nativeElement.focus(), {
|
|
486
|
+
injector: this.injector,
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
### Tester un composant zoneless
|
|
492
|
+
|
|
493
|
+
> ⚠️ `fixture.detectChanges()` **force** le rafraîchissement : un composant qui oublie de prévenir
|
|
494
|
+
> Angular (champ au lieu d'un signal, `markForCheck()` manquant, ...) passe le test, mais n'est pas
|
|
495
|
+
> rafraîchi dans l'application. Laissez Angular rafraîchir le composant, comme en production, avec
|
|
496
|
+
> `await fixture.whenStable()`.
|
|
497
|
+
|
|
498
|
+
```ts
|
|
499
|
+
it('should display the details', async () => {
|
|
500
|
+
fixture.componentRef.setInput('showDetails', true);
|
|
501
|
+
await fixture.whenStable();
|
|
502
|
+
|
|
503
|
+
expect(fixture.nativeElement.querySelector('.details')).not.toBeNull();
|
|
504
|
+
});
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
`await fixture.whenStable()` attend aussi les tâches en cours, comme les requêtes HTTP : le test ne
|
|
508
|
+
se termine pas avant elles (sinon `NG0205: Injector has already been destroyed`).
|
|
509
|
+
|
|
510
|
+
Gardez `fixture.detectChanges()` pour :
|
|
511
|
+
|
|
512
|
+
- vérifier un état intermédiaire, avant la fin d'un traitement asynchrone
|
|
513
|
+
- les tests avec `vi.useFakeTimers()` : le rafraîchissement d'Angular passe par des timers, qu'il
|
|
514
|
+
faut avancer (`await vi.advanceTimersByTimeAsync(...)`), sinon `whenStable()` ne se termine pas
|
|
515
|
+
|
|
516
|
+
> 💡 Pour que le test passe par le même chemin que l'utilisateur, déclenchez les événements dans le
|
|
517
|
+
> DOM (`input.dispatchEvent(new Event('change'))`, `button.click()`, ...) plutôt que d'appeler les
|
|
518
|
+
> méthodes du composant ou de ses services.
|
|
519
|
+
|
|
380
520
|
## Le routage de vos pages
|
|
381
521
|
|
|
382
522
|
### Formulaire simple
|
|
@@ -390,13 +530,21 @@ export const routes: Routes = [
|
|
|
390
530
|
component: PageWrapperComponent,
|
|
391
531
|
data: { root: true },
|
|
392
532
|
children: [
|
|
393
|
-
{
|
|
533
|
+
{
|
|
534
|
+
path: '',
|
|
535
|
+
component: PageOneComponent,
|
|
536
|
+
canActivate: [GesdemLoaderGuard],
|
|
537
|
+
},
|
|
394
538
|
{ path: '404', component: FoehnNotfoundComponent },
|
|
395
539
|
{ path: 'erreur', component: GesdemErrorComponent },
|
|
396
540
|
{
|
|
397
541
|
path: ':reference',
|
|
398
542
|
children: [
|
|
399
|
-
{
|
|
543
|
+
{
|
|
544
|
+
path: 'page-1',
|
|
545
|
+
component: PageOneComponent,
|
|
546
|
+
data: { order: 1 },
|
|
547
|
+
},
|
|
400
548
|
// {...},
|
|
401
549
|
// ...
|
|
402
550
|
{ path: 'erreur', component: GesdemErrorComponent },
|
|
@@ -431,7 +579,9 @@ export const routes: Routes = [
|
|
|
431
579
|
{
|
|
432
580
|
path: 'welcome',
|
|
433
581
|
loadChildren: () =>
|
|
434
|
-
import('./modules/welcome/welcome.routes').then(
|
|
582
|
+
import('./modules/welcome/welcome.routes').then(
|
|
583
|
+
(m) => m.welcome_routes,
|
|
584
|
+
),
|
|
435
585
|
},
|
|
436
586
|
{
|
|
437
587
|
path: 'inscription',
|
|
@@ -443,7 +593,11 @@ export const routes: Routes = [
|
|
|
443
593
|
// {...},
|
|
444
594
|
// ...
|
|
445
595
|
// recovery link should redirect to /inscription/reference
|
|
446
|
-
{
|
|
596
|
+
{
|
|
597
|
+
path: ':reference',
|
|
598
|
+
redirectTo: 'inscription/:reference',
|
|
599
|
+
pathMatch: 'full',
|
|
600
|
+
},
|
|
447
601
|
{ path: '', redirectTo: 'welcome', pathMatch: 'full' },
|
|
448
602
|
{ path: '404', component: FoehnNotfoundComponent },
|
|
449
603
|
{ path: '**', redirectTo: '/404', pathMatch: 'full' },
|
|
@@ -475,12 +629,20 @@ export const inscription_routes: Routes = [
|
|
|
475
629
|
path: '',
|
|
476
630
|
data: { root: true },
|
|
477
631
|
children: [
|
|
478
|
-
{
|
|
632
|
+
{
|
|
633
|
+
path: '',
|
|
634
|
+
component: PageOneComponent,
|
|
635
|
+
canActivate: [GesdemLoaderGuard],
|
|
636
|
+
},
|
|
479
637
|
{ path: 'erreur', component: GesdemErrorComponent },
|
|
480
638
|
{
|
|
481
639
|
path: ':reference',
|
|
482
640
|
children: [
|
|
483
|
-
{
|
|
641
|
+
{
|
|
642
|
+
path: 'page-1',
|
|
643
|
+
component: PageOneComponent,
|
|
644
|
+
data: { order: 1 },
|
|
645
|
+
},
|
|
484
646
|
// {...},
|
|
485
647
|
// ...
|
|
486
648
|
{ path: 'erreur', component: GesdemErrorComponent },
|
|
@@ -513,14 +675,14 @@ Votre composant doit :
|
|
|
513
675
|
1. `selector` devrait commencer par `"app-..."` pour éviter d'éventuels conflits de nommage
|
|
514
676
|
2. avoir un `providers`
|
|
515
677
|
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
|
|
678
|
+
4. répartir les attributs de votre modèle de données dans des **signaux** publics utilisés dans
|
|
517
679
|
votre HTML
|
|
518
680
|
5. implémenter une fonction `update` (ici ce sera `updateDate`) qui fera appel à `updateNgModel` pour
|
|
519
681
|
maintenir la synchronisation de votre modèle de données avec votre page
|
|
520
682
|
6. implémenter une fonction `handleUserInput` qui fera appel à `handleChange` pour continuer à
|
|
521
683
|
notifier le parent des changements de comportement de votre composant
|
|
522
684
|
7. implémenter une fonction `onModelChange` qui vous permettra d'initialiser correctement les
|
|
523
|
-
|
|
685
|
+
signaux correspondant à votre modèle
|
|
524
686
|
8. implémenter une fonction `getValidValue` afin de reconstruire votre modèle de données avant de le
|
|
525
687
|
retransmettre à votre page et retourner `null` si tous les attributs de votre objet sont vides (ça
|
|
526
688
|
c'est pour que Java soit content)
|
|
@@ -530,11 +692,14 @@ Votre composant doit :
|
|
|
530
692
|
> - **Pas de `multi: true`** dans le `providers` (retiré depuis la migration standalone).
|
|
531
693
|
> - Les `imports` du composant se déclarent **directement** dans le décorateur `@Component`. Chaque
|
|
532
694
|
> composant charge ce dont il a besoin (voir `foehn-input-date` dans ce projet).
|
|
695
|
+
> - L'état affiché par le composant est tenu dans des **signaux** (point 4) : `onModelChange` est
|
|
696
|
+
> appelé lorsque le modèle change, parfois en dehors d'un événement du template, et un simple champ
|
|
697
|
+
> ne serait alors pas affiché sans zone.js.
|
|
533
698
|
|
|
534
699
|
`input-date.component.ts`
|
|
535
700
|
|
|
536
701
|
```ts
|
|
537
|
-
import { Component, forwardRef } from '@angular/core';
|
|
702
|
+
import { Component, forwardRef, signal } from '@angular/core';
|
|
538
703
|
|
|
539
704
|
import { FoehnInputComponent } from '../foehn-input/foehn-input.component';
|
|
540
705
|
import { FoehnInputNumberComponent } from '../foehn-input/foehn-input-number.component';
|
|
@@ -563,10 +728,10 @@ import { SdkDictionaryPipe } from '../sdk-dictionary/sdk-dictionary.pipe';
|
|
|
563
728
|
export class InputDateComponent extends FoehnInputComponent<number[]> {
|
|
564
729
|
// point (3) : extends FoehnInputComponent<number[]>
|
|
565
730
|
|
|
566
|
-
// point (4) : les attributs de votre modèle répartis en
|
|
567
|
-
day
|
|
568
|
-
month
|
|
569
|
-
year
|
|
731
|
+
// point (4) : les attributs de votre modèle répartis en signaux publics
|
|
732
|
+
readonly day = signal<string | null>(null);
|
|
733
|
+
readonly month = signal<string | null>(null);
|
|
734
|
+
readonly year = signal<string | null>(null);
|
|
570
735
|
|
|
571
736
|
// point (5)
|
|
572
737
|
updateDate(): void {
|
|
@@ -585,21 +750,25 @@ export class InputDateComponent extends FoehnInputComponent<number[]> {
|
|
|
585
750
|
}
|
|
586
751
|
|
|
587
752
|
// point (7)
|
|
588
|
-
override onModelChange(value: number[]): void {
|
|
753
|
+
override onModelChange(value: number[] | null): void {
|
|
589
754
|
if (value && value.length > 2) {
|
|
590
|
-
this.year
|
|
591
|
-
this.month
|
|
592
|
-
this.day
|
|
755
|
+
this.year.set(this.toString(value[0]));
|
|
756
|
+
this.month.set(this.toString(value[1]));
|
|
757
|
+
this.day.set(this.toString(value[2]));
|
|
593
758
|
} else if (!value) {
|
|
594
|
-
this.year
|
|
595
|
-
this.month
|
|
596
|
-
this.day
|
|
759
|
+
this.year.set(null);
|
|
760
|
+
this.month.set(null);
|
|
761
|
+
this.day.set(null);
|
|
597
762
|
}
|
|
598
763
|
}
|
|
599
764
|
|
|
600
765
|
// point (8)
|
|
601
766
|
getValidValue(): number[] | null | undefined {
|
|
602
|
-
if (
|
|
767
|
+
if (
|
|
768
|
+
this.isEmpty(this.day()) &&
|
|
769
|
+
this.isEmpty(this.month()) &&
|
|
770
|
+
this.isEmpty(this.year())
|
|
771
|
+
) {
|
|
603
772
|
if (this.model_ !== undefined) {
|
|
604
773
|
return null;
|
|
605
774
|
}
|
|
@@ -607,7 +776,11 @@ export class InputDateComponent extends FoehnInputComponent<number[]> {
|
|
|
607
776
|
return undefined;
|
|
608
777
|
}
|
|
609
778
|
|
|
610
|
-
return [
|
|
779
|
+
return [
|
|
780
|
+
this.toNumber(this.year()),
|
|
781
|
+
this.toNumber(this.month()),
|
|
782
|
+
this.toNumber(this.day()),
|
|
783
|
+
];
|
|
611
784
|
}
|
|
612
785
|
|
|
613
786
|
// ...code
|
|
@@ -619,7 +792,8 @@ Chaque `foehn-input-*` utilisé dans votre vue doit :
|
|
|
619
792
|
1. avoir un `id` qui concatène l'appel à la fonction `buildId()` et le nom de l'attribut de votre objet
|
|
620
793
|
2. avoir un `name` qui concatène l'input **signal** `name()` et le nom de l'attribut de votre objet
|
|
621
794
|
3. avoir un `label`
|
|
622
|
-
4. utiliser votre
|
|
795
|
+
4. utiliser votre signal public comme model en double-binding (banana in the box) : `[(model)]="day"`
|
|
796
|
+
lit et écrit le signal (sans les parenthèses)
|
|
623
797
|
5. faire appel à `updateDate()` lors du déclenchement de l'événement `(modelChange)`
|
|
624
798
|
6. faire appel à `handleUserInput()` lors du déclenchement de l'événement `(userInput)`
|
|
625
799
|
|
|
@@ -694,15 +868,17 @@ Chaque `foehn-input-*` utilisé dans votre vue doit :
|
|
|
694
868
|
>
|
|
695
869
|
<!-- (facultatif) Seulement si vous souhaitez afficher un label/legend global à votre composant -->
|
|
696
870
|
@if (!!label()) {
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
871
|
+
<legend
|
|
872
|
+
[class.visually-hidden]="isLabelSrOnly()"
|
|
873
|
+
[class.vd-p]="!isLabelSrOnly()"
|
|
874
|
+
>
|
|
875
|
+
<span [innerHTML]="label()"></span>
|
|
876
|
+
@if (!required() && !hideNotRequiredExtraLabel()) {
|
|
877
|
+
<span aria-hidden="true"
|
|
878
|
+
>{{ 'foehn-input.optional' | fromDictionary }}</span
|
|
700
879
|
>
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
<span aria-hidden="true">{{ 'foehn-input.optional' | fromDictionary }}</span>
|
|
704
|
-
}
|
|
705
|
-
</legend>
|
|
880
|
+
}
|
|
881
|
+
</legend>
|
|
706
882
|
}
|
|
707
883
|
|
|
708
884
|
<!-- (obligatoire) -->
|
|
@@ -710,11 +886,11 @@ Chaque `foehn-input-*` utilisé dans votre vue doit :
|
|
|
710
886
|
|
|
711
887
|
<!-- (facultatif) Seulement si vous souhaitez pouvoir afficher un helpText global au composant -->
|
|
712
888
|
@if (helpText()) {
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
889
|
+
<small
|
|
890
|
+
[attr.id]="buildId() + 'Help'"
|
|
891
|
+
class="text-secondary"
|
|
892
|
+
[innerHTML]="helpText()"
|
|
893
|
+
></small>
|
|
718
894
|
}
|
|
719
895
|
|
|
720
896
|
<!-- ...vos foehn-input-* ici -->
|
|
@@ -769,13 +945,18 @@ Voici un exemple de page de formulaire contenant cette méthode (1) :
|
|
|
769
945
|
// ... vos composants foehn-*
|
|
770
946
|
],
|
|
771
947
|
})
|
|
772
|
-
export class VotrePageComponent
|
|
948
|
+
export class VotrePageComponent
|
|
949
|
+
extends AbstractPageComponent<BusinessForm>
|
|
950
|
+
implements OnInit
|
|
951
|
+
{
|
|
773
952
|
// vos propres dépendances : inject() au lieu du constructor
|
|
774
953
|
private readonly metaService = inject(MetaService);
|
|
775
954
|
|
|
776
955
|
ngOnInit(): void {
|
|
777
956
|
super.ngOnInit();
|
|
778
|
-
this.setPageTitle(
|
|
957
|
+
this.setPageTitle(
|
|
958
|
+
this._dictionaryService.getKeySync('votre-page.title'),
|
|
959
|
+
);
|
|
779
960
|
}
|
|
780
961
|
|
|
781
962
|
newForm(): BusinessForm {
|
|
@@ -889,37 +1070,38 @@ devra lui aussi remonter l'événement.
|
|
|
889
1070
|
tabindex="-1"
|
|
890
1071
|
>
|
|
891
1072
|
@if (label() && type() !== 'hidden') {
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
1073
|
+
<label
|
|
1074
|
+
[attr.for]="buildChildId()"
|
|
1075
|
+
[class]="'form-label ' + (isLabelSrOnly() ? 'visually-hidden' : (labelStyleModifier() ?? ''))"
|
|
1076
|
+
>
|
|
1077
|
+
{{ label() }} @if (!required() && !hideNotRequiredExtraLabel()) {
|
|
1078
|
+
<span aria-hidden="true"
|
|
1079
|
+
>{{ 'foehn-input.optional' | fromDictionary }}</span
|
|
895
1080
|
>
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
<span aria-hidden="true">{{ 'foehn-input.optional' | fromDictionary }}</span>
|
|
899
|
-
}
|
|
900
|
-
</label>
|
|
1081
|
+
}
|
|
1082
|
+
</label>
|
|
901
1083
|
}
|
|
902
1084
|
|
|
903
1085
|
<foehn-validation-alerts [component]="this" />
|
|
904
1086
|
|
|
905
1087
|
@if (helpText()) {
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
1088
|
+
<small
|
|
1089
|
+
[attr.id]="buildChildId() + 'Help'"
|
|
1090
|
+
class="form-text text-secondary"
|
|
1091
|
+
[innerHTML]="helpText()"
|
|
1092
|
+
></small>
|
|
911
1093
|
}
|
|
912
1094
|
|
|
913
1095
|
<!-- (1) votre boucle @for + track (identifiant unique) -->
|
|
914
|
-
@for (vehicule of vehicules; track vehicule.id; let i = $index) {
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
1096
|
+
@for (vehicule of vehicules(); track vehicule.id; let i = $index) {
|
|
1097
|
+
<!-- (2) votre composant réutilisable -->
|
|
1098
|
+
<app-vehicule-item
|
|
1099
|
+
[name]="name() + '[' + i + ']'"
|
|
1100
|
+
[model]="vehicule"
|
|
1101
|
+
(modelChange)="updateVehiculesList($event, i)"
|
|
1102
|
+
(userInput)="handleUserInput($event, i)"
|
|
1103
|
+
(removedVehicule)="handleRemovedVehicule(i)"
|
|
1104
|
+
/>
|
|
923
1105
|
}
|
|
924
1106
|
|
|
925
1107
|
<button class="btn btn-primary mt-2" (click)="addVehicule()">
|
|
@@ -930,6 +1112,9 @@ devra lui aussi remonter l'événement.
|
|
|
930
1112
|
|
|
931
1113
|
> 💡 Le `name` est un input signal : `[name]="name() + '[' + i + ']'"` (et non plus
|
|
932
1114
|
> `[name]="name + '[' + i + ']'"`).
|
|
1115
|
+
>
|
|
1116
|
+
> 💡 La variable d'une boucle `@for` ne peut pas être modifiée : on passe l'élément avec `[model]` et
|
|
1117
|
+
> on remplace l'élément de la liste dans `updateVehiculesList()` (appelée par `(modelChange)`).
|
|
933
1118
|
|
|
934
1119
|
`vehicules-list.component.ts` remonte à son tour l'événement (1) au parent (`AbstractPageComponent`)
|
|
935
1120
|
ainsi que supprimer l'élément de la liste (2). C'est aussi ici que vous pouvez gérer l'ajout d'un
|
|
@@ -954,15 +1139,29 @@ ainsi que supprimer l'élément de la liste (2). C'est aussi ici que vous pouvez
|
|
|
954
1139
|
export class VehiculesListComponent extends FoehnInputComponent<Vehicule[]> {
|
|
955
1140
|
readonly removedVehicule = output<void>(); // (1) événement à remonter au parent
|
|
956
1141
|
|
|
957
|
-
|
|
1142
|
+
// Signal contenant vos éléments répétables : il est remplacé, jamais modifié sur place
|
|
1143
|
+
readonly vehicules = signal<Vehicule[]>([]);
|
|
1144
|
+
|
|
1145
|
+
updateVehiculesList(vehicule: Vehicule, index: number): void {
|
|
1146
|
+
this.vehicules.update((vehicules) =>
|
|
1147
|
+
vehicules.map((item, i) => (i === index ? vehicule : item)),
|
|
1148
|
+
);
|
|
1149
|
+
this.getValidValueAndUpdateNgModel(); // Mise à jour de votre modèle
|
|
1150
|
+
}
|
|
958
1151
|
|
|
959
1152
|
handleRemovedVehicule(index: number): void {
|
|
960
1153
|
// (2) votre fonction de suppression de l'élément ainsi que la transmission de l'évènement
|
|
961
|
-
this.vehicules.
|
|
1154
|
+
this.vehicules.update((vehicules) =>
|
|
1155
|
+
vehicules.filter((_vehicule, i) => i !== index),
|
|
1156
|
+
); // suppression de l'élément
|
|
962
1157
|
this.getValidValueAndUpdateNgModel(); // Mise à jour de votre modèle
|
|
963
1158
|
this.removedVehicule.emit(); // (1) on remonte l'évènement
|
|
964
1159
|
}
|
|
965
1160
|
|
|
1161
|
+
override onModelChange(value: Vehicule[] | null): void {
|
|
1162
|
+
this.vehicules.set(value ?? []);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
966
1165
|
addVehicule(): void {
|
|
967
1166
|
// (3) Gestion de l'ajout d'un élément
|
|
968
1167
|
// ...code
|
|
@@ -992,7 +1191,11 @@ l'événement de suppression (2).
|
|
|
992
1191
|
/>
|
|
993
1192
|
<!-- (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
1193
|
|
|
995
|
-
<foehn-navigation
|
|
1194
|
+
<foehn-navigation
|
|
1195
|
+
id="navigation"
|
|
1196
|
+
(onPrevious)="previous()"
|
|
1197
|
+
(onNext)="send()"
|
|
1198
|
+
/>
|
|
996
1199
|
</foehn-form>
|
|
997
1200
|
```
|
|
998
1201
|
|
|
@@ -1003,7 +1206,10 @@ formulaire et ainsi rafraîchir l'index.
|
|
|
1003
1206
|
@Component({
|
|
1004
1207
|
// ...
|
|
1005
1208
|
})
|
|
1006
|
-
export class DemandePageComponent
|
|
1209
|
+
export class DemandePageComponent
|
|
1210
|
+
extends AbstractPageComponent<BusinessForm>
|
|
1211
|
+
implements OnInit
|
|
1212
|
+
{
|
|
1007
1213
|
// ...code
|
|
1008
1214
|
|
|
1009
1215
|
handleRemovedVehicule(): void {
|
|
@@ -1066,10 +1272,7 @@ export class BusinessForm {
|
|
|
1066
1272
|
name="identification.lastName"
|
|
1067
1273
|
/>
|
|
1068
1274
|
|
|
1069
|
-
<foehn-boolean-checkbox
|
|
1070
|
-
[(model)]="form.acceptCookies"
|
|
1071
|
-
name="acceptCookies"
|
|
1072
|
-
/>
|
|
1275
|
+
<foehn-boolean-checkbox [(model)]="form.acceptCookies" name="acceptCookies" />
|
|
1073
1276
|
<!-- 1 niveau de profondeur -->
|
|
1074
1277
|
```
|
|
1075
1278
|
|
|
@@ -1169,7 +1372,9 @@ export class ... extends AbstractPageComponent<YOUR_MODEL> implements OnInit {
|
|
|
1169
1372
|
save(): void {
|
|
1170
1373
|
// Activer l'affichage des erreurs
|
|
1171
1374
|
this._validationHandlerService.shouldDisplayErrors(true);
|
|
1172
|
-
// Appel au back pour faire votre validation
|
|
1375
|
+
// Appel au back pour faire votre validation. Le callback ne modifie aucun champ de la page :
|
|
1376
|
+
// les services de prestations-ng (growl, erreurs) rafraîchissent eux-mêmes l'affichage, aussi
|
|
1377
|
+
// sans zone.js. Un état propre à la page modifié ici devrait être un signal.
|
|
1173
1378
|
this.httpClient
|
|
1174
1379
|
.post<PostResponse>('api_url_ici', votre_objet)
|
|
1175
1380
|
.subscribe((response: PostResponse) => {
|