@dsivd/prestations-ng 19.2.0-beta.13 → 19.2.0-beta.15

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/GOOD_PRACTICES.md CHANGED
@@ -33,6 +33,7 @@
33
33
  - [Example with foehn-input-date of prestations-ng](good-practices#example_with_foehn-input-date_of_prestations-ng)
34
34
  - [Displaying the global validation of the component](good-practices#displaying_the_global_validation_of_the_component)
35
35
  - [Avoiding error display problems in a `@for` loop](good-practices#avoiding_error_display_problems_in_a__code__for__code__loop)
36
+ - [Types of the form fields](good-practices#types_of_the_form_fields)
36
37
  - [Form validation](good-practices#form_validation)
37
38
  - [Standard handling](good-practices#standard_handling)
38
39
  - [Manual handling](good-practices#manual_handling)
@@ -471,6 +472,9 @@ ngOnInit(): void {
471
472
  }
472
473
  ```
473
474
 
475
+ If a service fills the form for the page, the page must still be notified: let the service return an
476
+ `Observable` (or take a callback), and call `markForCheck()` in the page.
477
+
474
478
  ### Deriving a state from view queries
475
479
 
476
480
  > ⚠️ A field read by the template and modified in `ngAfterViewInit()`, `ngAfterContentInit()` or
@@ -552,8 +556,20 @@ it('should display the details', async () => {
552
556
  });
553
557
  ```
554
558
 
555
- `await fixture.whenStable()` also waits for the pending tasks, such as the HTTP requests: the test
556
- doesn't end before them (otherwise `NG0205: Injector has already been destroyed`).
559
+ `await fixture.whenStable()` also waits for the pending tasks of the application, such as the debounced
560
+ `userInput` of the prestations-ng inputs: the test doesn't end before them (otherwise
561
+ `NG0205: Injector has already been destroyed`). But it doesn't wait for the HTTP requests of a spec:
562
+ `provideHttpClientTesting()` keeps them pending until the test answers them. Answer them first with
563
+ `HttpTestingController`, then `await fixture.whenStable()`:
564
+
565
+ ```ts
566
+ it('should display the reference of the demand', async () => {
567
+ httpMock.expectOne('api/demande/1').flush({ reference: 'DEM-1' });
568
+ await fixture.whenStable();
569
+
570
+ expect(fixture.nativeElement.textContent).toContain('DEM-1');
571
+ });
572
+ ```
557
573
 
558
574
  Keep `fixture.detectChanges()` for:
559
575
 
@@ -1269,6 +1285,59 @@ export class DemandePageComponent
1269
1285
  }
1270
1286
  ```
1271
1287
 
1288
+ ## Types of the form fields
1289
+
1290
+ > ℹ️ `AbstractPageComponent` replaces `form` with the JSON sent by the backend, as is: at runtime, a field has
1291
+ > the type of the JSON written from the Java model, whatever its TypeScript type says.
1292
+
1293
+ Give the Java field the type matching the model of the component:
1294
+
1295
+ | Component | TypeScript model | Java field |
1296
+ | ------------------------------------------------------------------ | -------------------- | -------------------------------------- |
1297
+ | `foehn-input-text`, `foehn-input-textarea`, `foehn-input-email`... | `string` | `String` |
1298
+ | `foehn-input-time` | `string` | `String` with `@ValidStringTime` |
1299
+ | `foehn-input-date` | `(number \| null)[]` | `LenientLocalDate` |
1300
+ | `foehn-input-date-time` | `(number \| null)[]` | `LenientLocalDateTime` |
1301
+ | `foehn-boolean-checkbox`, `foehn-boolean-radio` | `boolean` | `Boolean` |
1302
+ | `foehn-input-number` | `string` | `Integer`, `BigDecimal`... (see below) |
1303
+
1304
+ > ⚠️ The model of `foehn-input-number` is a `string`: the component keeps the input as typed (decimal comma,
1305
+ > leading zeros, number of decimals). But the Java field is a number (`Integer`, `BigDecimal`), which is
1306
+ > written as a number in the JSON. The field holds `'0'` after the input of the user, and `0` once the form
1307
+ > comes back from the backend. The same happens with any component whose model is a `string` bound to a Java
1308
+ > field which isn't a `String`.
1309
+
1310
+ Never test such a field by its truthiness (`!!value`, `if (value)`, `value || ...`): `'0'` is true, `0` is
1311
+ false.
1312
+
1313
+ What **not** to do:
1314
+
1315
+ ```ts
1316
+ isAmountFilled(): boolean {
1317
+ // true after the input of '0', false once the form is loaded (0)
1318
+ return !!this.form?.amount;
1319
+ }
1320
+ ```
1321
+
1322
+ What **to** do:
1323
+
1324
+ ```ts
1325
+ isAmountFilled(): boolean {
1326
+ const amount = this.form?.amount;
1327
+ return amount !== null && amount !== undefined && amount !== '';
1328
+ }
1329
+
1330
+ getTotal(): number {
1331
+ // Number() converts '12.50' as well as 12.5
1332
+ return Number(this.form?.amount ?? 0) + Number(this.form?.fees ?? 0);
1333
+ }
1334
+ ```
1335
+
1336
+ > ⚠️ The values of the elements of `foehn-select` and `foehn-checkbox` must have the type of the Java field:
1337
+ > they are compared to the model with `===`. With `'1'` as value of an element and `1` sent by the backend
1338
+ > (`Integer`), no element is selected anymore once the form is loaded, and the component warns that the saved
1339
+ > value no longer exists in the list.
1340
+
1272
1341
  ## Form validation
1273
1342
 
1274
1343
  > ℹ️ When your form is saved in GESDEM, the `AbstractPageComponent` handles the
@@ -338,7 +338,7 @@ It's the most telling example to show the team: **the signal drives the user inp
338
338
 
339
339
  ### `rxResource()` — asynchronous calls driven by a signal, with loading/error state included
340
340
 
341
- An alternative to `toSignal()` when you also want state tracking (loading, error, reload) without coding it by hand. `rxResource()` accepts an `Observable` as data source, and is triggered again automatically when the signals the `request` depends on change.
341
+ An alternative to `toSignal()` when you also want state tracking (loading, error, reload) without coding it by hand. `rxResource()` accepts an `Observable` as data source, and is triggered again automatically when the signals read by its `params` function change.
342
342
 
343
343
  ```ts
344
344
  import { rxResource } from '@angular/core/rxjs-interop';
@@ -346,8 +346,8 @@ import { rxResource } from '@angular/core/rxjs-interop';
346
346
  readonly userId = input.required<string>();
347
347
 
348
348
  readonly userResource = rxResource({
349
- request: () => this.userId(),
350
- loader: ({ request: id }) => this.userService.getUser(id), // Observable<User>
349
+ params: () => this.userId(),
350
+ stream: ({ params: id }) => this.userService.getUser(id), // Observable<User>
351
351
  });
352
352
 
353
353
  // in the template: userResource.value(), userResource.isLoading(), userResource.error()
@@ -373,8 +373,8 @@ ngOnInit(): void {
373
373
  ```ts
374
374
  // 🟢 After: rxResource(), loading/error included, automatic re-fetch if userId() changes
375
375
  readonly userResource = rxResource({
376
- request: () => this.userId(),
377
- loader: ({ request: id }) => this.userService.getUser(id),
376
+ params: () => this.userId(),
377
+ stream: ({ params: id }) => this.userService.getUser(id),
378
378
  });
379
379
  // userResource.value(), userResource.isLoading(), userResource.error()
380
380
  ```
@@ -423,6 +423,50 @@ The `async` pipe stays fully legitimate when the `Observable` is consumed **only
423
423
 
424
424
  It also applies between layers: a service can compose entirely in RxJS (`debounceTime`, `retry`, `combineLatest`...) and stay testable independently of any component — it's the consuming component, "at the edge", that converts to a signal when displaying.
425
425
 
426
+ ### And the `BehaviorSubject`s?
427
+
428
+ > 🎤 a `BehaviorSubject` holds a current value like a signal, but it is also a stream: we don't convert them systematically, we look at what it represents and who consumes it.
429
+
430
+ It becomes a `signal()` when it is a **private state**, written with `next()` and read with `getValue()` or `| async`, without any operator behind it:
431
+
432
+ ```ts
433
+ // 🔴 A state kept in a BehaviorSubject
434
+ private readonly selectedTab = new BehaviorSubject<Tab>('summary');
435
+
436
+ // 🟢 The same state in a signal: readable in a computed(), no subscription
437
+ private readonly selectedTab = signal<Tab>('summary');
438
+ ```
439
+
440
+ It stays in RxJS when:
441
+
442
+ - it is the **source of an RxJS chain** (`switchMap` to an HTTP call, `debounceTime`, `combineLatest`...): as a signal, it would have to go back through `toObservable()`, which emits later (through an `effect()`) and only the last value of several writes in a row. A useless round trip, and the timing changes
443
+ - it carries **events**, not a state: a signal ignores a value equal to the current one (`Object.is`) and merges the writes in a row
444
+ - it is part of the **public API** of a library or a shared service: replacing the `Observable` breaks its consumers. If they need a signal, add one next to it (`toSignal()` in the service) rather than replacing it
445
+ - some code relies on the **synchronous notification**: `subscribe()` is called at each `next()`, right away, whereas an `effect()` runs later, once for several writes
446
+
447
+ ```ts
448
+ // 🔴 An event turned into a signal: the second reload triggers nothing
449
+ private readonly reloadTrigger = signal(true);
450
+
451
+ reload(): void {
452
+ this.reloadTrigger.set(true); // same value as before: no notification
453
+ }
454
+
455
+ // 🟢 An event stays a stream: each next() counts
456
+ private readonly reloadTrigger = new Subject<void>();
457
+
458
+ readonly alerts$ = this.reloadTrigger.pipe(
459
+ startWith(undefined),
460
+ switchMap(() => this.alertService.getAlerts()),
461
+ );
462
+
463
+ reload(): void {
464
+ this.reloadTrigger.next();
465
+ }
466
+ ```
467
+
468
+ > ⚠️ In a zoneless application, the `BehaviorSubject` itself is not the problem: a `subscribe()` writing a field read by the template, without notifying Angular, is. Fix the consumer with `toSignal()` or `| async` (see "Displaying an asynchronous state" in [GOOD_PRACTICES.md](GOOD_PRACTICES.md)).
469
+
426
470
  ---
427
471
 
428
472
  ## 7. Pitfall #1: mutation vs reassignment
@@ -501,3 +545,4 @@ export class ContactListComponent {
501
545
  - I never mutate a signal value in place anymore — always `.set()`/`.update()` with a new reference
502
546
  - I know when to use `untracked()` in an `effect()`
503
547
  - I don't try to replace all my RxJS with signals — I keep RxJS for composed async (HTTP, debounce, switchMap...) and I only convert to a signal at the edge, with `toSignal()`/`toObservable()`/`rxResource()`
548
+ - I only turn a `BehaviorSubject` into a signal when it is a private state without operators — the sources of RxJS chains, the events and the public APIs stay in RxJS
package/UPGRADING_V19.md CHANGED
@@ -1593,7 +1593,7 @@ then uninstall zone.js:
1593
1593
  npm uninstall zone.js
1594
1594
  ```
1595
1595
 
1596
- `zone.js` stays in your `package-lock.json` as an optional peer dependency of `@angular/core`: this is expected. Check
1596
+ `zone.js` can stay in your `package-lock.json` as an optional peer dependency of `@angular/core`: this is expected. Check
1597
1597
  instead that the build of your application no longer outputs a `polyfills` chunk (unless you still have other
1598
1598
  polyfills, e.g. `@angular/localize/init`).
1599
1599
 
@@ -1671,7 +1671,9 @@ Pay attention to:
1671
1671
  (`ExpressionChangedAfterItHasBeenCheckedError`). Fix the component (signal, `markForCheck()`) rather than the
1672
1672
  test.
1673
1673
  - `await fixture.whenStable()` waits for the change detection and for the pending tasks of the application
1674
- (e.g. the debounced `userInput` of the prestations-ng inputs), not for the timers.
1674
+ (e.g. the debounced `userInput` of the prestations-ng inputs), not for the timers, nor for the HTTP requests of a
1675
+ spec: `provideHttpClientTesting()` keeps them pending until the test answers them. Answer them first with
1676
+ `HttpTestingController` (`expectOne(...).flush(...)`), then `await fixture.whenStable()`.
1675
1677
 
1676
1678
  ### Last but not least, check if your application is working!!!
1677
1679
 
package/package.json CHANGED
@@ -39,7 +39,7 @@
39
39
  "ng-update": {
40
40
  "migrations": "./schematics/migration-collection.json"
41
41
  },
42
- "version": "19.2.0-beta.13",
42
+ "version": "19.2.0-beta.15",
43
43
  "module": "fesm2022/dsivd-prestations-ng.mjs",
44
44
  "typings": "types/dsivd-prestations-ng.d.ts",
45
45
  "exports": {