@dsivd/prestations-ng 19.2.0-beta.4 → 19.2.0-beta.6

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 CHANGED
@@ -21,6 +21,132 @@ Please check the [UPGRADING_V19.md](UPGRADING_V19.md) guide for more information
21
21
  - 🚨 `FoehnListItemDescription` now takes a type parameter: `FoehnListItemDescription<T>`.
22
22
  `getFormattedValue(item: T)` and `hideIfEmpty(item: T)` receive your item type directly, no cast needed.
23
23
  Replace `FoehnListItemDescription[]` with `FoehnListItemDescription<MyItem>[]` in your project.
24
+ - 🚀 **prestations-ng is now zoneless**: its components no longer need zone.js to be refreshed. They keep
25
+ working in an application using zone.js, and your application can now drop zone.js, see
26
+ [UPGRADING_V19.md](UPGRADING_V19.md#zoneless-optional-but-recommended).
27
+ - the state displayed by the components is held by signals (`signal()`, `computed()`, `linkedSignal()`)
28
+ - what was waiting for `NgZone.onMicrotaskEmpty` or a `setTimeout()` to reach the rendered DOM (focus,
29
+ scroll, value displayed by the autocompletes, default values...) now uses `afterNextRender()`
30
+ - the short asynchronous work of the components is tracked with `PendingTasks`, so that
31
+ `ApplicationRef.whenStable()` (and `fixture.whenStable()` in your tests) waits for it: the debounced
32
+ `userInput` output of the `foehn-input-*` components, the debounced time of `foehn-input-date-time`,
33
+ the locale of `foehn-date-picker`, the default country and the sanitized model of `foehn-input-address`
34
+ - `providePrestationsNgCore()` now provides `provideBrowserGlobalErrorListeners()`: without zone.js, the
35
+ errors thrown in a timer or a promise still reach your `ErrorHandler`
36
+ - 🚨 **The inputs and outputs of the components are unchanged**, but the following public fields are now
37
+ signals. If your code reads them (e.g. through `viewChild()` or in your tests), call them (`isVisible()`
38
+ instead of `isVisible`) and write them with `.set()`. Those marked _read-only_ are `computed()` signals.
39
+ - `foehn-agenda`: `srAnnouncements`
40
+ - `foehn-agenda-timeslot-panel`: `selectedTimeSlot`, `timeInputValue`, `isClosed`, `timeSlots`
41
+ (_read-only_), `timeslotRemaining` (_read-only_), `canClose` (_read-only_)
42
+ - `foehn-autocomplete`: `searchValue`, `inputHasFocus`, `hideSuggestions`, `displayedElements`,
43
+ `notificationMessageUpdated`, `VIEW_PORT_HEIGHT` (_read-only_)
44
+ - `foehn-multiselect-autocomplete`: `autocompleteInputValue`, `showEmptyListMessage`, `srAnnouncements`
45
+ - `foehn-select`, `foehn-radio`, `foehn-checkbox` and the autocompletes (`FoehnCheckableGroupComponent`):
46
+ `groupedElements`
47
+ - `foehn-radio`: `inputValue`
48
+ - `foehn-confirm-modal`: `isVisible` (`content` is now a read-only field instead of a getter)
49
+ - `foehn-date-picker`: `hostId`, `weekDays`, `weeks`, `selectedDate`, `monthListItem`
50
+ - `foehn-dropdown-menu`: `isDropdownVisible`, `popupPosition`
51
+ - `foehn-header`: `connectedEsMenu` (_read-only_)
52
+ - `foehn-input-address`: `country`, `locality`, `street`, `houseNumber`, `postOfficeBoxText`,
53
+ `addressLine1`, `addressLine2` (linked to the model: they follow it and can still be set),
54
+ `streetCustomErrors`
55
+ - `foehn-input-foreign-locality`: `zipCode`, `longName`
56
+ - `foehn-input-foreign-street`: `longName`
57
+ - `foehn-input-date`: `day`, `month`, `year`, `datePickerModel`
58
+ - `foehn-input-date-time`: `date`, `time` (linked to the model: they follow it and can still be set)
59
+ - `foehn-input-time`: `hour`, `minute` (linked to the model: they follow it and can still be set)
60
+ - `foehn-input-nav13`: `part2`, `part3`, `part4`
61
+ - `foehn-input-textarea`: `isCharCountExceeded`, `charCountLeft`, `charCountLabel`, `ariaLiveStatus`
62
+ - `foehn-list` and `foehn-table`: `currentPage`, `filteredList`
63
+ - `foehn-list-summary`: `listCopyForTable`, `tableConfiguration` (_read-only_)
64
+ - `foehn-notfound`: `seconds`
65
+ - `foehn-page`: `pageTitle`
66
+ - `foehn-page-counter`: `isStable` (was an `Observable<boolean>`)
67
+ - `foehn-page-modal`: `wrongPublicModalVisible`, `formattedPublicsCibles`
68
+ - `foehn-picture-upload`: `pictureToCrop`, `croppedPictureAsBlob`, `loading`
69
+ - `foehn-status-progress-bar`: `isTransmitted`
70
+ - `gesdem-action-recovery-login`: `demandeNotFound`, `rateLimited`, `rateLimitCounter`, `otpRecipient`,
71
+ `otp`
72
+ - `gesdem-action-recovery-registration`: `mailSent`, `model` (replaced by the new `updateModel()` method,
73
+ never mutated)
74
+ - `epayment-form` (`SdkEpaymentComponent`): `failureModalDisplayed`, `redirectModalDisplayed`
75
+ - `captcha` (`SdkRecaptchaComponent`): `isLoaded`, `hasLoadingError`
76
+ - `redirector` (`RedirectComponent`): `counter`
77
+ - The lifecycle hooks that have nothing left to do are removed: `ngOnChanges()` of
78
+ `foehn-agenda-timeslot-panel`, `ngAfterViewInit()` of `foehn-list-summary`, `ngOnInit()` and `ngOnDestroy()`
79
+ of `captcha`.
80
+ - `SdkDictionaryService.getKeySync()` reads a signal: a `computed()` or a template calling it is refreshed
81
+ when the dictionary is (re)loaded, e.g. when the language changes.
82
+ - `foehn-autocomplete` and `foehn-multiselect-autocomplete`: the value to display, its reset, the default
83
+ values and the focus after a click on a suggestion are applied after the next rendering instead of in a
84
+ `setTimeout()`.
85
+ - Unit tests of the library: Karma and Jasmine are replaced by Vitest 4 in browser mode (Playwright,
86
+ headless Chromium), see [CONTRIBUTING.md](CONTRIBUTING.md#unit-testing).
87
+ - `core-js` is no longer a peer dependency: prestations-ng no longer imports it since 19.1.1, see
88
+ [UPGRADING_V19.md](UPGRADING_V19.md#core-js-is-no-longer-needed-since-1920).
89
+ - `ng add @dsivd/prestations-ng` adds `iban` and `dayjs` (the CommonJS dependencies of prestations-ng) to the
90
+ `allowedCommonJsDependencies` of `angular.json`, instead of `core-js/modules/es.array.includes`, `iban` and
91
+ `zone.js/dist/zone-error`, and no longer sets `allowSyntheticDefaultImports` (implied by the `tsconfig.json` of
92
+ Angular 21: `"module": "preserve"` or `"moduleResolution": "bundler"`).
93
+
94
+ ### Deprecated
95
+
96
+ - `AbstractMenuPageComponent.ngZone` is no longer used and will be removed: `NgZone.onStable` never emits
97
+ without zone.js.
98
+
99
+ ### Fixed
100
+
101
+ - `providePrestationsNgCore()` registers again the HTTP interceptors lost with `FoehnPageModule` in 19.0.0: the
102
+ `foehn-transmit-waiting-modal` is displayed again during a long transmission, the warning before the expiration of
103
+ the session receives its date again, and a new version of the application deployed meanwhile is detected again.
104
+ - `foehn-select`, `foehn-radio`, `foehn-checkbox` and the autocompletes:
105
+ - the model is no longer set back to its previous value when the parent changes `elements` and `model`
106
+ at the same time
107
+ - no more error when the component is destroyed while its `elementsUrl` is loading
108
+ - with a `defaultElementValue`, the model can be emptied again (e.g. set to `null` by the parent): the
109
+ default value was set back at once
110
+ - a change of the model or of `disabled` no longer processes the elements again (the default value, the
111
+ display of the model, the grouped elements and, for the autocompletes, the matching of the model)
112
+ - `foehn-autocomplete`:
113
+ - the model is subscribed only once, whatever the number of times the elements are retrieved: the value
114
+ to display was selected again for each retrieval
115
+ - the height of the suggestion list always follows `itemHeightInSuggestionListInPx`, even before the first
116
+ search returning less than 5 suggestions
117
+ - dynamic list (`isSuggestionListDynamic`): a model already set is no longer reset, even for a moment
118
+ (`modelChange` emitted `null`, then the value again), when the loaded elements do not contain it; it is
119
+ displayed once the elements loaded for it contain it, including when they are given at once with the model
120
+ - `isSuggestionListDynamic` can change at runtime: only the matching of the current kind of list is active,
121
+ the matching of the previous kind went on
122
+ - dynamic list: a model changed by the project (another value, `null`) is displayed again; once a value had been
123
+ displayed, the display no longer followed the model. The elements loaded for the search of the user still do
124
+ not replace what they type.
125
+ - `foehn-input-address`: the deferred clean-up of the model (hidden address lines and post office box) applies
126
+ to the latest model, it could restore a previous value (e.g. remove the default country); nothing is
127
+ updated anymore once the component is destroyed.
128
+ - `foehn-multi-upload` and `foehn-bo-multi-upload`:
129
+ - after a failed upload (network, HTTP error), the next files are uploaded: nothing was uploaded anymore,
130
+ without any message, until the page was reloaded
131
+ - after a failed deletion (HTTP error, readonly document), the next files are deleted: nothing was deleted
132
+ anymore, and the error also reached the `ErrorHandler` as unhandled (its message is still displayed)
133
+ - `PendingUploadService.removePendingFile()`: a file which is not pending no longer removes the last pending file.
134
+ - `foehn-multi-upload` and `foehn-bo-multi-upload`: `customErrors` is used again for the errors which have a default
135
+ message (`413`, `415`, `500`, `FILENAME_TOO_LONG`, `FILENAME_SHOULD_NOT_CONTAIN`...): the default messages
136
+ overwrote the given object. Its messages can use `{fileName}` and `{fileMaxSize}`, `{fileExtensions}`,
137
+ `{maxFileNameLength}`, `{fileNameIllegalCharacters}` (as documented) as well as `%1$s` and `%2$s`.
138
+ - `UploaderHelper`: `getDefaultCustomErrors()` and `mergeCustomErrors()` are added, `createDefaultCustomErrors()` is
139
+ deprecated as it overwrites the given custom errors.
140
+ - `foehn-agenda-timeslot-panel`: when the time slots of the day change, the panel is closed again (only the
141
+ first time slots are displayed) unless the selected slot would be hidden; the list was cut while the panel
142
+ stayed open.
143
+ - `foehn-breadcrumb`: a breadcrumb updated in place is displayed again when it is emitted.
144
+ - `fromDictionary` pipe: its subscription is released when the pipe is destroyed.
145
+ - `SdkLogsService`: a log is not sent anymore once the application is destroyed (e.g. at the end of a test),
146
+ when it made the `HttpClient` throw `NG0205`.
147
+ - `ng add @dsivd/prestations-ng` works again on the Angular workspaces created since Angular 14: it failed because
148
+ `angular.json` no longer has a `defaultProject` (the dependencies are now added to every application), then
149
+ because of the comments of `tsconfig.json` (now kept, like its formatting).
24
150
 
25
151
  ## [19.1.1]
26
152
 
package/CONTRIBUTING.md CHANGED
@@ -4,9 +4,11 @@
4
4
 
5
5
  - Node 22.18+
6
6
  - npm 10.2+
7
- - Chrome or Chromium
8
7
  - G++
9
8
 
9
+ Chromium is downloaded by Puppeteer during `npm install`: it is used by the unit tests (through Playwright)
10
+ and by the e2e tests (Cypress).
11
+
10
12
  ## Developing using the Dev Tool
11
13
 
12
14
  Since version `8`, `prestations-ng` has a standalone application that allows
@@ -218,6 +220,43 @@ export NODE_OPTIONS=--max_old_space_size=4096
218
220
 
219
221
  Use at your own risks.
220
222
 
223
+ ## Unit testing
224
+
225
+ The unit tests run with [Vitest](https://vitest.dev/) through the `@angular/build:unit-test` builder of the
226
+ Angular CLI, in the browser mode of Vitest: [Playwright](https://playwright.dev/) drives the headless Chromium
227
+ downloaded by Puppeteer (another browser can be used by setting the `CHROME_BIN` environment variable).
228
+ Neither the library nor the Dev Tool uses zone.js: the tests are **zoneless** too.
229
+
230
+ | Command | Tests |
231
+ | ------------------------- | ---------------------------------------------------------------- |
232
+ | `npm run test:lib` | library (`projects/prestations-ng/src/**/*.spec.ts`) |
233
+ | `npm run test:devtool` | Dev Tool (`src/**/*.spec.ts`) |
234
+ | `npm run test:schematics` | schematics, in Node.js (`projects/prestations-ng/schematics`) |
235
+ | `npm run test:eslint` | ESLint plugin, in Node.js (`projects/prestations-ng/src/eslint`) |
236
+ | `npm test` | builds the library, then runs all of the above |
237
+
238
+ - `ng test prestations-ng` without `--no-watch` runs the tests in watch mode.
239
+ - Only some specs: `ng test prestations-ng --no-watch --include=foehn-table` (folder or file, relative to
240
+ `projects/prestations-ng/src`), and `--filter="FoehnTableComponent"` to filter by test name.
241
+ - The configuration is in `angular.json` (`test` targets), in the `vitest-base.config.mts` file of each project and in
242
+ the `vitest-base.config.mts` file they share, at the root.
243
+ The coverage of the library is written in `projects/prestations-ng/coverage` (read by SonarQube).
244
+ - The configuration files of the tools (`.mjs`/`.mts`, e.g. `eslint.config.mjs`, `vitest-base.config.*`) are listed in
245
+ `tsconfig.config.json`, referenced by `tsconfig.json`: the IDE checks them with the options of the project instead of
246
+ its default ones. Add any new configuration file to its `files`.
247
+
248
+ ### Writing zoneless tests
249
+
250
+ - Use `vi` (`vi.spyOn()`, `vi.fn()`...), the Jest-like API of Vitest. Unlike `spyOn()` in Jasmine,
251
+ `vi.spyOn()` calls the original method: add `.mockReturnValue(...)` or `.mockImplementation(...)` to stub it.
252
+ The spies are restored before each test.
253
+ - `fakeAsync()`, `tick()`, `flush()` and `waitForAsync()` require zone.js: use `async` tests with
254
+ `await fixture.whenStable()`, or `vi.useFakeTimers()` and `await vi.advanceTimersByTimeAsync(ms)`.
255
+ - `fixture.whenStable()` waits for the change detection and the Angular pending tasks (HTTP requests...), not
256
+ for the timers.
257
+ - Without zone.js, `fixture.detectChanges()` only refreshes the views that have been notified. A test (or a test
258
+ host component) changing the state of a component must use signals, as the components do.
259
+
221
260
  ## E2E testing
222
261
 
223
262
  `Protractor` is no longer included in new Angular projects as of Angular 12.
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,9 @@
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)
22
27
  - [Le routage de vos pages](good-practices#le_routage_de_vos_pages)
23
28
  - [Formulaire simple](good-practices#formulaire_simple)
24
29
  - [Formulaire complexe (sous-routes)](good-practices#formulaire_complexe__sous-routes_)
@@ -151,8 +156,7 @@ Exemple :
151
156
  <foehn-modal
152
157
  modalHeaderText="Modal dialog"
153
158
  modalSize="modal-lg"
154
- [isModalVisible]="isModalVisible"
155
- (isModalVisibleChange)="updateVisibilityStatus($event)"
159
+ [(isModalVisible)]="isModalVisible"
156
160
  [modalTriggerHtmlElement]="modalTrigger"
157
161
  >
158
162
  <!-- on utilise le déclencheur -->
@@ -160,6 +164,15 @@ Exemple :
160
164
  </foehn-modal>
161
165
  ```
162
166
 
167
+ ```ts
168
+ // Un signal : la modale s'affiche et se masque aussi sans zone.js
169
+ readonly isModalVisible = signal(false);
170
+
171
+ openModal(): void {
172
+ this.isModalVisible.set(true);
173
+ }
174
+ ```
175
+
163
176
  ### La balise `<label>`
164
177
 
165
178
  > ⚠️ Tous nos composants `foehn-input-*` disposent d'une balise label ([`<label>`](https://developer.mozilla.org/fr/docs/Web/HTML/Element/label))
@@ -383,6 +396,97 @@ export class PageOneComponent
383
396
  </div>
384
397
  ```
385
398
 
399
+ ## Application zoneless
400
+
401
+ > ℹ️ Depuis la v19.2, prestations-ng n'a plus besoin de zone.js et nous vous recommandons de rendre
402
+ > votre application zoneless (`provideZonelessChangeDetection()`, voir
403
+ > [UPGRADING_V19.md](UPGRADING_V19.md#zoneless-optional-but-recommended)).
404
+ >
405
+ > Sans zone.js, Angular ne rafraîchit un composant que lorsqu'il est prévenu d'un changement :
406
+ >
407
+ > - un listener de son template a été appelé (`(click)`, `(modelChange)`, `(userInput)`, ...), y
408
+ > compris les double-bindings `[(model)]`
409
+ > - un signal lu par son template a changé (`signal()`, `computed()`, `input()`, `model()`,
410
+ > `toSignal()`, ...)
411
+ > - `ChangeDetectorRef.markForCheck()` a été appelé, ce que fait le pipe `async` lorsqu'un observable
412
+ > émet
413
+
414
+ ### Afficher un état asynchrone
415
+
416
+ > ⚠️ Un champ modifié ailleurs (dans un `subscribe()`, un `setTimeout()`, une `Promise`, un
417
+ > callback, ...) n'est plus affiché tant que rien d'autre ne rafraîchit le composant. Tenez-le dans
418
+ > un **signal**.
419
+
420
+ Ce qu'il ne faut **pas** faire :
421
+
422
+ ```ts
423
+ isLoading = true;
424
+
425
+ ngOnInit(): void {
426
+ this.myService.load().subscribe(() => {
427
+ this.isLoading = false; // pas affiché sans zone.js
428
+ });
429
+ }
430
+ ```
431
+
432
+ Ce qu'il **faut** faire :
433
+
434
+ ```ts
435
+ readonly isLoading = signal(true);
436
+
437
+ ngOnInit(): void {
438
+ this.myService.load().subscribe(() => {
439
+ this.isLoading.set(false);
440
+ });
441
+ }
442
+ ```
443
+
444
+ ```html
445
+ @if (isLoading()) {
446
+ <p>{{ 'page.loading' | fromDictionary }}</p>
447
+ }
448
+ ```
449
+
450
+ > 💡 Pour un observable affiché tel quel, `toSignal(this.myService.load())` ou le pipe `async` font
451
+ > le travail pour vous.
452
+
453
+ Dans vos pages (`AbstractPageComponent`), prestations-ng rafraîchit la page lorsqu'il charge `form`
454
+ et `reference`. Si vous modifiez vous-même le formulaire dans un callback asynchrone, appelez
455
+ `markForCheck()` ensuite :
456
+
457
+ ```ts
458
+ private readonly changeDetectorRef = inject(ChangeDetectorRef);
459
+
460
+ ngOnInit(): void {
461
+ super.ngOnInit();
462
+ this.sessionInfoWithApplicationService.prefillForm((info) => {
463
+ this.form.email = info?.email ?? null;
464
+ this.changeDetectorRef.markForCheck();
465
+ });
466
+ }
467
+ ```
468
+
469
+ ### Agir sur le DOM après le rendu
470
+
471
+ > ⚠️ `NgZone` (`onStable`, `onMicrotaskEmpty`, `run()`, ...) ne fonctionne plus sans zone.js, et un
472
+ > `setTimeout()` n'attend pas le rendu. Pour agir sur le DOM rendu (focus, scroll, ...), utilisez
473
+ > `afterNextRender()`.
474
+
475
+ ```ts
476
+ readonly detailsVisible = signal(false);
477
+ readonly detailsField = viewChild<ElementRef<HTMLElement>>('detailsField');
478
+
479
+ private readonly injector = inject(Injector);
480
+
481
+ showDetails(): void {
482
+ this.detailsVisible.set(true);
483
+ // Le champ n'existe qu'une fois le détail rendu
484
+ afterNextRender(() => this.detailsField()?.nativeElement.focus(), {
485
+ injector: this.injector,
486
+ });
487
+ }
488
+ ```
489
+
386
490
  ## Le routage de vos pages
387
491
 
388
492
  ### Formulaire simple
@@ -541,14 +645,14 @@ Votre composant doit :
541
645
  1. `selector` devrait commencer par `"app-..."` pour éviter d'éventuels conflits de nommage
542
646
  2. avoir un `providers`
543
647
  3. étendre `FoehnInputComponent` et vous devez lui préciser votre modèle de données
544
- 4. répartir les attributs de votre modèle de données dans des variables publiques utilisées dans
648
+ 4. répartir les attributs de votre modèle de données dans des **signaux** publics utilisés dans
545
649
  votre HTML
546
650
  5. implémenter une fonction `update` (ici ce sera `updateDate`) qui fera appel à `updateNgModel` pour
547
651
  maintenir la synchronisation de votre modèle de données avec votre page
548
652
  6. implémenter une fonction `handleUserInput` qui fera appel à `handleChange` pour continuer à
549
653
  notifier le parent des changements de comportement de votre composant
550
654
  7. implémenter une fonction `onModelChange` qui vous permettra d'initialiser correctement les
551
- variables publiques correspondant à votre modèle
655
+ signaux correspondant à votre modèle
552
656
  8. implémenter une fonction `getValidValue` afin de reconstruire votre modèle de données avant de le
553
657
  retransmettre à votre page et retourner `null` si tous les attributs de votre objet sont vides (ça
554
658
  c'est pour que Java soit content)
@@ -558,11 +662,14 @@ Votre composant doit :
558
662
  > - **Pas de `multi: true`** dans le `providers` (retiré depuis la migration standalone).
559
663
  > - Les `imports` du composant se déclarent **directement** dans le décorateur `@Component`. Chaque
560
664
  > composant charge ce dont il a besoin (voir `foehn-input-date` dans ce projet).
665
+ > - L'état affiché par le composant est tenu dans des **signaux** (point 4) : `onModelChange` est
666
+ > appelé lorsque le modèle change, parfois en dehors d'un événement du template, et un simple champ
667
+ > ne serait alors pas affiché sans zone.js.
561
668
 
562
669
  `input-date.component.ts`
563
670
 
564
671
  ```ts
565
- import { Component, forwardRef } from '@angular/core';
672
+ import { Component, forwardRef, signal } from '@angular/core';
566
673
 
567
674
  import { FoehnInputComponent } from '../foehn-input/foehn-input.component';
568
675
  import { FoehnInputNumberComponent } from '../foehn-input/foehn-input-number.component';
@@ -591,10 +698,10 @@ import { SdkDictionaryPipe } from '../sdk-dictionary/sdk-dictionary.pipe';
591
698
  export class InputDateComponent extends FoehnInputComponent<number[]> {
592
699
  // point (3) : extends FoehnInputComponent<number[]>
593
700
 
594
- // point (4) : les attributs de votre modèle répartis en variables publiques
595
- day: string;
596
- month: string;
597
- year: string;
701
+ // point (4) : les attributs de votre modèle répartis en signaux publics
702
+ readonly day = signal<string | null>(null);
703
+ readonly month = signal<string | null>(null);
704
+ readonly year = signal<string | null>(null);
598
705
 
599
706
  // point (5)
600
707
  updateDate(): void {
@@ -613,24 +720,24 @@ export class InputDateComponent extends FoehnInputComponent<number[]> {
613
720
  }
614
721
 
615
722
  // point (7)
616
- override onModelChange(value: number[]): void {
723
+ override onModelChange(value: number[] | null): void {
617
724
  if (value && value.length > 2) {
618
- this.year = this.toString(value[0]);
619
- this.month = this.toString(value[1]);
620
- this.day = this.toString(value[2]);
725
+ this.year.set(this.toString(value[0]));
726
+ this.month.set(this.toString(value[1]));
727
+ this.day.set(this.toString(value[2]));
621
728
  } else if (!value) {
622
- this.year = null;
623
- this.month = null;
624
- this.day = null;
729
+ this.year.set(null);
730
+ this.month.set(null);
731
+ this.day.set(null);
625
732
  }
626
733
  }
627
734
 
628
735
  // point (8)
629
736
  getValidValue(): number[] | null | undefined {
630
737
  if (
631
- this.isEmpty(this.day) &&
632
- this.isEmpty(this.month) &&
633
- this.isEmpty(this.year)
738
+ this.isEmpty(this.day()) &&
739
+ this.isEmpty(this.month()) &&
740
+ this.isEmpty(this.year())
634
741
  ) {
635
742
  if (this.model_ !== undefined) {
636
743
  return null;
@@ -640,9 +747,9 @@ export class InputDateComponent extends FoehnInputComponent<number[]> {
640
747
  }
641
748
 
642
749
  return [
643
- this.toNumber(this.year),
644
- this.toNumber(this.month),
645
- this.toNumber(this.day),
750
+ this.toNumber(this.year()),
751
+ this.toNumber(this.month()),
752
+ this.toNumber(this.day()),
646
753
  ];
647
754
  }
648
755
 
@@ -655,7 +762,8 @@ Chaque `foehn-input-*` utilisé dans votre vue doit :
655
762
  1. avoir un `id` qui concatène l'appel à la fonction `buildId()` et le nom de l'attribut de votre objet
656
763
  2. avoir un `name` qui concatène l'input **signal** `name()` et le nom de l'attribut de votre objet
657
764
  3. avoir un `label`
658
- 4. utiliser votre variable publique comme model en double-binding (banana in the box)
765
+ 4. utiliser votre signal public comme model en double-binding (banana in the box) : `[(model)]="day"`
766
+ lit et écrit le signal (sans les parenthèses)
659
767
  5. faire appel à `updateDate()` lors du déclenchement de l'événement `(modelChange)`
660
768
  6. faire appel à `handleUserInput()` lors du déclenchement de l'événement `(userInput)`
661
769
 
@@ -955,11 +1063,11 @@ devra lui aussi remonter l'événement.
955
1063
  }
956
1064
 
957
1065
  <!-- (1) votre boucle @for + track (identifiant unique) -->
958
- @for (vehicule of vehicules; track vehicule.id; let i = $index) {
1066
+ @for (vehicule of vehicules(); track vehicule.id; let i = $index) {
959
1067
  <!-- (2) votre composant réutilisable -->
960
1068
  <app-vehicule-item
961
1069
  [name]="name() + '[' + i + ']'"
962
- [(model)]="vehicule"
1070
+ [model]="vehicule"
963
1071
  (modelChange)="updateVehiculesList($event, i)"
964
1072
  (userInput)="handleUserInput($event, i)"
965
1073
  (removedVehicule)="handleRemovedVehicule(i)"
@@ -974,6 +1082,9 @@ devra lui aussi remonter l'événement.
974
1082
 
975
1083
  > 💡 Le `name` est un input signal : `[name]="name() + '[' + i + ']'"` (et non plus
976
1084
  > `[name]="name + '[' + i + ']'"`).
1085
+ >
1086
+ > 💡 La variable d'une boucle `@for` ne peut pas être modifiée : on passe l'élément avec `[model]` et
1087
+ > on remplace l'élément de la liste dans `updateVehiculesList()` (appelée par `(modelChange)`).
977
1088
 
978
1089
  `vehicules-list.component.ts` remonte à son tour l'événement (1) au parent (`AbstractPageComponent`)
979
1090
  ainsi que supprimer l'élément de la liste (2). C'est aussi ici que vous pouvez gérer l'ajout d'un
@@ -998,15 +1109,29 @@ ainsi que supprimer l'élément de la liste (2). C'est aussi ici que vous pouvez
998
1109
  export class VehiculesListComponent extends FoehnInputComponent<Vehicule[]> {
999
1110
  readonly removedVehicule = output<void>(); // (1) événement à remonter au parent
1000
1111
 
1001
- vehicules: Vehicule[] = []; // Tableau contenant vos éléments répétables
1112
+ // Signal contenant vos éléments répétables : il est remplacé, jamais modifié sur place
1113
+ readonly vehicules = signal<Vehicule[]>([]);
1114
+
1115
+ updateVehiculesList(vehicule: Vehicule, index: number): void {
1116
+ this.vehicules.update((vehicules) =>
1117
+ vehicules.map((item, i) => (i === index ? vehicule : item)),
1118
+ );
1119
+ this.getValidValueAndUpdateNgModel(); // Mise à jour de votre modèle
1120
+ }
1002
1121
 
1003
1122
  handleRemovedVehicule(index: number): void {
1004
1123
  // (2) votre fonction de suppression de l'élément ainsi que la transmission de l'évènement
1005
- this.vehicules.splice(index, 1); // suppression de l'élément
1124
+ this.vehicules.update((vehicules) =>
1125
+ vehicules.filter((_vehicule, i) => i !== index),
1126
+ ); // suppression de l'élément
1006
1127
  this.getValidValueAndUpdateNgModel(); // Mise à jour de votre modèle
1007
1128
  this.removedVehicule.emit(); // (1) on remonte l'évènement
1008
1129
  }
1009
1130
 
1131
+ override onModelChange(value: Vehicule[] | null): void {
1132
+ this.vehicules.set(value ?? []);
1133
+ }
1134
+
1010
1135
  addVehicule(): void {
1011
1136
  // (3) Gestion de l'ajout d'un élément
1012
1137
  // ...code
@@ -1217,7 +1342,9 @@ export class ... extends AbstractPageComponent<YOUR_MODEL> implements OnInit {
1217
1342
  save(): void {
1218
1343
  // Activer l'affichage des erreurs
1219
1344
  this._validationHandlerService.shouldDisplayErrors(true);
1220
- // Appel au back pour faire votre validation
1345
+ // Appel au back pour faire votre validation. Le callback ne modifie aucun champ de la page :
1346
+ // les services de prestations-ng (growl, erreurs) rafraîchissent eux-mêmes l'affichage, aussi
1347
+ // sans zone.js. Un état propre à la page modifié ici devrait être un signal.
1221
1348
  this.httpClient
1222
1349
  .post<PostResponse>('api_url_ici', votre_objet)
1223
1350
  .subscribe((response: PostResponse) => {
package/UPGRADING_V19.md CHANGED
@@ -1485,6 +1485,136 @@ getListItemsDescription(): FoehnListItemDescription<MyItem>[] {
1485
1485
 
1486
1486
  The same applies to the function passed to `getListItemTitle`, which now receives a `MyItem`.
1487
1487
 
1488
+ ### `core-js` is no longer needed (since 19.2.0)
1489
+
1490
+ prestations-ng no longer imports `core-js` and no longer has it as a peer dependency. If your project does not
1491
+ import `core-js` itself, uninstall it:
1492
+
1493
+ ```bash
1494
+ npm uninstall core-js
1495
+ ```
1496
+
1497
+ Then, in your `angular.json` file, remove `"core-js/modules/es.array.includes"` from `"allowedCommonJsDependencies"`.
1498
+ The CommonJS dependencies of prestations-ng are now `"iban"` and `"dayjs"` (which also covers `dayjs/plugin/*` and
1499
+ `dayjs/locale/*`):
1500
+
1501
+ ```json
1502
+ "allowedCommonJsDependencies": ["iban", "dayjs"]
1503
+ ```
1504
+
1505
+ ### Zoneless (optional but recommended)
1506
+
1507
+ Since v19.2, prestations-ng does not need zone.js anymore: the state of its components is held by signals, which
1508
+ tell Angular when a component has to be refreshed. prestations-ng keeps working in an application using zone.js,
1509
+ so this step is optional, but a zoneless application is lighter, faster and easier to debug. See the
1510
+ [Angular zoneless guide](https://angular.dev/guide/zoneless).
1511
+
1512
+ In any case, if your code reads some fields of the prestations-ng components (e.g. through `viewChild()` or in
1513
+ your tests), check the fields that are now signals in the [CHANGELOG](CHANGELOG.md#1920---should-be-aligned-with-prestations-be-192x).
1514
+
1515
+ #### Enable the zoneless change detection
1516
+
1517
+ Zoneless is the default since Angular 21: `ng update` added `provideZoneChangeDetection()` to keep zone.js in your
1518
+ application. Replace it in your `app.config.ts` (`providePrestationsNgCore()` already provides
1519
+ `provideBrowserGlobalErrorListeners()`, which reports to your `ErrorHandler` the errors zone.js used to catch):
1520
+
1521
+ ```diff
1522
+ export const appConfig: ApplicationConfig = {
1523
+ providers: [
1524
+ - provideZoneChangeDetection({ eventCoalescing: true }),
1525
+ + provideZonelessChangeDetection(),
1526
+ providePrestationsNgCore(),
1527
+ ...
1528
+ ],
1529
+ };
1530
+ ```
1531
+
1532
+ Remove `zone.js` and `zone.js/testing` from the `polyfills` of the `build` and `test` targets in your
1533
+ `angular.json` (and from your `polyfills.ts` file if you still have one): without them, the `TestBed` is zoneless
1534
+ too.
1535
+
1536
+ ```diff
1537
+ "polyfills": [
1538
+ - "zone.js"
1539
+ ],
1540
+ ```
1541
+
1542
+ then uninstall zone.js:
1543
+
1544
+ ```bash
1545
+ npm uninstall zone.js
1546
+ ```
1547
+
1548
+ #### Check your components
1549
+
1550
+ Without zone.js, Angular refreshes a component only when it is notified that something changed:
1551
+
1552
+ - a listener of its template has been called (`(click)`, `(modelChange)`, `(userInput)`...), including the
1553
+ `[(model)]` two-way bindings
1554
+ - a signal read by its template has changed (`signal()`, `computed()`, `input()`, `model()`, `toSignal()`...)
1555
+ - `ChangeDetectorRef.markForCheck()` has been called, which is what the `async` pipe does when an observable emits
1556
+
1557
+ A field modified elsewhere (in a `subscribe()`, a `setTimeout()`, a `Promise`, a callback...) is not displayed
1558
+ anymore until something else refreshes the component. Hold it in a signal (see
1559
+ [INTRODUCTION_ANGULAR_SIGNALS.md](INTRODUCTION_ANGULAR_SIGNALS.md)):
1560
+
1561
+ ```diff
1562
+ - isLoading = true;
1563
+ + readonly isLoading = signal(true);
1564
+
1565
+ ngOnInit(): void {
1566
+ this.myService.load().subscribe(() => {
1567
+ - this.isLoading = false;
1568
+ + this.isLoading.set(false);
1569
+ });
1570
+ }
1571
+ ```
1572
+
1573
+ ```diff
1574
+ - @if (isLoading) {
1575
+ + @if (isLoading()) {
1576
+ ```
1577
+
1578
+ Pay attention to:
1579
+
1580
+ - your pages (`AbstractPageComponent` and its subclasses): prestations-ng refreshes them when it sets `form` and
1581
+ `reference`. But if you modify the form yourself in an asynchronous callback, e.g. the one given to
1582
+ `SessionInfoWithApplicationService.prefillForm()` or a `subscribe()` of your own service, call
1583
+ `markForCheck()` afterwards:
1584
+
1585
+ ```ts
1586
+ private readonly changeDetectorRef = inject(ChangeDetectorRef);
1587
+
1588
+ ngOnInit(): void {
1589
+ super.ngOnInit();
1590
+ this.sessionInfoWithApplicationService.prefillForm((info) => {
1591
+ this.form.email = info?.email ?? null;
1592
+ this.changeDetectorRef.markForCheck();
1593
+ });
1594
+ }
1595
+ ```
1596
+
1597
+ - `NgZone`: `onStable`, `onMicrotaskEmpty` and `onUnstable` never emit without zone.js, and `run()` or
1598
+ `runOutsideAngular()` are useless. To act on the rendered DOM (focus, scroll...), replace them, as well as the
1599
+ `setTimeout()` waiting for the view, by `afterNextRender()`. `AbstractMenuPageComponent.ngZone` is deprecated.
1600
+
1601
+ ```diff
1602
+ - this.ngZone.onStable.pipe(first()).subscribe(() => this.focusFirstField());
1603
+ + afterNextRender(() => this.focusFirstField(), { injector: this.injector });
1604
+ ```
1605
+
1606
+ #### Check your tests
1607
+
1608
+ - `fakeAsync()`, `tick()`, `flush()` and `waitForAsync()` need zone.js: write `async` tests, with
1609
+ `vi.useFakeTimers()` and `await vi.advanceTimersByTimeAsync(ms)` to control the timers (see
1610
+ [Migrate to vitest](#migrate-to-vitest)).
1611
+ - `fixture.detectChanges()` only refreshes the views which have been notified, then checks that nothing changed
1612
+ meanwhile: a field modified without notification makes the test fail with the error `NG0100`
1613
+ (`ExpressionChangedAfterItHasBeenCheckedError`). Fix the component (signal, `markForCheck()`) rather than the
1614
+ test.
1615
+ - `await fixture.whenStable()` waits for the change detection and for the pending tasks of the application
1616
+ (e.g. the debounced `userInput` of the prestations-ng inputs), not for the timers.
1617
+
1488
1618
  ### Last but not least, check if your application is working!!!
1489
1619
 
1490
1620
  #### Be sure your application gets a fresh start
@@ -8,6 +8,8 @@ import templateRules from './template-rules.mjs';
8
8
  *
9
9
  * Meant for the `extends` of a `**\/*.html` block. It brings its own parser and plugins,
10
10
  * so do not spread `angular.configs.templateRecommended` alongside it.
11
+ *
12
+ * @returns {import('typescript-eslint').ConfigArray}
11
13
  */
12
14
  const templateRecommended = (plugin) => [
13
15
  ...angular.configs.templateRecommended,
@@ -1,7 +1,11 @@
1
1
  import templateBase from './template-base.mjs';
2
2
 
3
- // Only the rules written by this library. `templateRecommended` includes it, alongside
4
- // the house style; use this layer directly to opt out of the latter.
3
+ /**
4
+ * Only the rules written by this library. `templateRecommended` includes it, alongside
5
+ * the house style; use this layer directly to opt out of the latter.
6
+ *
7
+ * @returns {import('typescript-eslint').ConfigArray}
8
+ */
5
9
  const templateRules = (plugin) => [
6
10
  templateBase(plugin),
7
11
  {