@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.
Files changed (36) hide show
  1. package/CHANGELOG.md +186 -7
  2. package/CONTRIBUTING.md +43 -3
  3. package/GOOD_PRACTICES.md +329 -124
  4. package/UPGRADING_V18.md +1 -0
  5. package/UPGRADING_V19.md +224 -3
  6. package/dsivd-prestations-ng-19.2.0-beta.10.tgz +0 -0
  7. package/eslint/configs/spec-ts-recommended.mjs +34 -0
  8. package/eslint/configs/template-recommended.mjs +2 -0
  9. package/eslint/configs/template-rules.mjs +6 -2
  10. package/eslint/configs/ts-recommended.mjs +2 -0
  11. package/eslint/configs/ts-rules.mjs +6 -2
  12. package/eslint/index.mjs +17 -0
  13. package/eslint/rules/no-direct-signal-mutation.mjs +1 -1
  14. package/eslint/signal-names.mjs +66 -35
  15. package/fesm2022/dsivd-prestations-ng.mjs +5013 -4196
  16. package/fesm2022/dsivd-prestations-ng.mjs.map +1 -1
  17. package/package.json +1 -2
  18. package/schematics/ng-add/index.js +2 -4
  19. package/schematics/ng-update/migrations/update-13_2_0/index.js +7 -2
  20. package/schematics/utility/add-allow-synthetic-default-imports.js +2 -2
  21. package/schematics/utility/add-allowed-common-js-dependencies.js +39 -29
  22. package/schematics/utility/foehn2-bootstrap5-migration.js +20 -20
  23. package/schematics/utility/migrate-to-karma-coverage.js +2 -2
  24. package/schematics/utility/remove-emit-decorator-metadata.js +8 -9
  25. package/src/eslint/__tests__/signal-names.test.mjs +129 -0
  26. package/src/eslint/configs/__tests__/configs.test.mjs +46 -2
  27. package/src/eslint/configs/spec-ts-recommended.mjs +34 -0
  28. package/src/eslint/configs/template-recommended.mjs +2 -0
  29. package/src/eslint/configs/template-rules.mjs +6 -2
  30. package/src/eslint/configs/ts-recommended.mjs +2 -0
  31. package/src/eslint/configs/ts-rules.mjs +6 -2
  32. package/src/eslint/index.mjs +17 -0
  33. package/src/eslint/rules/no-direct-signal-mutation.mjs +1 -1
  34. package/src/eslint/signal-names.mjs +66 -35
  35. package/types/dsivd-prestations-ng.d.ts +1184 -998
  36. 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
- <h3>...</h3>
99
- <h2>...</h2>
100
- <h6>...</h6>
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
- <section>
111
- <h2>...</h2>
112
- <h3>...</h3>
113
- <h4>...</h4>
114
- ...
115
- ...
116
- </section>
117
- <h2>...</h2>
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
- <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 -->
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
- <li>100g de sucre</li>
228
- <li>un œuf</li>
229
- <li>150g de farine</li>
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
- </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>
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
- <fieldset>
266
- <legend>Choose your favorite monster</legend>
279
+ <fieldset>
280
+ <legend>Choose your favorite monster</legend>
267
281
 
268
- <input type="radio" id="kraken" name="monster" value="K" />
269
- <label for="kraken">Kraken</label><br />
282
+ <input type="radio" id="kraken" name="monster" value="K" />
283
+ <label for="kraken">Kraken</label><br />
270
284
 
271
- <input type="radio" id="sasquatch" name="monster" value="S" />
272
- <label for="sasquatch">Sasquatch</label><br />
285
+ <input type="radio" id="sasquatch" name="monster" value="S" />
286
+ <label for="sasquatch">Sasquatch</label><br />
273
287
 
274
- <input type="radio" id="mothman" name="monster" value="M" />
275
- <label for="mothman">Mothman</label>
276
- </fieldset>
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 extends AbstractPageComponent<BusinessForm> implements OnInit {
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>{{ 'La_clé_du_dictionnaire_de_votre_titre_H2_ici' | fromDictionary }}</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
- { path: '', component: PageOneComponent, canActivate: [GesdemLoaderGuard] },
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
- { path: 'page-1', component: PageOneComponent, data: { order: 1 } },
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((m) => m.welcome_routes),
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
- { path: ':reference', redirectTo: 'inscription/:reference', pathMatch: 'full' },
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
- { path: '', component: PageOneComponent, canActivate: [GesdemLoaderGuard] },
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
- { path: 'page-1', component: PageOneComponent, data: { order: 1 } },
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 variables publiques utilisées dans
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
- variables publiques correspondant à votre modèle
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 variables publiques
567
- day: string;
568
- month: string;
569
- year: string;
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 = this.toString(value[0]);
591
- this.month = this.toString(value[1]);
592
- this.day = this.toString(value[2]);
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 = null;
595
- this.month = null;
596
- this.day = null;
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 (this.isEmpty(this.day) && this.isEmpty(this.month) && this.isEmpty(this.year)) {
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 [this.toNumber(this.year), this.toNumber(this.month), this.toNumber(this.day)];
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 variable publique comme model en double-binding (banana in the box)
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
- <legend
698
- [class.visually-hidden]="isLabelSrOnly()"
699
- [class.vd-p]="!isLabelSrOnly()"
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
- <span [innerHTML]="label()"></span>
702
- @if (!required() && !hideNotRequiredExtraLabel()) {
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
- <small
714
- [attr.id]="buildId() + 'Help'"
715
- class="text-secondary"
716
- [innerHTML]="helpText()"
717
- ></small>
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 extends AbstractPageComponent<BusinessForm> implements OnInit {
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(this._dictionaryService.getKeySync('votre-page.title'));
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
- <label
893
- [attr.for]="buildChildId()"
894
- [class]="'form-label ' + (isLabelSrOnly() ? 'visually-hidden' : (labelStyleModifier() ?? ''))"
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
- {{ label() }}
897
- @if (!required() && !hideNotRequiredExtraLabel()) {
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
- <small
907
- [attr.id]="buildChildId() + 'Help'"
908
- class="form-text text-secondary"
909
- [innerHTML]="helpText()"
910
- ></small>
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
- <!-- (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
- />
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
- vehicules: Vehicule[] = []; // Tableau contenant vos éléments répétables
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.splice(index, 1); // suppression de l'élément
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 id="navigation" (onPrevious)="previous()" (onNext)="send()" />
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 extends AbstractPageComponent<BusinessForm> implements OnInit {
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) => {