@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 +71 -2
- package/INTRODUCTION_ANGULAR_SIGNALS.md +50 -5
- package/UPGRADING_V19.md +4 -2
- package/package.json +1 -1
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
|
|
556
|
-
doesn't end before them (otherwise
|
|
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
|
|
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
|
-
|
|
350
|
-
|
|
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
|
-
|
|
377
|
-
|
|
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`
|
|
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.
|
|
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": {
|