@dsivd/prestations-ng 19.2.0-beta.11 → 19.2.0-beta.13
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 +5 -0
- package/CONTRIBUTING.md +1 -1
- package/GOOD_PRACTICES.md +443 -396
- package/INTRODUCTION_ANGULAR_SIGNALS.md +150 -150
- package/UPGRADING_V19.md +50 -5
- package/fesm2022/dsivd-prestations-ng.mjs +4 -1
- package/fesm2022/dsivd-prestations-ng.mjs.map +1 -1
- package/package.json +1 -1
- package/dsivd-prestations-ng-19.2.0-beta.11.tgz +0 -0
package/GOOD_PRACTICES.md
CHANGED
|
@@ -1,103 +1,104 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Technical usage (DEV)
|
|
2
2
|
|
|
3
|
-
>
|
|
4
|
-
>
|
|
5
|
-
>
|
|
6
|
-
> prestations-ng
|
|
7
|
-
>
|
|
3
|
+
> This guide describes the good practices to develop a prestation with `prestations-ng`.
|
|
4
|
+
> It has been updated for version **v19+**: **standalone** components, **signals** and the
|
|
5
|
+
> **`inject`** function (no more modules, no more constructor injection). Since **v19.2**,
|
|
6
|
+
> prestations-ng is **zoneless**: the examples hold the state of your components in signals,
|
|
7
|
+
> which works with or without zone.js (see [Zoneless application](good-practices#zoneless_application)).
|
|
8
8
|
>
|
|
9
|
-
> 📚
|
|
9
|
+
> 📚 To go further:
|
|
10
10
|
> [ANGULAR_SIGNALS.md](ANGULAR_SIGNALS.md) · [COMPONENTS.md](COMPONENTS.md) · [UPGRADING_V19.md](UPGRADING_V19.md)
|
|
11
11
|
|
|
12
|
-
##
|
|
12
|
+
## Table of contents
|
|
13
13
|
|
|
14
14
|
- [Documentation](good-practices#documentation)
|
|
15
|
-
- [
|
|
16
|
-
- [
|
|
17
|
-
- [
|
|
18
|
-
- [
|
|
19
|
-
- [
|
|
20
|
-
- [
|
|
21
|
-
- [
|
|
22
|
-
- [
|
|
23
|
-
- [
|
|
24
|
-
- [
|
|
25
|
-
- [
|
|
26
|
-
- [
|
|
27
|
-
- [
|
|
28
|
-
- [
|
|
29
|
-
|
|
30
|
-
- [
|
|
31
|
-
- [
|
|
32
|
-
|
|
33
|
-
- [
|
|
34
|
-
- [
|
|
35
|
-
- [
|
|
36
|
-
|
|
37
|
-
- [
|
|
15
|
+
- [Updating the library](good-practices#updating_the_library)
|
|
16
|
+
- [Accessibility](good-practices#accessibility)
|
|
17
|
+
- [HTML structure](good-practices#html_structure)
|
|
18
|
+
- [Focus](good-practices#focus)
|
|
19
|
+
- [The `<label>` tag](good-practices#the__code__lt_label_gt___code__tag)
|
|
20
|
+
- [The `<ul>` or `<ol>` tag](good-practices#the__code__lt_ul_gt___code__or__code__lt_ol_gt___code__tag)
|
|
21
|
+
- [The `<fieldset>` and `<legend>` tags](good-practices#the__code__lt_fieldset_gt___code__and__code__lt_legend_gt___code__tags)
|
|
22
|
+
- [Screen reader (show / hide)](good-practices#screen_reader__show___hide_)
|
|
23
|
+
- [A prestations-ng page](good-practices#a_prestations-ng_page)
|
|
24
|
+
- [Zoneless application](good-practices#zoneless_application)
|
|
25
|
+
- [Displaying an asynchronous state](good-practices#displaying_an_asynchronous_state)
|
|
26
|
+
- [Deriving a state from view queries](good-practices#deriving_a_state_from_view_queries)
|
|
27
|
+
- [Acting on the DOM after rendering](good-practices#acting_on_the_dom_after_rendering)
|
|
28
|
+
- [Testing a zoneless component](good-practices#testing_a_zoneless_component)
|
|
29
|
+
- [Routing your pages](good-practices#routing_your_pages)
|
|
30
|
+
- [Simple form](good-practices#simple_form)
|
|
31
|
+
- [Complex form (sub-routes)](good-practices#complex_form__sub-routes_)
|
|
32
|
+
- [Creating a reusable component](good-practices#creating_a_reusable_component)
|
|
33
|
+
- [Example with foehn-input-date of prestations-ng](good-practices#example_with_foehn-input-date_of_prestations-ng)
|
|
34
|
+
- [Displaying the global validation of the component](good-practices#displaying_the_global_validation_of_the_component)
|
|
35
|
+
- [Avoiding error display problems in a `@for` loop](good-practices#avoiding_error_display_problems_in_a__code__for__code__loop)
|
|
36
|
+
- [Form validation](good-practices#form_validation)
|
|
37
|
+
- [Standard handling](good-practices#standard_handling)
|
|
38
|
+
- [Manual handling](good-practices#manual_handling)
|
|
38
39
|
- [Back-end](good-practices#back-end)
|
|
39
40
|
- [Front-end](good-practices#front-end)
|
|
40
|
-
- [
|
|
41
|
-
- [
|
|
42
|
-
- [
|
|
41
|
+
- [Manual validation of a component](good-practices#manual_validation_of_a_component)
|
|
42
|
+
- [Hiding an error](good-practices#hiding_an_error)
|
|
43
|
+
- [Displaying an error](good-practices#displaying_an_error)
|
|
43
44
|
|
|
44
|
-
> ⚠️
|
|
45
|
-
>
|
|
45
|
+
> ⚠️ All your HTML pages must have a `<foehn-form>`, and it **can't be
|
|
46
|
+
> conditional** (no `@if` around `<foehn-form>`).
|
|
46
47
|
|
|
47
48
|
## Documentation
|
|
48
49
|
|
|
49
50
|
- [PrestaKit](https://www.vd.ch/toutes-les-autorites/departements/departement-des-infrastructures-et-des-ressources-humaines-dirh/direction-generale-du-numerique-et-des-systemes-dinformation-dgnsi/prestakit-sdk-pour-le-developpement-des-prestations-en-ligne/)
|
|
50
51
|
- [prestations-ng](https://dsi-vd.github.io/prestations-ng/)
|
|
51
|
-
- [prestations-ng (doc
|
|
52
|
-
- [foehn](https://dsi-vd.github.io/foehn-design-system/components/detail/formulaire-prestation--default.html) (
|
|
52
|
+
- [prestations-ng (generated doc)](https://dsi-vd.github.io/prestations-ng/generateddoc/)
|
|
53
|
+
- [foehn](https://dsi-vd.github.io/foehn-design-system/components/detail/formulaire-prestation--default.html) (Graphic charter)
|
|
53
54
|
- [CSS](https://www.w3schools.com/css/default.asp)
|
|
54
55
|
- [Bootstrap](https://getbootstrap.com/docs/5.3/layout/grid/)
|
|
55
56
|
- [Flex - Bootstrap](https://getbootstrap.com/docs/5.3/utilities/flex/)
|
|
56
57
|
- [Flex](https://css-tricks.com/snippets/css/a-guide-to-flexbox/)
|
|
57
58
|
- [HTML](https://www.w3schools.com/html/default.asp)
|
|
58
|
-
- [
|
|
59
|
+
- [Section elements](https://www.alsacreations.com/article/lire/1376-html5-section-article-nav-header-footer-aside.html) (in French)
|
|
59
60
|
- [Description list](https://www.w3schools.com/tags/tag_dl.asp)
|
|
60
61
|
- [RXJS](https://rxjs-dev.firebaseapp.com/)
|
|
61
62
|
|
|
62
|
-
##
|
|
63
|
+
## Updating the library
|
|
63
64
|
|
|
64
|
-
> ℹ️
|
|
65
|
-
>
|
|
66
|
-
>
|
|
65
|
+
> ℹ️ Keep your libraries up to date by subscribing on the [PrestaKit](https://www.vd.ch/toutes-les-autorites/departements/departement-des-infrastructures-et-des-ressources-humaines-dirh/direction-generale-du-numerique-et-des-systemes-dinformation-dgnsi/prestakit-sdk-pour-le-developpement-des-prestations-en-ligne/)
|
|
66
|
+
> page: you will be notified of the new releases. You will then benefit from the bug fixes,
|
|
67
|
+
> improvements and new features.
|
|
67
68
|
>
|
|
68
|
-
>
|
|
69
|
+
> Take the time to read the [changelog](CHANGELOG.md) of **prestations-ng** at each new release.
|
|
69
70
|
>
|
|
70
|
-
>
|
|
71
|
-
>
|
|
72
|
-
> [
|
|
71
|
+
> Don't hesitate to open JIRA tickets (PRESTAKIT-xxx) in [PrestaKit (formerly SDK Cyber)](https://issuetracker.etat-de-vaud.ch/outils/issuetracker/browse/PRESTAKIT)
|
|
72
|
+
> to report bugs, or even to suggest improvements in order to
|
|
73
|
+
> [contribute](CONTRIBUTING.md) to the development of prestations-ng.
|
|
73
74
|
|
|
74
|
-
|
|
75
|
+
Update the prestations-ng library (npm):
|
|
75
76
|
|
|
76
77
|
```bash
|
|
77
78
|
ng update @dsivd/prestations-ng
|
|
78
79
|
```
|
|
79
80
|
|
|
80
|
-
##
|
|
81
|
+
## Accessibility
|
|
81
82
|
|
|
82
|
-
> ℹ️
|
|
83
|
-
>
|
|
83
|
+
> ℹ️ When developing prestations for the State, we must do everything we can to make our web pages
|
|
84
|
+
> as accessible as possible to the general public.
|
|
84
85
|
>
|
|
85
|
-
> Prestations-ng
|
|
86
|
-
>
|
|
86
|
+
> Prestations-ng does a good part of the job for us, but it's up to the developer to make the
|
|
87
|
+
> necessary efforts so that the prestation stays accessible.
|
|
87
88
|
|
|
88
|
-
###
|
|
89
|
+
### HTML structure
|
|
89
90
|
|
|
90
|
-
> ⚠️
|
|
91
|
+
> ⚠️ We recommend to:
|
|
91
92
|
>
|
|
92
|
-
> -
|
|
93
|
-
> -
|
|
94
|
-
> -
|
|
95
|
-
> -
|
|
96
|
-
> -
|
|
97
|
-
> -
|
|
98
|
-
> -
|
|
93
|
+
> - have a single h1 tag (`<h1>`)
|
|
94
|
+
> - respect the hierarchy h1, h2, h3..
|
|
95
|
+
> - group your information in sections (`<section>`) if you have several h2 (`<h2>`)
|
|
96
|
+
> - use the Bootstrap CSS class `col-md-8` to define the width of your form
|
|
97
|
+
> - organize your text with paragraphs (`<p>`)
|
|
98
|
+
> - use the `<dl>`, `<dt>` and `<dd>` tags to display keys / values (see [foehn-recap-section](https://dsi-vd.github.io/prestations-ng/recap))
|
|
99
|
+
> - use [foehn-abbr](https://dsi-vd.github.io/prestations-ng/misc) for the abbreviations whenever possible, and the abbr tag (`<abbr>`) otherwise
|
|
99
100
|
|
|
100
|
-
|
|
101
|
+
What **not** to do:
|
|
101
102
|
|
|
102
103
|
```html
|
|
103
104
|
<h1>...</h1>
|
|
@@ -107,7 +108,7 @@ Ce qu'il ne faut **pas** faire :
|
|
|
107
108
|
... ... ...
|
|
108
109
|
```
|
|
109
110
|
|
|
110
|
-
|
|
111
|
+
What **to** do:
|
|
111
112
|
|
|
112
113
|
```html
|
|
113
114
|
<h1>...</h1>
|
|
@@ -122,27 +123,27 @@ Ce qu'il **faut** faire :
|
|
|
122
123
|
<section>...</section>
|
|
123
124
|
```
|
|
124
125
|
|
|
125
|
-
|
|
126
|
+
Some useful HTML attributes:
|
|
126
127
|
|
|
127
|
-
-
|
|
128
|
-
- `title="[
|
|
129
|
-
- `alt="[
|
|
130
|
-
-
|
|
131
|
-
- `title="[
|
|
128
|
+
- For the images (`<img>`)
|
|
129
|
+
- `title="[explicit text here]"`
|
|
130
|
+
- `alt="[explicit text here]"`
|
|
131
|
+
- For the links (`<a>`)
|
|
132
|
+
- `title="[explicit text here]"`
|
|
132
133
|
- `target="_blank"`
|
|
133
|
-
- [
|
|
134
|
+
- [Make the tags accessible](https://www.w3.org/TR/wai-aria-1.2/#intro_ria_accessibility)
|
|
134
135
|
- [`aria-*`](https://w3c.github.io/using-aria/#aria-states-and-properties-aria-attributes)
|
|
135
136
|
- [`role`](https://www.w3.org/WAI/PF/HTML/wiki/RoleAttribute)
|
|
136
137
|
|
|
137
|
-
###
|
|
138
|
+
### Focus
|
|
138
139
|
|
|
139
|
-
> ⚠️
|
|
140
|
-
>
|
|
140
|
+
> ⚠️ It's important that the focus in an HTML page is under control. If the focus is lost, the
|
|
141
|
+
> screen reader is lost too.
|
|
141
142
|
>
|
|
142
|
-
>
|
|
143
|
-
>
|
|
143
|
+
> If, for example, a modal is opened by clicking on a button, then when the modal is closed, the
|
|
144
|
+
> focus must be put back on the triggering button.
|
|
144
145
|
|
|
145
|
-
|
|
146
|
+
Example:
|
|
146
147
|
|
|
147
148
|
```html
|
|
148
149
|
<button
|
|
@@ -151,7 +152,7 @@ Exemple :
|
|
|
151
152
|
(click)="openModal()"
|
|
152
153
|
#modalTrigger
|
|
153
154
|
>
|
|
154
|
-
<!--
|
|
155
|
+
<!-- we declare the trigger of the modal -->
|
|
155
156
|
Open Modal
|
|
156
157
|
</button>
|
|
157
158
|
<foehn-modal
|
|
@@ -160,13 +161,13 @@ Exemple :
|
|
|
160
161
|
[(isModalVisible)]="isModalVisible"
|
|
161
162
|
[modalTriggerHtmlElement]="modalTrigger"
|
|
162
163
|
>
|
|
163
|
-
<!--
|
|
164
|
+
<!-- we use the trigger -->
|
|
164
165
|
<div>Here is the dialog</div>
|
|
165
166
|
</foehn-modal>
|
|
166
167
|
```
|
|
167
168
|
|
|
168
169
|
```ts
|
|
169
|
-
//
|
|
170
|
+
// A signal: the modal is also displayed and hidden without zone.js
|
|
170
171
|
readonly isModalVisible = signal(false);
|
|
171
172
|
|
|
172
173
|
openModal(): void {
|
|
@@ -174,105 +175,106 @@ openModal(): void {
|
|
|
174
175
|
}
|
|
175
176
|
```
|
|
176
177
|
|
|
177
|
-
###
|
|
178
|
+
### The `<label>` tag
|
|
178
179
|
|
|
179
|
-
> ⚠️
|
|
180
|
-
>
|
|
180
|
+
> ⚠️ All our `foehn-input-*` components have a label tag ([`<label>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/label))
|
|
181
|
+
> which describes the input field. It's important that it is filled with an explicit content.
|
|
181
182
|
>
|
|
182
|
-
>
|
|
183
|
-
>
|
|
184
|
-
>
|
|
183
|
+
> If for any reason the business doesn't want to display it, you must still fill it and add an
|
|
184
|
+
> `[isLabelSrOnly]="true"` attribute. The label won't be displayed, only read by the screen
|
|
185
|
+
> reader.
|
|
185
186
|
|
|
186
|
-
|
|
187
|
+
Example:
|
|
187
188
|
|
|
188
189
|
```html
|
|
189
190
|
<foehn-input-number
|
|
190
191
|
name="..."
|
|
191
192
|
[(model)]="..."
|
|
192
|
-
[label]="'
|
|
193
|
+
[label]="'The text read by the screen reader'"
|
|
193
194
|
[isLabelSrOnly]="true"
|
|
194
195
|
[required]="..."
|
|
195
196
|
[maxlength]="..."
|
|
196
197
|
/>
|
|
197
198
|
```
|
|
198
199
|
|
|
199
|
-
> 💡 `label`, `isLabelSrOnly`, `required`, ...
|
|
200
|
-
>
|
|
201
|
-
>
|
|
200
|
+
> 💡 `label`, `isLabelSrOnly`, `required`, ... are now **input signals** in the component.
|
|
201
|
+
> You keep feeding them from the parent template with a `[label]="..."` binding.
|
|
202
|
+
> Inside the component, you read them by calling them: `label()`, `required()`, ...
|
|
202
203
|
|
|
203
|
-
###
|
|
204
|
+
### The `<ul>` or `<ol>` tag
|
|
204
205
|
|
|
205
206
|
> ⚠️
|
|
206
207
|
>
|
|
207
|
-
> -
|
|
208
|
-
>
|
|
209
|
-
> [`<ol>`](https://developer.mozilla.org/
|
|
210
|
-
> [`<ul>`](https://developer.mozilla.org/
|
|
211
|
-
>
|
|
212
|
-
>
|
|
213
|
-
> [`<ol>`](https://developer.mozilla.org/
|
|
214
|
-
>
|
|
215
|
-
>
|
|
216
|
-
>
|
|
217
|
-
> [`<
|
|
218
|
-
>
|
|
219
|
-
>
|
|
208
|
+
> - The [`<ul>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ul) element must be used
|
|
209
|
+
> to group several items that have no order relationship. If you hesitate between
|
|
210
|
+
> [`<ol>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ol) and
|
|
211
|
+
> [`<ul>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ul), ask yourself whether
|
|
212
|
+
> changing the order of the items of the list has an impact: if moving an item changes the
|
|
213
|
+
> meaning, the order is important and
|
|
214
|
+
> [`<ol>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ol) must be used, otherwise
|
|
215
|
+
> the order doesn't matter and [`<ul>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ul)
|
|
216
|
+
> can be used.
|
|
217
|
+
> - If [`<ol>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ol) or
|
|
218
|
+
> [`<ul>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ul) only contains a single
|
|
219
|
+
> [`<li>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/li) element (not dynamic),
|
|
220
|
+
> ask yourself whether a list is necessary and whether a paragraph
|
|
221
|
+
> ([`<p>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/p)) wouldn't be a better
|
|
220
222
|
> option.
|
|
221
|
-
> -
|
|
222
|
-
>
|
|
223
|
-
> [`<ul>`](https://developer.mozilla.org/
|
|
223
|
+
> - **NEVER** put a tag other than [`<li>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/li)
|
|
224
|
+
> between two [`<ol>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ol) or
|
|
225
|
+
> [`<ul>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ul) tags.
|
|
224
226
|
|
|
225
|
-
|
|
227
|
+
Example:
|
|
226
228
|
|
|
227
229
|
```html
|
|
228
230
|
<ul>
|
|
229
|
-
<li>
|
|
231
|
+
<li>an artichoke</li>
|
|
230
232
|
<li>
|
|
231
|
-
|
|
232
|
-
<!--
|
|
233
|
+
The things for the cake
|
|
234
|
+
<!-- Note that </li> isn't there -->
|
|
233
235
|
<ul>
|
|
234
|
-
<li>
|
|
236
|
+
<li>three eggs</li>
|
|
235
237
|
<li>
|
|
236
|
-
|
|
237
|
-
<!--
|
|
238
|
+
The sponge cake
|
|
239
|
+
<!-- Here we open another list -->
|
|
238
240
|
<ul>
|
|
239
|
-
<li>100g
|
|
240
|
-
<li>
|
|
241
|
-
<li>150g
|
|
241
|
+
<li>100g of sugar</li>
|
|
242
|
+
<li>an egg</li>
|
|
243
|
+
<li>150g of flour</li>
|
|
242
244
|
</ul>
|
|
243
245
|
</li>
|
|
244
|
-
<!--
|
|
245
|
-
<li>200g
|
|
246
|
+
<!-- We close the most nested list -->
|
|
247
|
+
<li>200g of chocolate</li>
|
|
246
248
|
</ul>
|
|
247
|
-
<!--
|
|
249
|
+
<!-- We close the nested list with </li> -->
|
|
248
250
|
</li>
|
|
249
|
-
<li>
|
|
251
|
+
<li>Paper towels</li>
|
|
250
252
|
<li>
|
|
251
|
-
|
|
253
|
+
To do in this order for a nice dishwashing
|
|
252
254
|
<ol>
|
|
253
|
-
<li>
|
|
254
|
-
<li>
|
|
255
|
-
<li>
|
|
256
|
-
<li>
|
|
255
|
+
<li>Fill the dishwasher</li>
|
|
256
|
+
<li>Put a dishwasher tablet</li>
|
|
257
|
+
<li>Close the door of the dishwasher</li>
|
|
258
|
+
<li>Start the dishwasher</li>
|
|
257
259
|
</ol>
|
|
258
260
|
</li>
|
|
259
261
|
</ul>
|
|
260
262
|
```
|
|
261
263
|
|
|
262
|
-
###
|
|
264
|
+
### The `<fieldset>` and `<legend>` tags
|
|
263
265
|
|
|
264
|
-
> ⚠️
|
|
265
|
-
>
|
|
266
|
-
> ([`<label>`](https://developer.mozilla.org/
|
|
267
|
-
>
|
|
268
|
-
> ([`<legend>`](https://developer.mozilla.org/
|
|
266
|
+
> ⚠️ The HTML element [`<fieldset>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/fieldset) is
|
|
267
|
+
> used to group several interactive controls as well as labels
|
|
268
|
+
> ([`<label>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/label)) in an HTML form,
|
|
269
|
+
> and it must have a caption
|
|
270
|
+
> ([`<legend>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/legend)) describing this group.
|
|
269
271
|
>
|
|
270
|
-
>
|
|
271
|
-
>
|
|
272
|
-
> [`<fieldset>`](https://developer.mozilla.org/
|
|
273
|
-
>
|
|
272
|
+
> The HTML element [`<legend>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/legend) represents
|
|
273
|
+
> a caption for the content of its parent element
|
|
274
|
+
> [`<fieldset>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/fieldset). It can't be
|
|
275
|
+
> used on its own.
|
|
274
276
|
|
|
275
|
-
|
|
277
|
+
Example:
|
|
276
278
|
|
|
277
279
|
```html
|
|
278
280
|
<form>
|
|
@@ -291,45 +293,45 @@ Exemple :
|
|
|
291
293
|
</form>
|
|
292
294
|
```
|
|
293
295
|
|
|
294
|
-
###
|
|
296
|
+
### Screen reader (show / hide)
|
|
295
297
|
|
|
296
|
-
> ⚠️
|
|
297
|
-
>
|
|
298
|
-
>
|
|
298
|
+
> ⚠️ It's important to keep in mind that a screen reader goes through our HTML code. So we can
|
|
299
|
+
> decide at any time to explicitly show it content to read, or to hide content from it so as not to
|
|
300
|
+
> overload the information.
|
|
299
301
|
|
|
300
|
-
-
|
|
301
|
-
-
|
|
302
|
+
- Show only to the screen reader: use the CSS class `sr-only`
|
|
303
|
+
- Hide content from the screen reader: use the attribute `aria-hidden="true"`
|
|
302
304
|
|
|
303
|
-
|
|
305
|
+
Example:
|
|
304
306
|
|
|
305
307
|
```html
|
|
306
308
|
<a href="https://www.vd.ch">
|
|
307
309
|
<span class="sr-only">
|
|
308
|
-
<!--
|
|
309
|
-
|
|
310
|
+
<!-- Show only to the screen reader -->
|
|
311
|
+
Back to the home page
|
|
310
312
|
</span>
|
|
311
313
|
<img
|
|
312
314
|
aria-hidden="true"
|
|
313
315
|
class="img-fluid footer-logo"
|
|
314
|
-
src="[
|
|
316
|
+
src="[url_to_my_image]"
|
|
315
317
|
alt="Canton de Vaud"
|
|
316
318
|
/>
|
|
317
|
-
<!--
|
|
319
|
+
<!-- Hide only from the screen reader -->
|
|
318
320
|
</a>
|
|
319
321
|
```
|
|
320
322
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
+
The result of the example above is a link represented by an image for sighted people, and a link
|
|
324
|
+
with text for visually impaired people.
|
|
323
325
|
|
|
324
|
-
###
|
|
326
|
+
### A prestations-ng page
|
|
325
327
|
|
|
326
|
-
1.
|
|
327
|
-
2.
|
|
328
|
-
3.
|
|
329
|
-
4.
|
|
330
|
-
5.
|
|
331
|
-
6.
|
|
332
|
-
7.
|
|
328
|
+
1. Extend the abstract class `AbstractPageComponent`, specifying a model
|
|
329
|
+
2. Add an h1 title with `setPageTitle`
|
|
330
|
+
3. Implement the `newForm` function
|
|
331
|
+
4. Add the `<foehn-form>` in the page (⚠️ **it must not be conditional**)
|
|
332
|
+
5. The first heading tag of your page must be an h2 (`<h2>`)
|
|
333
|
+
6. Add `<foehn-navigation>`
|
|
334
|
+
7. If you have several pages, use the `<foehn-page-counter>` component
|
|
333
335
|
|
|
334
336
|
`page-one.component.ts`
|
|
335
337
|
|
|
@@ -337,12 +339,12 @@ avec du texte pour les non-voyants.
|
|
|
337
339
|
@Component({
|
|
338
340
|
templateUrl: './page-one.component.html',
|
|
339
341
|
imports: [
|
|
340
|
-
//
|
|
342
|
+
// each component imports what its template needs
|
|
341
343
|
FoehnFormComponent,
|
|
342
344
|
FoehnNavigationComponent,
|
|
343
345
|
FoehnPageCounterComponent,
|
|
344
346
|
SdkDictionaryPipe,
|
|
345
|
-
// ...
|
|
347
|
+
// ... your foehn-* components used in the template
|
|
346
348
|
],
|
|
347
349
|
})
|
|
348
350
|
export class PageOneComponent
|
|
@@ -351,7 +353,7 @@ export class PageOneComponent
|
|
|
351
353
|
{
|
|
352
354
|
ngOnInit(): void {
|
|
353
355
|
super.ngOnInit();
|
|
354
|
-
// point (2)
|
|
356
|
+
// point (2): adds the h1 title
|
|
355
357
|
this.setPageTitle(this._dictionaryService.getKeySync('page.one.title'));
|
|
356
358
|
}
|
|
357
359
|
|
|
@@ -362,28 +364,28 @@ export class PageOneComponent
|
|
|
362
364
|
}
|
|
363
365
|
```
|
|
364
366
|
|
|
365
|
-
> 💡
|
|
366
|
-
>
|
|
367
|
-
>
|
|
367
|
+
> 💡 No more `constructor`: the dependencies of `AbstractPageComponent` (including `_dictionaryService`)
|
|
368
|
+
> are injected with `inject()` in the abstract class. `activatedRoute` is therefore no longer a
|
|
369
|
+
> constructor parameter to pass.
|
|
368
370
|
|
|
369
371
|
`page-one.component.html`
|
|
370
372
|
|
|
371
373
|
```html
|
|
372
374
|
<div class="container mt-5">
|
|
373
375
|
<foehn-page-counter />
|
|
374
|
-
<!-- point (7)
|
|
376
|
+
<!-- point (7): if needed -->
|
|
375
377
|
|
|
376
|
-
<!-- point (4)
|
|
378
|
+
<!-- point (4): must always be present in the page and not conditional (no @if) -->
|
|
377
379
|
<foehn-form>
|
|
378
380
|
<div class="row">
|
|
379
381
|
<div class="col-md-8">
|
|
380
|
-
<!-- point (5)
|
|
382
|
+
<!-- point (5): always have an <h2> first in your page -->
|
|
381
383
|
<h2>
|
|
382
|
-
{{ '
|
|
384
|
+
{{ 'The_dictionary_key_of_your_H2_title_here' |
|
|
383
385
|
fromDictionary }}
|
|
384
386
|
</h2>
|
|
385
387
|
|
|
386
|
-
<!-- [
|
|
388
|
+
<!-- [YOUR FOEHN COMPONENTS HERE] -->
|
|
387
389
|
|
|
388
390
|
<foehn-navigation
|
|
389
391
|
id="navigation"
|
|
@@ -397,40 +399,40 @@ export class PageOneComponent
|
|
|
397
399
|
</div>
|
|
398
400
|
```
|
|
399
401
|
|
|
400
|
-
##
|
|
402
|
+
## Zoneless application
|
|
401
403
|
|
|
402
|
-
> ℹ️
|
|
403
|
-
>
|
|
404
|
+
> ℹ️ Since v19.2, prestations-ng no longer needs zone.js and we recommend that you make your
|
|
405
|
+
> application zoneless (`provideZonelessChangeDetection()`, see
|
|
404
406
|
> [UPGRADING_V19.md](UPGRADING_V19.md#zoneless-optional-but-recommended)).
|
|
405
407
|
>
|
|
406
|
-
>
|
|
408
|
+
> Without zone.js, Angular only refreshes a component when it is notified of a change:
|
|
407
409
|
>
|
|
408
|
-
> -
|
|
409
|
-
>
|
|
410
|
-
> -
|
|
410
|
+
> - a listener of its template has been called (`(click)`, `(modelChange)`, `(userInput)`, ...),
|
|
411
|
+
> including the `[(model)]` two-way bindings
|
|
412
|
+
> - a signal read by its template has changed (`signal()`, `computed()`, `input()`, `model()`,
|
|
411
413
|
> `toSignal()`, ...)
|
|
412
|
-
> - `ChangeDetectorRef.markForCheck()`
|
|
413
|
-
>
|
|
414
|
+
> - `ChangeDetectorRef.markForCheck()` has been called, which is what the `async` pipe does when an
|
|
415
|
+
> observable emits
|
|
414
416
|
|
|
415
|
-
###
|
|
417
|
+
### Displaying an asynchronous state
|
|
416
418
|
|
|
417
|
-
> ⚠️
|
|
418
|
-
> callback, ...)
|
|
419
|
-
>
|
|
419
|
+
> ⚠️ A field modified elsewhere (in a `subscribe()`, a `setTimeout()`, a `Promise`, a
|
|
420
|
+
> callback, ...) is no longer displayed until something else refreshes the component. Hold it in
|
|
421
|
+
> a **signal**.
|
|
420
422
|
|
|
421
|
-
|
|
423
|
+
What **not** to do:
|
|
422
424
|
|
|
423
425
|
```ts
|
|
424
426
|
isLoading = true;
|
|
425
427
|
|
|
426
428
|
ngOnInit(): void {
|
|
427
429
|
this.myService.load().subscribe(() => {
|
|
428
|
-
this.isLoading = false; //
|
|
430
|
+
this.isLoading = false; // not displayed without zone.js
|
|
429
431
|
});
|
|
430
432
|
}
|
|
431
433
|
```
|
|
432
434
|
|
|
433
|
-
|
|
435
|
+
What **to** do:
|
|
434
436
|
|
|
435
437
|
```ts
|
|
436
438
|
readonly isLoading = signal(true);
|
|
@@ -448,12 +450,12 @@ ngOnInit(): void {
|
|
|
448
450
|
}
|
|
449
451
|
```
|
|
450
452
|
|
|
451
|
-
> 💡
|
|
452
|
-
>
|
|
453
|
+
> 💡 For an observable displayed as is, `toSignal(this.myService.load())` or the `async` pipe do
|
|
454
|
+
> the job for you.
|
|
453
455
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
`markForCheck()`
|
|
456
|
+
In your pages (`AbstractPageComponent`), prestations-ng refreshes the page when it loads `form`
|
|
457
|
+
and `reference`. If you modify the form yourself in an asynchronous callback, call
|
|
458
|
+
`markForCheck()` afterwards:
|
|
457
459
|
|
|
458
460
|
```ts
|
|
459
461
|
private readonly changeDetectorRef = inject(ChangeDetectorRef);
|
|
@@ -461,16 +463,62 @@ private readonly changeDetectorRef = inject(ChangeDetectorRef);
|
|
|
461
463
|
ngOnInit(): void {
|
|
462
464
|
super.ngOnInit();
|
|
463
465
|
this.sessionInfoWithApplicationService.prefillForm((info) => {
|
|
464
|
-
this.form
|
|
465
|
-
|
|
466
|
+
if (this.form) {
|
|
467
|
+
this.form.email = info?.email ?? null;
|
|
468
|
+
this.changeDetectorRef.markForCheck();
|
|
469
|
+
}
|
|
466
470
|
});
|
|
467
471
|
}
|
|
468
472
|
```
|
|
469
473
|
|
|
470
|
-
###
|
|
474
|
+
### Deriving a state from view queries
|
|
475
|
+
|
|
476
|
+
> ⚠️ A field read by the template and modified in `ngAfterViewInit()`, `ngAfterContentInit()` or
|
|
477
|
+
> `ngAfterViewChecked()` is no longer displayed until something else refreshes the component (with
|
|
478
|
+
> zone.js, the next refresh displayed it, after an `NG0100` error in development).
|
|
479
|
+
> Compute it with `computed()` from `viewChild()` or `contentChild()`.
|
|
480
|
+
|
|
481
|
+
What **not** to do:
|
|
482
|
+
|
|
483
|
+
```ts
|
|
484
|
+
@ViewChild('referenceLink') referenceLink?: TemplateRef<unknown>;
|
|
485
|
+
columns: FoehnTableColumnConfiguration<Order>[] = [];
|
|
486
|
+
|
|
487
|
+
ngAfterViewInit(): void {
|
|
488
|
+
// not displayed without zone.js
|
|
489
|
+
this.columns = [{ id: 'reference', columnLabelKey: 'orders.reference', template: this.referenceLink }];
|
|
490
|
+
}
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
What **to** do:
|
|
494
|
+
|
|
495
|
+
```ts
|
|
496
|
+
readonly referenceLink = viewChild<TemplateRef<unknown>>('referenceLink');
|
|
497
|
+
readonly columns = computed<FoehnTableColumnConfiguration<Order>[]>(() => [
|
|
498
|
+
{ id: 'reference', columnLabelKey: 'orders.reference', template: this.referenceLink() },
|
|
499
|
+
]);
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
```html
|
|
503
|
+
<foehn-table
|
|
504
|
+
[name]="'orders'"
|
|
505
|
+
[columnsConfiguration]="columns()"
|
|
506
|
+
[model]="orders()"
|
|
507
|
+
></foehn-table>
|
|
508
|
+
|
|
509
|
+
<ng-template #referenceLink let-order="item">
|
|
510
|
+
<a [routerLink]="['/demande', order.id]">{{ order.reference }}</a>
|
|
511
|
+
</ng-template>
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
> 💡 The `computed()` follows the result of the `viewChild()`, including when the element appears or
|
|
515
|
+
> disappears in an `@if`: no need to hide the content until it is resolved. If the element must
|
|
516
|
+
> always exist, `viewChild.required()` guarantees it (error `NG0951` otherwise).
|
|
517
|
+
|
|
518
|
+
### Acting on the DOM after rendering
|
|
471
519
|
|
|
472
|
-
> ⚠️ `NgZone` (`onStable`, `onMicrotaskEmpty`, `run()`, ...)
|
|
473
|
-
> `setTimeout()`
|
|
520
|
+
> ⚠️ `NgZone` (`onStable`, `onMicrotaskEmpty`, `run()`, ...) no longer works without zone.js, and a
|
|
521
|
+
> `setTimeout()` doesn't wait for the rendering. To act on the rendered DOM (focus, scroll, ...), use
|
|
474
522
|
> `afterNextRender()`.
|
|
475
523
|
|
|
476
524
|
```ts
|
|
@@ -481,18 +529,18 @@ private readonly injector = inject(Injector);
|
|
|
481
529
|
|
|
482
530
|
showDetails(): void {
|
|
483
531
|
this.detailsVisible.set(true);
|
|
484
|
-
//
|
|
532
|
+
// The field only exists once the details are rendered
|
|
485
533
|
afterNextRender(() => this.detailsField()?.nativeElement.focus(), {
|
|
486
534
|
injector: this.injector,
|
|
487
535
|
});
|
|
488
536
|
}
|
|
489
537
|
```
|
|
490
538
|
|
|
491
|
-
###
|
|
539
|
+
### Testing a zoneless component
|
|
492
540
|
|
|
493
|
-
> ⚠️ `fixture.detectChanges()` **
|
|
494
|
-
> Angular (
|
|
495
|
-
>
|
|
541
|
+
> ⚠️ `fixture.detectChanges()` **forces** the refresh: a component that forgets to notify
|
|
542
|
+
> Angular (a field instead of a signal, a missing `markForCheck()`, ...) passes the test, but isn't
|
|
543
|
+
> refreshed in the application. Let Angular refresh the component, as in production, with
|
|
496
544
|
> `await fixture.whenStable()`.
|
|
497
545
|
|
|
498
546
|
```ts
|
|
@@ -504,22 +552,22 @@ it('should display the details', async () => {
|
|
|
504
552
|
});
|
|
505
553
|
```
|
|
506
554
|
|
|
507
|
-
`await fixture.whenStable()`
|
|
508
|
-
|
|
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`).
|
|
509
557
|
|
|
510
|
-
|
|
558
|
+
Keep `fixture.detectChanges()` for:
|
|
511
559
|
|
|
512
|
-
-
|
|
513
|
-
-
|
|
514
|
-
|
|
560
|
+
- checking an intermediate state, before the end of an asynchronous processing
|
|
561
|
+
- the tests with `vi.useFakeTimers()`: the refresh of Angular goes through timers, which must be
|
|
562
|
+
advanced (`await vi.advanceTimersByTimeAsync(...)`), otherwise `whenStable()` never ends
|
|
515
563
|
|
|
516
|
-
> 💡
|
|
517
|
-
> DOM (`input.dispatchEvent(new Event('change'))`, `button.click()`, ...)
|
|
518
|
-
>
|
|
564
|
+
> 💡 So that the test goes through the same path as the user, trigger the events in the
|
|
565
|
+
> DOM (`input.dispatchEvent(new Event('change'))`, `button.click()`, ...) rather than calling the
|
|
566
|
+
> methods of the component or of its services.
|
|
519
567
|
|
|
520
|
-
##
|
|
568
|
+
## Routing your pages
|
|
521
569
|
|
|
522
|
-
###
|
|
570
|
+
### Simple form
|
|
523
571
|
|
|
524
572
|
`app.routes.ts`
|
|
525
573
|
|
|
@@ -559,14 +607,14 @@ export const routes: Routes = [
|
|
|
559
607
|
];
|
|
560
608
|
```
|
|
561
609
|
|
|
562
|
-
###
|
|
610
|
+
### Complex form (sub-routes)
|
|
563
611
|
|
|
564
|
-
> ℹ️
|
|
565
|
-
>
|
|
612
|
+
> ℹ️ Prefer **sub-routes** (`*.routes.ts`) to group your routes and thus better structure your
|
|
613
|
+
> project.
|
|
566
614
|
>
|
|
567
|
-
>
|
|
568
|
-
>
|
|
569
|
-
>
|
|
615
|
+
> As there are no more modules (**standalone** components), you no longer load a `*.module.ts` but
|
|
616
|
+
> a routes file exporting a `Routes` constant. See [UPGRADING_V19.md](UPGRADING_V19.md)
|
|
617
|
+
> for the details of the migration.
|
|
570
618
|
|
|
571
619
|
`app.routes.ts`
|
|
572
620
|
|
|
@@ -606,9 +654,9 @@ export const routes: Routes = [
|
|
|
606
654
|
];
|
|
607
655
|
```
|
|
608
656
|
|
|
609
|
-
> 💡
|
|
657
|
+
> 💡 So we load `welcome.routes.ts` (and no longer `welcome.module.ts`):
|
|
610
658
|
> `loadChildren: () => import('./modules/welcome/welcome.routes').then((m) => m.welcome_routes)`
|
|
611
|
-
>
|
|
659
|
+
> instead of `import('./modules/welcome/welcome.module').then((m) => m.WelcomeModule)`.
|
|
612
660
|
|
|
613
661
|
`welcome.routes.ts`
|
|
614
662
|
|
|
@@ -656,45 +704,44 @@ export const inscription_routes: Routes = [
|
|
|
656
704
|
];
|
|
657
705
|
```
|
|
658
706
|
|
|
659
|
-
> 💡
|
|
707
|
+
> 💡 You can also lazy-load a component:
|
|
660
708
|
>
|
|
661
709
|
> ```ts
|
|
662
710
|
> { path: 'list', loadComponent: () => import('./demande-list-page/demande-list-page.component').then((m) => m.DemandeListPageComponent) },
|
|
663
711
|
> ```
|
|
664
712
|
|
|
665
|
-
##
|
|
713
|
+
## Creating a reusable component
|
|
666
714
|
|
|
667
|
-
> ℹ️
|
|
668
|
-
>
|
|
669
|
-
> fonctionnera pas correctement.
|
|
715
|
+
> ℹ️ Creating a reusable component in Angular isn't difficult, but making it fit prestations-ng is
|
|
716
|
+
> another matter. Some rules must be respected, otherwise it won't work properly.
|
|
670
717
|
|
|
671
|
-
###
|
|
718
|
+
### Example with foehn-input-date of prestations-ng
|
|
672
719
|
|
|
673
|
-
|
|
720
|
+
Your component must:
|
|
674
721
|
|
|
675
|
-
1. `selector`
|
|
676
|
-
2.
|
|
677
|
-
3.
|
|
678
|
-
4.
|
|
679
|
-
|
|
680
|
-
5.
|
|
681
|
-
|
|
682
|
-
6.
|
|
683
|
-
|
|
684
|
-
7.
|
|
685
|
-
|
|
686
|
-
8.
|
|
687
|
-
|
|
688
|
-
|
|
722
|
+
1. `selector` should start with `"app-..."` to avoid possible naming conflicts
|
|
723
|
+
2. have a `providers`
|
|
724
|
+
3. extend `FoehnInputComponent`, to which you must give your data model
|
|
725
|
+
4. spread the attributes of your data model in public **signals** used in
|
|
726
|
+
your HTML
|
|
727
|
+
5. implement an `update` function (here it will be `updateDate`) calling `updateNgModel` to
|
|
728
|
+
keep your data model synchronized with your page
|
|
729
|
+
6. implement a `handleUserInput` function calling `handleChange` to keep
|
|
730
|
+
notifying the parent of the behavior changes of your component
|
|
731
|
+
7. implement an `onModelChange` function to correctly initialize the
|
|
732
|
+
signals corresponding to your model
|
|
733
|
+
8. implement a `getValidValue` function to rebuild your data model before sending it
|
|
734
|
+
back to your page, and return `null` if all the attributes of your object are empty (this
|
|
735
|
+
is to keep Java happy)
|
|
689
736
|
|
|
690
|
-
> 🚨
|
|
737
|
+
> 🚨 **standalone / signals** points to respect:
|
|
691
738
|
>
|
|
692
|
-
> - **
|
|
693
|
-
> -
|
|
694
|
-
>
|
|
695
|
-
> -
|
|
696
|
-
>
|
|
697
|
-
>
|
|
739
|
+
> - **No `multi: true`** in the `providers` (removed since the standalone migration).
|
|
740
|
+
> - The `imports` of the component are declared **directly** in the `@Component` decorator. Each
|
|
741
|
+
> component loads what it needs (see `foehn-input-date` in this project).
|
|
742
|
+
> - The state displayed by the component is held in **signals** (point 4): `onModelChange` is
|
|
743
|
+
> called when the model changes, sometimes outside of an event of the template, and a plain field
|
|
744
|
+
> would then not be displayed without zone.js.
|
|
698
745
|
|
|
699
746
|
`input-date.component.ts`
|
|
700
747
|
|
|
@@ -707,7 +754,7 @@ import { FoehnValidationAlertsComponent } from '../foehn-validation-alerts/foehn
|
|
|
707
754
|
import { SdkDictionaryPipe } from '../sdk-dictionary/sdk-dictionary.pipe';
|
|
708
755
|
|
|
709
756
|
@Component({
|
|
710
|
-
// point (1)
|
|
757
|
+
// point (1): in your prestation, the selector will rather be 'app-...'
|
|
711
758
|
selector: 'app-input-date',
|
|
712
759
|
templateUrl: './input-date.component.html',
|
|
713
760
|
// point (2)
|
|
@@ -715,10 +762,10 @@ import { SdkDictionaryPipe } from '../sdk-dictionary/sdk-dictionary.pipe';
|
|
|
715
762
|
{
|
|
716
763
|
provide: FoehnInputComponent,
|
|
717
764
|
useExisting: forwardRef(() => InputDateComponent),
|
|
718
|
-
// ⚠️
|
|
765
|
+
// ⚠️ no more `multi: true` in standalone
|
|
719
766
|
},
|
|
720
767
|
],
|
|
721
|
-
//
|
|
768
|
+
// the imports are declared directly here: each component loads what it needs
|
|
722
769
|
imports: [
|
|
723
770
|
FoehnValidationAlertsComponent,
|
|
724
771
|
FoehnInputNumberComponent,
|
|
@@ -726,9 +773,9 @@ import { SdkDictionaryPipe } from '../sdk-dictionary/sdk-dictionary.pipe';
|
|
|
726
773
|
],
|
|
727
774
|
})
|
|
728
775
|
export class InputDateComponent extends FoehnInputComponent<number[]> {
|
|
729
|
-
// point (3)
|
|
776
|
+
// point (3): extends FoehnInputComponent<number[]>
|
|
730
777
|
|
|
731
|
-
// point (4)
|
|
778
|
+
// point (4): the attributes of your model spread in public signals
|
|
732
779
|
readonly day = signal<string | null>(null);
|
|
733
780
|
readonly month = signal<string | null>(null);
|
|
734
781
|
readonly year = signal<string | null>(null);
|
|
@@ -787,17 +834,17 @@ export class InputDateComponent extends FoehnInputComponent<number[]> {
|
|
|
787
834
|
}
|
|
788
835
|
```
|
|
789
836
|
|
|
790
|
-
|
|
837
|
+
Each `foehn-input-*` used in your view must:
|
|
791
838
|
|
|
792
|
-
1.
|
|
793
|
-
2.
|
|
794
|
-
3.
|
|
795
|
-
4.
|
|
796
|
-
|
|
797
|
-
5.
|
|
798
|
-
6.
|
|
839
|
+
1. have an `id` concatenating the call to the `buildId()` function and the name of the attribute of your object
|
|
840
|
+
2. have a `name` concatenating the **signal** input `name()` and the name of the attribute of your object
|
|
841
|
+
3. have a `label`
|
|
842
|
+
4. use your public signal as model in two-way binding (banana in the box): `[(model)]="day"`
|
|
843
|
+
reads and writes the signal (without the parentheses)
|
|
844
|
+
5. call `updateDate()` when the `(modelChange)` event is triggered
|
|
845
|
+
6. call `handleUserInput()` when the `(userInput)` event is triggered
|
|
799
846
|
|
|
800
|
-
> 💡 `name`
|
|
847
|
+
> 💡 `name` is now an **input signal**. You call it: `name() + '_day'` (and no longer
|
|
801
848
|
> `name + '_day'`).
|
|
802
849
|
|
|
803
850
|
`input-date.component.html`
|
|
@@ -840,20 +887,20 @@ Chaque `foehn-input-*` utilisé dans votre vue doit :
|
|
|
840
887
|
/>
|
|
841
888
|
```
|
|
842
889
|
|
|
843
|
-
###
|
|
890
|
+
### Displaying the global validation of the component
|
|
844
891
|
|
|
845
|
-
-
|
|
846
|
-
-
|
|
847
|
-
|
|
892
|
+
- Add the code below in the HTML part of your component
|
|
893
|
+
- Return an error matching the `name` of your component when it is used in
|
|
894
|
+
your form page.
|
|
848
895
|
|
|
849
|
-
> 💡
|
|
896
|
+
> 💡 **signals** points to note in this template:
|
|
850
897
|
>
|
|
851
|
-
> - `@if`
|
|
898
|
+
> - `@if` replaces `*ngIf`, and the attributes of the component are signals: you call them
|
|
852
899
|
> (`label()`, `required()`, `isLabelSrOnly()`, `hideNotRequiredExtraLabel()`, `helpText()`).
|
|
853
|
-
> - `[ngClass]`
|
|
900
|
+
> - `[ngClass]` is replaced by `[class.xxx]="..."` bindings.
|
|
854
901
|
|
|
855
902
|
```html
|
|
856
|
-
<!-- (
|
|
903
|
+
<!-- (mandatory) Displays a red vertical bar on the whole component -->
|
|
857
904
|
<div
|
|
858
905
|
class="form-group"
|
|
859
906
|
[class.has-danger]="hasErrorsToDisplay()"
|
|
@@ -861,12 +908,12 @@ Chaque `foehn-input-*` utilisé dans votre vue doit :
|
|
|
861
908
|
[attr.id]="buildId('Container')"
|
|
862
909
|
tabindex="-1"
|
|
863
910
|
>
|
|
864
|
-
<!-- (
|
|
911
|
+
<!-- (mandatory) Groups your fields for a global validation -->
|
|
865
912
|
<fieldset
|
|
866
913
|
[attr.aria-describedby]="getDescribedBy()"
|
|
867
914
|
[attr.aria-invalid]="hasErrorsToDisplay() || null"
|
|
868
915
|
>
|
|
869
|
-
<!-- (
|
|
916
|
+
<!-- (optional) Only if you want to display a global label/legend for your component -->
|
|
870
917
|
@if (!!label()) {
|
|
871
918
|
<legend
|
|
872
919
|
[class.visually-hidden]="isLabelSrOnly()"
|
|
@@ -881,10 +928,10 @@ Chaque `foehn-input-*` utilisé dans votre vue doit :
|
|
|
881
928
|
</legend>
|
|
882
929
|
}
|
|
883
930
|
|
|
884
|
-
<!-- (
|
|
931
|
+
<!-- (mandatory) -->
|
|
885
932
|
<foehn-validation-alerts [component]="this" />
|
|
886
933
|
|
|
887
|
-
<!-- (
|
|
934
|
+
<!-- (optional) Only if you want to be able to display a global helpText for the component -->
|
|
888
935
|
@if (helpText()) {
|
|
889
936
|
<small
|
|
890
937
|
[attr.id]="buildId() + 'Help'"
|
|
@@ -893,36 +940,36 @@ Chaque `foehn-input-*` utilisé dans votre vue doit :
|
|
|
893
940
|
></small>
|
|
894
941
|
}
|
|
895
942
|
|
|
896
|
-
<!-- ...
|
|
943
|
+
<!-- ...your foehn-input-* here -->
|
|
897
944
|
</fieldset>
|
|
898
945
|
</div>
|
|
899
946
|
```
|
|
900
947
|
|
|
901
|
-
###
|
|
948
|
+
### Avoiding error display problems in a `@for` loop
|
|
902
949
|
|
|
903
|
-
> ⚠️
|
|
904
|
-
> validation
|
|
905
|
-
>
|
|
950
|
+
> ⚠️ Don't forget to put a `track` with a unique identifier in the `@for` loop, otherwise the
|
|
951
|
+
> validation won't be displayed. When a unique identifier such as `id` isn't possible, track
|
|
952
|
+
> by object or, as a last resort, `track $index`.
|
|
906
953
|
|
|
907
|
-
> ℹ️
|
|
908
|
-
>
|
|
909
|
-
>
|
|
954
|
+
> ℹ️ In an Angular `@for` loop with a `FoehnInputComponent` component, if you validate your
|
|
955
|
+
> form, the errors returned are synchronized with the index of your components repeated in the
|
|
956
|
+
> loop.
|
|
910
957
|
>
|
|
911
|
-
> **
|
|
912
|
-
>
|
|
913
|
-
>
|
|
914
|
-
>
|
|
958
|
+
> **Scenario:** In your list, when you remove a component that has errors at
|
|
959
|
+
> index 0 (for example) and the next component (index 1) has no errors or not the same
|
|
960
|
+
> errors, then the latter ends up with the errors of the component just removed at
|
|
961
|
+
> index 0.
|
|
915
962
|
>
|
|
916
|
-
> **
|
|
917
|
-
>
|
|
918
|
-
>
|
|
919
|
-
>
|
|
963
|
+
> **Explanation:** This happens because the errors returned by GESDEM are only
|
|
964
|
+
> refreshed when the data is saved. So the component removed at
|
|
965
|
+
> index 0 is replaced by the one at index 1, but the errors at index 0 still exist
|
|
966
|
+
> and are therefore carried over to the component coming from index 1.
|
|
920
967
|
|
|
921
|
-
|
|
922
|
-
|
|
968
|
+
Here is the good practice to put in place to work around these error display problems in a
|
|
969
|
+
`@for` loop.
|
|
923
970
|
|
|
924
|
-
|
|
925
|
-
|
|
971
|
+
When you remove an item from your list, you must call a method of
|
|
972
|
+
your `AbstractPageComponent`. This method will contain the following code:
|
|
926
973
|
|
|
927
974
|
```ts
|
|
928
975
|
/*
|
|
@@ -932,30 +979,30 @@ votre `AbstractPageComponent`. Cette méthode contiendra le code suivant :
|
|
|
932
979
|
this._gesdemService.validate(this.form).pipe(first()).subscribe();
|
|
933
980
|
```
|
|
934
981
|
|
|
935
|
-
|
|
982
|
+
Here is an example of a form page containing this method (1):
|
|
936
983
|
|
|
937
|
-
> 💡
|
|
938
|
-
> (
|
|
939
|
-
>
|
|
984
|
+
> 💡 No more `constructor`: we use **inject**. The dependencies of `AbstractPageComponent`
|
|
985
|
+
> (including `_dictionaryService` and `_gesdemService`) are already injected in the abstract class. For
|
|
986
|
+
> your own dependencies, use `inject()`.
|
|
940
987
|
|
|
941
988
|
```ts
|
|
942
989
|
@Component({
|
|
943
|
-
templateUrl: './
|
|
990
|
+
templateUrl: './your-page.component.html',
|
|
944
991
|
imports: [
|
|
945
|
-
// ...
|
|
992
|
+
// ... your foehn-* components
|
|
946
993
|
],
|
|
947
994
|
})
|
|
948
|
-
export class
|
|
995
|
+
export class YourPageComponent
|
|
949
996
|
extends AbstractPageComponent<BusinessForm>
|
|
950
997
|
implements OnInit
|
|
951
998
|
{
|
|
952
|
-
//
|
|
999
|
+
// your own dependencies: inject() instead of the constructor
|
|
953
1000
|
private readonly metaService = inject(MetaService);
|
|
954
1001
|
|
|
955
1002
|
ngOnInit(): void {
|
|
956
1003
|
super.ngOnInit();
|
|
957
1004
|
this.setPageTitle(
|
|
958
|
-
this._dictionaryService.getKeySync('
|
|
1005
|
+
this._dictionaryService.getKeySync('your-page.title'),
|
|
959
1006
|
);
|
|
960
1007
|
}
|
|
961
1008
|
|
|
@@ -974,12 +1021,12 @@ export class VotrePageComponent
|
|
|
974
1021
|
}
|
|
975
1022
|
```
|
|
976
1023
|
|
|
977
|
-
|
|
978
|
-
|
|
1024
|
+
It's up to you to decide how to call this method when the item is removed from
|
|
1025
|
+
your list.
|
|
979
1026
|
|
|
980
|
-
|
|
1027
|
+
Here is an elegant way to handle your lists of items:
|
|
981
1028
|
|
|
982
|
-
- Structure
|
|
1029
|
+
- Structure of the folders and files
|
|
983
1030
|
|
|
984
1031
|
```text
|
|
985
1032
|
demande-page/
|
|
@@ -996,7 +1043,7 @@ demande-page/
|
|
|
996
1043
|
|
|
997
1044
|
**vehicule-item**
|
|
998
1045
|
|
|
999
|
-
`vehicule-item.component.html`
|
|
1046
|
+
`vehicule-item.component.html` contains the button removing the item from the list (1).
|
|
1000
1047
|
|
|
1001
1048
|
```html
|
|
1002
1049
|
<div
|
|
@@ -1009,7 +1056,7 @@ demande-page/
|
|
|
1009
1056
|
<foehn-validation-alerts [component]="this" />
|
|
1010
1057
|
|
|
1011
1058
|
<div class="row d-flex align-items-end vehicule-infos">
|
|
1012
|
-
<!-- ...
|
|
1059
|
+
<!-- ... your foehn-input-* components -->
|
|
1013
1060
|
|
|
1014
1061
|
<div class="col-md-2 mt-md-0 mt-xs-2">
|
|
1015
1062
|
<button class="btn btn-danger" (click)="removeVehicule()">
|
|
@@ -1021,10 +1068,10 @@ demande-page/
|
|
|
1021
1068
|
</div>
|
|
1022
1069
|
```
|
|
1023
1070
|
|
|
1024
|
-
`vehicule-item.component.ts`
|
|
1025
|
-
parent (
|
|
1071
|
+
`vehicule-item.component.ts` contains the removal function, which sends the event up to its
|
|
1072
|
+
parent (the list of items).
|
|
1026
1073
|
|
|
1027
|
-
> 💡 `@Output() + EventEmitter`
|
|
1074
|
+
> 💡 `@Output() + EventEmitter` are replaced by the `output()` function.
|
|
1028
1075
|
|
|
1029
1076
|
```ts
|
|
1030
1077
|
@Component({
|
|
@@ -1038,7 +1085,7 @@ parent (la liste des éléments).
|
|
|
1038
1085
|
],
|
|
1039
1086
|
imports: [
|
|
1040
1087
|
FoehnValidationAlertsComponent,
|
|
1041
|
-
// ...
|
|
1088
|
+
// ... your foehn-input-*
|
|
1042
1089
|
SdkDictionaryPipe,
|
|
1043
1090
|
],
|
|
1044
1091
|
})
|
|
@@ -1055,11 +1102,11 @@ export class VehiculeItemComponent extends FoehnInputComponent<Vehicule> {
|
|
|
1055
1102
|
|
|
1056
1103
|
**vehicules-list**
|
|
1057
1104
|
|
|
1058
|
-
`vehicules-list.component.html`
|
|
1059
|
-
|
|
1105
|
+
`vehicules-list.component.html` contains your `@for` loop (1) and your repeatable component (2), which
|
|
1106
|
+
must also send the event up.
|
|
1060
1107
|
|
|
1061
|
-
> ⚠️
|
|
1062
|
-
> validation
|
|
1108
|
+
> ⚠️ In the `@for` loop, don't forget to put a `track` (unique identifier), otherwise the
|
|
1109
|
+
> validation won't be displayed. Here, for example, we track with `vehicule.id`.
|
|
1063
1110
|
|
|
1064
1111
|
```html
|
|
1065
1112
|
<div
|
|
@@ -1092,9 +1139,9 @@ devra lui aussi remonter l'événement.
|
|
|
1092
1139
|
></small>
|
|
1093
1140
|
}
|
|
1094
1141
|
|
|
1095
|
-
<!-- (1)
|
|
1142
|
+
<!-- (1) your @for loop + track (unique identifier) -->
|
|
1096
1143
|
@for (vehicule of vehicules(); track vehicule.id; let i = $index) {
|
|
1097
|
-
<!-- (2)
|
|
1144
|
+
<!-- (2) your reusable component -->
|
|
1098
1145
|
<app-vehicule-item
|
|
1099
1146
|
[name]="name() + '[' + i + ']'"
|
|
1100
1147
|
[model]="vehicule"
|
|
@@ -1110,15 +1157,15 @@ devra lui aussi remonter l'événement.
|
|
|
1110
1157
|
</div>
|
|
1111
1158
|
```
|
|
1112
1159
|
|
|
1113
|
-
> 💡
|
|
1160
|
+
> 💡 The `name` is an input signal: `[name]="name() + '[' + i + ']'"` (and no longer
|
|
1114
1161
|
> `[name]="name + '[' + i + ']'"`).
|
|
1115
1162
|
>
|
|
1116
|
-
> 💡
|
|
1117
|
-
>
|
|
1163
|
+
> 💡 The variable of a `@for` loop can't be modified: the item is passed with `[model]`, and
|
|
1164
|
+
> the item of the list is replaced in `updateVehiculesList()` (called by `(modelChange)`).
|
|
1118
1165
|
|
|
1119
|
-
`vehicules-list.component.ts`
|
|
1120
|
-
|
|
1121
|
-
|
|
1166
|
+
`vehicules-list.component.ts` in turn sends the event (1) up to the parent (`AbstractPageComponent`)
|
|
1167
|
+
and removes the item from the list (2). This is also where you can handle the addition of an
|
|
1168
|
+
item (3).
|
|
1122
1169
|
|
|
1123
1170
|
```ts
|
|
1124
1171
|
@Component({
|
|
@@ -1137,25 +1184,25 @@ ainsi que supprimer l'élément de la liste (2). C'est aussi ici que vous pouvez
|
|
|
1137
1184
|
],
|
|
1138
1185
|
})
|
|
1139
1186
|
export class VehiculesListComponent extends FoehnInputComponent<Vehicule[]> {
|
|
1140
|
-
readonly removedVehicule = output<void>(); // (1)
|
|
1187
|
+
readonly removedVehicule = output<void>(); // (1) event to send up to the parent
|
|
1141
1188
|
|
|
1142
|
-
// Signal
|
|
1189
|
+
// Signal holding your repeatable items: it is replaced, never modified in place
|
|
1143
1190
|
readonly vehicules = signal<Vehicule[]>([]);
|
|
1144
1191
|
|
|
1145
1192
|
updateVehiculesList(vehicule: Vehicule, index: number): void {
|
|
1146
1193
|
this.vehicules.update((vehicules) =>
|
|
1147
1194
|
vehicules.map((item, i) => (i === index ? vehicule : item)),
|
|
1148
1195
|
);
|
|
1149
|
-
this.getValidValueAndUpdateNgModel(); //
|
|
1196
|
+
this.getValidValueAndUpdateNgModel(); // Update of your model
|
|
1150
1197
|
}
|
|
1151
1198
|
|
|
1152
1199
|
handleRemovedVehicule(index: number): void {
|
|
1153
|
-
// (2)
|
|
1200
|
+
// (2) your function removing the item and sending the event
|
|
1154
1201
|
this.vehicules.update((vehicules) =>
|
|
1155
1202
|
vehicules.filter((_vehicule, i) => i !== index),
|
|
1156
|
-
); //
|
|
1157
|
-
this.getValidValueAndUpdateNgModel(); //
|
|
1158
|
-
this.removedVehicule.emit(); // (1)
|
|
1203
|
+
); // removal of the item
|
|
1204
|
+
this.getValidValueAndUpdateNgModel(); // Update of your model
|
|
1205
|
+
this.removedVehicule.emit(); // (1) the event is sent up
|
|
1159
1206
|
}
|
|
1160
1207
|
|
|
1161
1208
|
override onModelChange(value: Vehicule[] | null): void {
|
|
@@ -1163,19 +1210,19 @@ export class VehiculesListComponent extends FoehnInputComponent<Vehicule[]> {
|
|
|
1163
1210
|
}
|
|
1164
1211
|
|
|
1165
1212
|
addVehicule(): void {
|
|
1166
|
-
// (3)
|
|
1213
|
+
// (3) Handling of the addition of an item
|
|
1167
1214
|
// ...code
|
|
1168
1215
|
}
|
|
1169
1216
|
}
|
|
1170
1217
|
```
|
|
1171
1218
|
|
|
1172
|
-
> 💡
|
|
1173
|
-
>
|
|
1219
|
+
> 💡 No need to define a dedicated `trackByFn` anymore: the new `@for` syntax directly uses
|
|
1220
|
+
> the `track` expression (here `vehicule.id`).
|
|
1174
1221
|
|
|
1175
1222
|
**demande-page**
|
|
1176
1223
|
|
|
1177
|
-
`demande-page.component.html`
|
|
1178
|
-
|
|
1224
|
+
`demande-page.component.html` uses your vehicle list component (1) and listens to
|
|
1225
|
+
the removal event (2).
|
|
1179
1226
|
|
|
1180
1227
|
```html
|
|
1181
1228
|
<foehn-form>
|
|
@@ -1189,7 +1236,7 @@ l'événement de suppression (2).
|
|
|
1189
1236
|
[required]="true"
|
|
1190
1237
|
(removedVehicule)="handleRemovedVehicule()"
|
|
1191
1238
|
/>
|
|
1192
|
-
<!-- (1)
|
|
1239
|
+
<!-- (1) Use of your list - (2) Call of the method saving the data in GESDEM to refresh the errors -->
|
|
1193
1240
|
|
|
1194
1241
|
<foehn-navigation
|
|
1195
1242
|
id="navigation"
|
|
@@ -1199,8 +1246,8 @@ l'événement de suppression (2).
|
|
|
1199
1246
|
</foehn-form>
|
|
1200
1247
|
```
|
|
1201
1248
|
|
|
1202
|
-
`demande-page.component.ts`
|
|
1203
|
-
|
|
1249
|
+
`demande-page.component.ts` calls GESDEM to generate the errors of the
|
|
1250
|
+
form again, and thus refresh the index.
|
|
1204
1251
|
|
|
1205
1252
|
```ts
|
|
1206
1253
|
@Component({
|
|
@@ -1222,20 +1269,20 @@ export class DemandePageComponent
|
|
|
1222
1269
|
}
|
|
1223
1270
|
```
|
|
1224
1271
|
|
|
1225
|
-
##
|
|
1272
|
+
## Form validation
|
|
1226
1273
|
|
|
1227
|
-
> ℹ️
|
|
1228
|
-
>
|
|
1274
|
+
> ℹ️ When your form is saved in GESDEM, the `AbstractPageComponent` handles the
|
|
1275
|
+
> display of the errors for you.
|
|
1229
1276
|
>
|
|
1230
|
-
>
|
|
1231
|
-
>
|
|
1277
|
+
> If, however, you want to save the data somewhere else than in GESDEM, you will have to handle
|
|
1278
|
+
> the display of the errors manually.
|
|
1232
1279
|
|
|
1233
|
-
###
|
|
1280
|
+
### Standard handling
|
|
1234
1281
|
|
|
1235
|
-
> ℹ️
|
|
1236
|
-
>
|
|
1282
|
+
> ℹ️ When your data is saved in GESDEM, the display of the errors is handled automatically
|
|
1283
|
+
> for you. However, you must respect some rules.
|
|
1237
1284
|
|
|
1238
|
-
|
|
1285
|
+
Let's take an example of a simple front-end model with two levels of depth:
|
|
1239
1286
|
|
|
1240
1287
|
`identification.ts`
|
|
1241
1288
|
|
|
@@ -1255,8 +1302,8 @@ export class BusinessForm {
|
|
|
1255
1302
|
}
|
|
1256
1303
|
```
|
|
1257
1304
|
|
|
1258
|
-
> ⚠️
|
|
1259
|
-
>
|
|
1305
|
+
> ⚠️ The model above must be reflected in the `name` of the `foehn-input-*`
|
|
1306
|
+
> components of your page.
|
|
1260
1307
|
|
|
1261
1308
|
`page-1.html`
|
|
1262
1309
|
|
|
@@ -1265,7 +1312,7 @@ export class BusinessForm {
|
|
|
1265
1312
|
[(model)]="form.identification.firstName"
|
|
1266
1313
|
name="identification.firstName"
|
|
1267
1314
|
/>
|
|
1268
|
-
<!-- 2
|
|
1315
|
+
<!-- 2 levels of depth -->
|
|
1269
1316
|
|
|
1270
1317
|
<foehn-input-text
|
|
1271
1318
|
[(model)]="form.identification.lastName"
|
|
@@ -1273,26 +1320,26 @@ export class BusinessForm {
|
|
|
1273
1320
|
/>
|
|
1274
1321
|
|
|
1275
1322
|
<foehn-boolean-checkbox [(model)]="form.acceptCookies" name="acceptCookies" />
|
|
1276
|
-
<!-- 1
|
|
1323
|
+
<!-- 1 level of depth -->
|
|
1277
1324
|
```
|
|
1278
1325
|
|
|
1279
|
-
-
|
|
1280
|
-
|
|
1281
|
-
-
|
|
1326
|
+
- On the back-end side, you can handle your errors with the annotations (`@NotEmpty`, `@NotNull`, ...),
|
|
1327
|
+
and **prestations-be** does the rest for you.
|
|
1328
|
+
- If you want to return an error specific to a field of your form, here is the syntax:
|
|
1282
1329
|
|
|
1283
1330
|
```java
|
|
1284
|
-
new AbstractSdkController.FormError("identification.lastName", "ErrorCodeHere", "
|
|
1331
|
+
new AbstractSdkController.FormError("identification.lastName", "ErrorCodeHere", "The last name is required if you don't specify a first name")
|
|
1285
1332
|
```
|
|
1286
1333
|
|
|
1287
|
-
> ⚠️
|
|
1288
|
-
>
|
|
1334
|
+
> ⚠️ The name of the error must match the name of your component in your HTML
|
|
1335
|
+
> page.
|
|
1289
1336
|
|
|
1290
|
-
###
|
|
1337
|
+
### Manual handling
|
|
1291
1338
|
|
|
1292
1339
|
#### Back-end
|
|
1293
1340
|
|
|
1294
|
-
|
|
1295
|
-
|
|
1341
|
+
On the JAVA back-end side, the server must return an object containing the errors of the form to
|
|
1342
|
+
be compatible with prestations-ng.
|
|
1296
1343
|
|
|
1297
1344
|
```java
|
|
1298
1345
|
@Getter
|
|
@@ -1306,23 +1353,23 @@ public class PostResponse<T> {
|
|
|
1306
1353
|
|
|
1307
1354
|
@Builder.Default
|
|
1308
1355
|
private List<AbstractSdkController.FormError> errors = new ArrayList<>();
|
|
1309
|
-
// AbstractSdkController.FormError
|
|
1356
|
+
// AbstractSdkController.FormError comes from prestations-be, the backend part of prestations-ng
|
|
1310
1357
|
}
|
|
1311
1358
|
```
|
|
1312
1359
|
|
|
1313
|
-
|
|
1360
|
+
Then, add your errors to your model before returning it to the front-end:
|
|
1314
1361
|
|
|
1315
1362
|
```java
|
|
1316
1363
|
List<AbstractSdkController.FormError> errors = new ArrayList<>();
|
|
1317
|
-
errors.add(new AbstractSdkController.FormError("identification.lastName", "ErrorCodeHere", "
|
|
1364
|
+
errors.add(new AbstractSdkController.FormError("identification.lastName", "ErrorCodeHere", "The last name is required if you don't specify a first name"));
|
|
1318
1365
|
|
|
1319
1366
|
PostResponse.<MyObject>builder().dto(myObject).errors(errors).build();
|
|
1320
1367
|
```
|
|
1321
1368
|
|
|
1322
1369
|
#### Front-end
|
|
1323
1370
|
|
|
1324
|
-
|
|
1325
|
-
page
|
|
1371
|
+
On the front-end side, as for the standard validation, the names of the `foehn-input-*` components of your
|
|
1372
|
+
HTML page and the names of your errors must match.
|
|
1326
1373
|
|
|
1327
1374
|
```html
|
|
1328
1375
|
<foehn-input-text
|
|
@@ -1331,15 +1378,15 @@ page HTML et le nom de vos erreurs devront correspondre.
|
|
|
1331
1378
|
/>
|
|
1332
1379
|
```
|
|
1333
1380
|
|
|
1334
|
-
> ⚠️
|
|
1335
|
-
>
|
|
1381
|
+
> ⚠️ When your data has been saved, you will have to trigger the validation
|
|
1382
|
+
> manually.
|
|
1336
1383
|
|
|
1337
|
-
1.
|
|
1338
|
-
|
|
1339
|
-
2.
|
|
1384
|
+
1. In your `page.ts`, create a `handleValidationErrors` function to handle the display of the
|
|
1385
|
+
errors.
|
|
1386
|
+
2. This function is called when the back-end answers.
|
|
1340
1387
|
|
|
1341
|
-
> 💡
|
|
1342
|
-
> TypeScript,
|
|
1388
|
+
> 💡 **signals** point: no more `@ViewChild('formWithModels')`. To use a component in the
|
|
1389
|
+
> TypeScript, use a `viewChild(...)` **signal**. Then access the instance by calling it:
|
|
1343
1390
|
> `this.formWithModels()`.
|
|
1344
1391
|
|
|
1345
1392
|
`page.ts`
|
|
@@ -1356,41 +1403,41 @@ import {
|
|
|
1356
1403
|
// ...
|
|
1357
1404
|
})
|
|
1358
1405
|
export class ... extends AbstractPageComponent<YOUR_MODEL> implements OnInit {
|
|
1359
|
-
//
|
|
1406
|
+
// a viewChild signal instead of @ViewChild
|
|
1360
1407
|
readonly formWithModels = viewChild(FoehnFormComponent);
|
|
1361
1408
|
|
|
1362
|
-
//
|
|
1409
|
+
// your own dependencies with inject()
|
|
1363
1410
|
private readonly httpClient = inject(HttpClient);
|
|
1364
1411
|
|
|
1365
1412
|
ngOnInit(): void {
|
|
1366
1413
|
super.ngOnInit();
|
|
1367
|
-
//
|
|
1414
|
+
// Enable the display of the errors
|
|
1368
1415
|
this._validationHandlerService.shouldDisplayErrors(true);
|
|
1369
1416
|
}
|
|
1370
1417
|
|
|
1371
|
-
//
|
|
1418
|
+
// Save function
|
|
1372
1419
|
save(): void {
|
|
1373
|
-
//
|
|
1420
|
+
// Enable the display of the errors
|
|
1374
1421
|
this._validationHandlerService.shouldDisplayErrors(true);
|
|
1375
|
-
//
|
|
1376
|
-
//
|
|
1377
|
-
//
|
|
1422
|
+
// Call to the back-end to do your validation. The callback modifies no field of the page:
|
|
1423
|
+
// the services of prestations-ng (growl, errors) refresh the display themselves, also
|
|
1424
|
+
// without zone.js. A state of the page modified here should be a signal.
|
|
1378
1425
|
this.httpClient
|
|
1379
|
-
.post<PostResponse>('
|
|
1426
|
+
.post<PostResponse>('api_url_here', your_object)
|
|
1380
1427
|
.subscribe((response: PostResponse) => {
|
|
1381
1428
|
if (!response.errors.length) {
|
|
1382
|
-
//
|
|
1429
|
+
// Tell the user that everything went well
|
|
1383
1430
|
this._growlService.addWithType(
|
|
1384
1431
|
GrowlType.SUCCESS,
|
|
1385
|
-
'
|
|
1432
|
+
'Saved successfully',
|
|
1386
1433
|
);
|
|
1387
1434
|
}
|
|
1388
|
-
//
|
|
1435
|
+
// Handling of the errors returned by the back-end
|
|
1389
1436
|
this.handleValidationErrors(response.errors); // point (2)
|
|
1390
1437
|
});
|
|
1391
1438
|
}
|
|
1392
1439
|
|
|
1393
|
-
//
|
|
1440
|
+
// Display of the errors
|
|
1394
1441
|
private handleValidationErrors(errors: ErrorTemplate[]): void {
|
|
1395
1442
|
// point (1)
|
|
1396
1443
|
const form = this.formWithModels();
|
|
@@ -1408,21 +1455,21 @@ export class ... extends AbstractPageComponent<YOUR_MODEL> implements OnInit {
|
|
|
1408
1455
|
|
|
1409
1456
|
```html
|
|
1410
1457
|
<foehn-form>
|
|
1411
|
-
<!-- ...
|
|
1458
|
+
<!-- ... your foehn-input-* components -->
|
|
1412
1459
|
</foehn-form>
|
|
1413
1460
|
```
|
|
1414
1461
|
|
|
1415
|
-
##
|
|
1462
|
+
## Manual validation of a component
|
|
1416
1463
|
|
|
1417
|
-
###
|
|
1464
|
+
### Hiding an error
|
|
1418
1465
|
|
|
1419
|
-
> ✅
|
|
1466
|
+
> ✅ The `markAsDirty()` function simulates an interaction with the component.
|
|
1420
1467
|
>
|
|
1421
|
-
> ℹ️
|
|
1468
|
+
> ℹ️ Hides the error of a foehn-input component in error.
|
|
1422
1469
|
|
|
1423
|
-
###
|
|
1470
|
+
### Displaying an error
|
|
1424
1471
|
|
|
1425
|
-
> ✅
|
|
1426
|
-
>
|
|
1472
|
+
> ✅ The `markAsPristine()` function puts the component back in its original state, as if
|
|
1473
|
+
> no interaction had happened.
|
|
1427
1474
|
>
|
|
1428
|
-
> ℹ️
|
|
1475
|
+
> ℹ️ Displays the error of a foehn-input component in error.
|