@dsivd/prestations-ng 19.2.0-beta.12 → 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 +2 -0
- package/GOOD_PRACTICES.md +439 -394
- package/INTRODUCTION_ANGULAR_SIGNALS.md +150 -150
- package/UPGRADING_V19.md +34 -0
- package/package.json +1 -1
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);
|
|
@@ -469,10 +471,54 @@ ngOnInit(): void {
|
|
|
469
471
|
}
|
|
470
472
|
```
|
|
471
473
|
|
|
472
|
-
###
|
|
474
|
+
### Deriving a state from view queries
|
|
473
475
|
|
|
474
|
-
> ⚠️
|
|
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
|
|
519
|
+
|
|
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
|
|
476
522
|
> `afterNextRender()`.
|
|
477
523
|
|
|
478
524
|
```ts
|
|
@@ -483,18 +529,18 @@ private readonly injector = inject(Injector);
|
|
|
483
529
|
|
|
484
530
|
showDetails(): void {
|
|
485
531
|
this.detailsVisible.set(true);
|
|
486
|
-
//
|
|
532
|
+
// The field only exists once the details are rendered
|
|
487
533
|
afterNextRender(() => this.detailsField()?.nativeElement.focus(), {
|
|
488
534
|
injector: this.injector,
|
|
489
535
|
});
|
|
490
536
|
}
|
|
491
537
|
```
|
|
492
538
|
|
|
493
|
-
###
|
|
539
|
+
### Testing a zoneless component
|
|
494
540
|
|
|
495
|
-
> ⚠️ `fixture.detectChanges()` **
|
|
496
|
-
> Angular (
|
|
497
|
-
>
|
|
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
|
|
498
544
|
> `await fixture.whenStable()`.
|
|
499
545
|
|
|
500
546
|
```ts
|
|
@@ -506,22 +552,22 @@ it('should display the details', async () => {
|
|
|
506
552
|
});
|
|
507
553
|
```
|
|
508
554
|
|
|
509
|
-
`await fixture.whenStable()`
|
|
510
|
-
|
|
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`).
|
|
511
557
|
|
|
512
|
-
|
|
558
|
+
Keep `fixture.detectChanges()` for:
|
|
513
559
|
|
|
514
|
-
-
|
|
515
|
-
-
|
|
516
|
-
|
|
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
|
|
517
563
|
|
|
518
|
-
> 💡
|
|
519
|
-
> DOM (`input.dispatchEvent(new Event('change'))`, `button.click()`, ...)
|
|
520
|
-
>
|
|
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.
|
|
521
567
|
|
|
522
|
-
##
|
|
568
|
+
## Routing your pages
|
|
523
569
|
|
|
524
|
-
###
|
|
570
|
+
### Simple form
|
|
525
571
|
|
|
526
572
|
`app.routes.ts`
|
|
527
573
|
|
|
@@ -561,14 +607,14 @@ export const routes: Routes = [
|
|
|
561
607
|
];
|
|
562
608
|
```
|
|
563
609
|
|
|
564
|
-
###
|
|
610
|
+
### Complex form (sub-routes)
|
|
565
611
|
|
|
566
|
-
> ℹ️
|
|
567
|
-
>
|
|
612
|
+
> ℹ️ Prefer **sub-routes** (`*.routes.ts`) to group your routes and thus better structure your
|
|
613
|
+
> project.
|
|
568
614
|
>
|
|
569
|
-
>
|
|
570
|
-
>
|
|
571
|
-
>
|
|
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.
|
|
572
618
|
|
|
573
619
|
`app.routes.ts`
|
|
574
620
|
|
|
@@ -608,9 +654,9 @@ export const routes: Routes = [
|
|
|
608
654
|
];
|
|
609
655
|
```
|
|
610
656
|
|
|
611
|
-
> 💡
|
|
657
|
+
> 💡 So we load `welcome.routes.ts` (and no longer `welcome.module.ts`):
|
|
612
658
|
> `loadChildren: () => import('./modules/welcome/welcome.routes').then((m) => m.welcome_routes)`
|
|
613
|
-
>
|
|
659
|
+
> instead of `import('./modules/welcome/welcome.module').then((m) => m.WelcomeModule)`.
|
|
614
660
|
|
|
615
661
|
`welcome.routes.ts`
|
|
616
662
|
|
|
@@ -658,45 +704,44 @@ export const inscription_routes: Routes = [
|
|
|
658
704
|
];
|
|
659
705
|
```
|
|
660
706
|
|
|
661
|
-
> 💡
|
|
707
|
+
> 💡 You can also lazy-load a component:
|
|
662
708
|
>
|
|
663
709
|
> ```ts
|
|
664
710
|
> { path: 'list', loadComponent: () => import('./demande-list-page/demande-list-page.component').then((m) => m.DemandeListPageComponent) },
|
|
665
711
|
> ```
|
|
666
712
|
|
|
667
|
-
##
|
|
713
|
+
## Creating a reusable component
|
|
668
714
|
|
|
669
|
-
> ℹ️
|
|
670
|
-
>
|
|
671
|
-
> 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.
|
|
672
717
|
|
|
673
|
-
###
|
|
718
|
+
### Example with foehn-input-date of prestations-ng
|
|
674
719
|
|
|
675
|
-
|
|
720
|
+
Your component must:
|
|
676
721
|
|
|
677
|
-
1. `selector`
|
|
678
|
-
2.
|
|
679
|
-
3.
|
|
680
|
-
4.
|
|
681
|
-
|
|
682
|
-
5.
|
|
683
|
-
|
|
684
|
-
6.
|
|
685
|
-
|
|
686
|
-
7.
|
|
687
|
-
|
|
688
|
-
8.
|
|
689
|
-
|
|
690
|
-
|
|
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)
|
|
691
736
|
|
|
692
|
-
> 🚨
|
|
737
|
+
> 🚨 **standalone / signals** points to respect:
|
|
693
738
|
>
|
|
694
|
-
> - **
|
|
695
|
-
> -
|
|
696
|
-
>
|
|
697
|
-
> -
|
|
698
|
-
>
|
|
699
|
-
>
|
|
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.
|
|
700
745
|
|
|
701
746
|
`input-date.component.ts`
|
|
702
747
|
|
|
@@ -709,7 +754,7 @@ import { FoehnValidationAlertsComponent } from '../foehn-validation-alerts/foehn
|
|
|
709
754
|
import { SdkDictionaryPipe } from '../sdk-dictionary/sdk-dictionary.pipe';
|
|
710
755
|
|
|
711
756
|
@Component({
|
|
712
|
-
// point (1)
|
|
757
|
+
// point (1): in your prestation, the selector will rather be 'app-...'
|
|
713
758
|
selector: 'app-input-date',
|
|
714
759
|
templateUrl: './input-date.component.html',
|
|
715
760
|
// point (2)
|
|
@@ -717,10 +762,10 @@ import { SdkDictionaryPipe } from '../sdk-dictionary/sdk-dictionary.pipe';
|
|
|
717
762
|
{
|
|
718
763
|
provide: FoehnInputComponent,
|
|
719
764
|
useExisting: forwardRef(() => InputDateComponent),
|
|
720
|
-
// ⚠️
|
|
765
|
+
// ⚠️ no more `multi: true` in standalone
|
|
721
766
|
},
|
|
722
767
|
],
|
|
723
|
-
//
|
|
768
|
+
// the imports are declared directly here: each component loads what it needs
|
|
724
769
|
imports: [
|
|
725
770
|
FoehnValidationAlertsComponent,
|
|
726
771
|
FoehnInputNumberComponent,
|
|
@@ -728,9 +773,9 @@ import { SdkDictionaryPipe } from '../sdk-dictionary/sdk-dictionary.pipe';
|
|
|
728
773
|
],
|
|
729
774
|
})
|
|
730
775
|
export class InputDateComponent extends FoehnInputComponent<number[]> {
|
|
731
|
-
// point (3)
|
|
776
|
+
// point (3): extends FoehnInputComponent<number[]>
|
|
732
777
|
|
|
733
|
-
// point (4)
|
|
778
|
+
// point (4): the attributes of your model spread in public signals
|
|
734
779
|
readonly day = signal<string | null>(null);
|
|
735
780
|
readonly month = signal<string | null>(null);
|
|
736
781
|
readonly year = signal<string | null>(null);
|
|
@@ -789,17 +834,17 @@ export class InputDateComponent extends FoehnInputComponent<number[]> {
|
|
|
789
834
|
}
|
|
790
835
|
```
|
|
791
836
|
|
|
792
|
-
|
|
837
|
+
Each `foehn-input-*` used in your view must:
|
|
793
838
|
|
|
794
|
-
1.
|
|
795
|
-
2.
|
|
796
|
-
3.
|
|
797
|
-
4.
|
|
798
|
-
|
|
799
|
-
5.
|
|
800
|
-
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
|
|
801
846
|
|
|
802
|
-
> 💡 `name`
|
|
847
|
+
> 💡 `name` is now an **input signal**. You call it: `name() + '_day'` (and no longer
|
|
803
848
|
> `name + '_day'`).
|
|
804
849
|
|
|
805
850
|
`input-date.component.html`
|
|
@@ -842,20 +887,20 @@ Chaque `foehn-input-*` utilisé dans votre vue doit :
|
|
|
842
887
|
/>
|
|
843
888
|
```
|
|
844
889
|
|
|
845
|
-
###
|
|
890
|
+
### Displaying the global validation of the component
|
|
846
891
|
|
|
847
|
-
-
|
|
848
|
-
-
|
|
849
|
-
|
|
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.
|
|
850
895
|
|
|
851
|
-
> 💡
|
|
896
|
+
> 💡 **signals** points to note in this template:
|
|
852
897
|
>
|
|
853
|
-
> - `@if`
|
|
898
|
+
> - `@if` replaces `*ngIf`, and the attributes of the component are signals: you call them
|
|
854
899
|
> (`label()`, `required()`, `isLabelSrOnly()`, `hideNotRequiredExtraLabel()`, `helpText()`).
|
|
855
|
-
> - `[ngClass]`
|
|
900
|
+
> - `[ngClass]` is replaced by `[class.xxx]="..."` bindings.
|
|
856
901
|
|
|
857
902
|
```html
|
|
858
|
-
<!-- (
|
|
903
|
+
<!-- (mandatory) Displays a red vertical bar on the whole component -->
|
|
859
904
|
<div
|
|
860
905
|
class="form-group"
|
|
861
906
|
[class.has-danger]="hasErrorsToDisplay()"
|
|
@@ -863,12 +908,12 @@ Chaque `foehn-input-*` utilisé dans votre vue doit :
|
|
|
863
908
|
[attr.id]="buildId('Container')"
|
|
864
909
|
tabindex="-1"
|
|
865
910
|
>
|
|
866
|
-
<!-- (
|
|
911
|
+
<!-- (mandatory) Groups your fields for a global validation -->
|
|
867
912
|
<fieldset
|
|
868
913
|
[attr.aria-describedby]="getDescribedBy()"
|
|
869
914
|
[attr.aria-invalid]="hasErrorsToDisplay() || null"
|
|
870
915
|
>
|
|
871
|
-
<!-- (
|
|
916
|
+
<!-- (optional) Only if you want to display a global label/legend for your component -->
|
|
872
917
|
@if (!!label()) {
|
|
873
918
|
<legend
|
|
874
919
|
[class.visually-hidden]="isLabelSrOnly()"
|
|
@@ -883,10 +928,10 @@ Chaque `foehn-input-*` utilisé dans votre vue doit :
|
|
|
883
928
|
</legend>
|
|
884
929
|
}
|
|
885
930
|
|
|
886
|
-
<!-- (
|
|
931
|
+
<!-- (mandatory) -->
|
|
887
932
|
<foehn-validation-alerts [component]="this" />
|
|
888
933
|
|
|
889
|
-
<!-- (
|
|
934
|
+
<!-- (optional) Only if you want to be able to display a global helpText for the component -->
|
|
890
935
|
@if (helpText()) {
|
|
891
936
|
<small
|
|
892
937
|
[attr.id]="buildId() + 'Help'"
|
|
@@ -895,36 +940,36 @@ Chaque `foehn-input-*` utilisé dans votre vue doit :
|
|
|
895
940
|
></small>
|
|
896
941
|
}
|
|
897
942
|
|
|
898
|
-
<!-- ...
|
|
943
|
+
<!-- ...your foehn-input-* here -->
|
|
899
944
|
</fieldset>
|
|
900
945
|
</div>
|
|
901
946
|
```
|
|
902
947
|
|
|
903
|
-
###
|
|
948
|
+
### Avoiding error display problems in a `@for` loop
|
|
904
949
|
|
|
905
|
-
> ⚠️
|
|
906
|
-
> validation
|
|
907
|
-
>
|
|
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`.
|
|
908
953
|
|
|
909
|
-
> ℹ️
|
|
910
|
-
>
|
|
911
|
-
>
|
|
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.
|
|
912
957
|
>
|
|
913
|
-
> **
|
|
914
|
-
>
|
|
915
|
-
>
|
|
916
|
-
>
|
|
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.
|
|
917
962
|
>
|
|
918
|
-
> **
|
|
919
|
-
>
|
|
920
|
-
>
|
|
921
|
-
>
|
|
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.
|
|
922
967
|
|
|
923
|
-
|
|
924
|
-
|
|
968
|
+
Here is the good practice to put in place to work around these error display problems in a
|
|
969
|
+
`@for` loop.
|
|
925
970
|
|
|
926
|
-
|
|
927
|
-
|
|
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:
|
|
928
973
|
|
|
929
974
|
```ts
|
|
930
975
|
/*
|
|
@@ -934,30 +979,30 @@ votre `AbstractPageComponent`. Cette méthode contiendra le code suivant :
|
|
|
934
979
|
this._gesdemService.validate(this.form).pipe(first()).subscribe();
|
|
935
980
|
```
|
|
936
981
|
|
|
937
|
-
|
|
982
|
+
Here is an example of a form page containing this method (1):
|
|
938
983
|
|
|
939
|
-
> 💡
|
|
940
|
-
> (
|
|
941
|
-
>
|
|
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()`.
|
|
942
987
|
|
|
943
988
|
```ts
|
|
944
989
|
@Component({
|
|
945
|
-
templateUrl: './
|
|
990
|
+
templateUrl: './your-page.component.html',
|
|
946
991
|
imports: [
|
|
947
|
-
// ...
|
|
992
|
+
// ... your foehn-* components
|
|
948
993
|
],
|
|
949
994
|
})
|
|
950
|
-
export class
|
|
995
|
+
export class YourPageComponent
|
|
951
996
|
extends AbstractPageComponent<BusinessForm>
|
|
952
997
|
implements OnInit
|
|
953
998
|
{
|
|
954
|
-
//
|
|
999
|
+
// your own dependencies: inject() instead of the constructor
|
|
955
1000
|
private readonly metaService = inject(MetaService);
|
|
956
1001
|
|
|
957
1002
|
ngOnInit(): void {
|
|
958
1003
|
super.ngOnInit();
|
|
959
1004
|
this.setPageTitle(
|
|
960
|
-
this._dictionaryService.getKeySync('
|
|
1005
|
+
this._dictionaryService.getKeySync('your-page.title'),
|
|
961
1006
|
);
|
|
962
1007
|
}
|
|
963
1008
|
|
|
@@ -976,12 +1021,12 @@ export class VotrePageComponent
|
|
|
976
1021
|
}
|
|
977
1022
|
```
|
|
978
1023
|
|
|
979
|
-
|
|
980
|
-
|
|
1024
|
+
It's up to you to decide how to call this method when the item is removed from
|
|
1025
|
+
your list.
|
|
981
1026
|
|
|
982
|
-
|
|
1027
|
+
Here is an elegant way to handle your lists of items:
|
|
983
1028
|
|
|
984
|
-
- Structure
|
|
1029
|
+
- Structure of the folders and files
|
|
985
1030
|
|
|
986
1031
|
```text
|
|
987
1032
|
demande-page/
|
|
@@ -998,7 +1043,7 @@ demande-page/
|
|
|
998
1043
|
|
|
999
1044
|
**vehicule-item**
|
|
1000
1045
|
|
|
1001
|
-
`vehicule-item.component.html`
|
|
1046
|
+
`vehicule-item.component.html` contains the button removing the item from the list (1).
|
|
1002
1047
|
|
|
1003
1048
|
```html
|
|
1004
1049
|
<div
|
|
@@ -1011,7 +1056,7 @@ demande-page/
|
|
|
1011
1056
|
<foehn-validation-alerts [component]="this" />
|
|
1012
1057
|
|
|
1013
1058
|
<div class="row d-flex align-items-end vehicule-infos">
|
|
1014
|
-
<!-- ...
|
|
1059
|
+
<!-- ... your foehn-input-* components -->
|
|
1015
1060
|
|
|
1016
1061
|
<div class="col-md-2 mt-md-0 mt-xs-2">
|
|
1017
1062
|
<button class="btn btn-danger" (click)="removeVehicule()">
|
|
@@ -1023,10 +1068,10 @@ demande-page/
|
|
|
1023
1068
|
</div>
|
|
1024
1069
|
```
|
|
1025
1070
|
|
|
1026
|
-
`vehicule-item.component.ts`
|
|
1027
|
-
parent (
|
|
1071
|
+
`vehicule-item.component.ts` contains the removal function, which sends the event up to its
|
|
1072
|
+
parent (the list of items).
|
|
1028
1073
|
|
|
1029
|
-
> 💡 `@Output() + EventEmitter`
|
|
1074
|
+
> 💡 `@Output() + EventEmitter` are replaced by the `output()` function.
|
|
1030
1075
|
|
|
1031
1076
|
```ts
|
|
1032
1077
|
@Component({
|
|
@@ -1040,7 +1085,7 @@ parent (la liste des éléments).
|
|
|
1040
1085
|
],
|
|
1041
1086
|
imports: [
|
|
1042
1087
|
FoehnValidationAlertsComponent,
|
|
1043
|
-
// ...
|
|
1088
|
+
// ... your foehn-input-*
|
|
1044
1089
|
SdkDictionaryPipe,
|
|
1045
1090
|
],
|
|
1046
1091
|
})
|
|
@@ -1057,11 +1102,11 @@ export class VehiculeItemComponent extends FoehnInputComponent<Vehicule> {
|
|
|
1057
1102
|
|
|
1058
1103
|
**vehicules-list**
|
|
1059
1104
|
|
|
1060
|
-
`vehicules-list.component.html`
|
|
1061
|
-
|
|
1105
|
+
`vehicules-list.component.html` contains your `@for` loop (1) and your repeatable component (2), which
|
|
1106
|
+
must also send the event up.
|
|
1062
1107
|
|
|
1063
|
-
> ⚠️
|
|
1064
|
-
> 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`.
|
|
1065
1110
|
|
|
1066
1111
|
```html
|
|
1067
1112
|
<div
|
|
@@ -1094,9 +1139,9 @@ devra lui aussi remonter l'événement.
|
|
|
1094
1139
|
></small>
|
|
1095
1140
|
}
|
|
1096
1141
|
|
|
1097
|
-
<!-- (1)
|
|
1142
|
+
<!-- (1) your @for loop + track (unique identifier) -->
|
|
1098
1143
|
@for (vehicule of vehicules(); track vehicule.id; let i = $index) {
|
|
1099
|
-
<!-- (2)
|
|
1144
|
+
<!-- (2) your reusable component -->
|
|
1100
1145
|
<app-vehicule-item
|
|
1101
1146
|
[name]="name() + '[' + i + ']'"
|
|
1102
1147
|
[model]="vehicule"
|
|
@@ -1112,15 +1157,15 @@ devra lui aussi remonter l'événement.
|
|
|
1112
1157
|
</div>
|
|
1113
1158
|
```
|
|
1114
1159
|
|
|
1115
|
-
> 💡
|
|
1160
|
+
> 💡 The `name` is an input signal: `[name]="name() + '[' + i + ']'"` (and no longer
|
|
1116
1161
|
> `[name]="name + '[' + i + ']'"`).
|
|
1117
1162
|
>
|
|
1118
|
-
> 💡
|
|
1119
|
-
>
|
|
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)`).
|
|
1120
1165
|
|
|
1121
|
-
`vehicules-list.component.ts`
|
|
1122
|
-
|
|
1123
|
-
|
|
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).
|
|
1124
1169
|
|
|
1125
1170
|
```ts
|
|
1126
1171
|
@Component({
|
|
@@ -1139,25 +1184,25 @@ ainsi que supprimer l'élément de la liste (2). C'est aussi ici que vous pouvez
|
|
|
1139
1184
|
],
|
|
1140
1185
|
})
|
|
1141
1186
|
export class VehiculesListComponent extends FoehnInputComponent<Vehicule[]> {
|
|
1142
|
-
readonly removedVehicule = output<void>(); // (1)
|
|
1187
|
+
readonly removedVehicule = output<void>(); // (1) event to send up to the parent
|
|
1143
1188
|
|
|
1144
|
-
// Signal
|
|
1189
|
+
// Signal holding your repeatable items: it is replaced, never modified in place
|
|
1145
1190
|
readonly vehicules = signal<Vehicule[]>([]);
|
|
1146
1191
|
|
|
1147
1192
|
updateVehiculesList(vehicule: Vehicule, index: number): void {
|
|
1148
1193
|
this.vehicules.update((vehicules) =>
|
|
1149
1194
|
vehicules.map((item, i) => (i === index ? vehicule : item)),
|
|
1150
1195
|
);
|
|
1151
|
-
this.getValidValueAndUpdateNgModel(); //
|
|
1196
|
+
this.getValidValueAndUpdateNgModel(); // Update of your model
|
|
1152
1197
|
}
|
|
1153
1198
|
|
|
1154
1199
|
handleRemovedVehicule(index: number): void {
|
|
1155
|
-
// (2)
|
|
1200
|
+
// (2) your function removing the item and sending the event
|
|
1156
1201
|
this.vehicules.update((vehicules) =>
|
|
1157
1202
|
vehicules.filter((_vehicule, i) => i !== index),
|
|
1158
|
-
); //
|
|
1159
|
-
this.getValidValueAndUpdateNgModel(); //
|
|
1160
|
-
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
|
|
1161
1206
|
}
|
|
1162
1207
|
|
|
1163
1208
|
override onModelChange(value: Vehicule[] | null): void {
|
|
@@ -1165,19 +1210,19 @@ export class VehiculesListComponent extends FoehnInputComponent<Vehicule[]> {
|
|
|
1165
1210
|
}
|
|
1166
1211
|
|
|
1167
1212
|
addVehicule(): void {
|
|
1168
|
-
// (3)
|
|
1213
|
+
// (3) Handling of the addition of an item
|
|
1169
1214
|
// ...code
|
|
1170
1215
|
}
|
|
1171
1216
|
}
|
|
1172
1217
|
```
|
|
1173
1218
|
|
|
1174
|
-
> 💡
|
|
1175
|
-
>
|
|
1219
|
+
> 💡 No need to define a dedicated `trackByFn` anymore: the new `@for` syntax directly uses
|
|
1220
|
+
> the `track` expression (here `vehicule.id`).
|
|
1176
1221
|
|
|
1177
1222
|
**demande-page**
|
|
1178
1223
|
|
|
1179
|
-
`demande-page.component.html`
|
|
1180
|
-
|
|
1224
|
+
`demande-page.component.html` uses your vehicle list component (1) and listens to
|
|
1225
|
+
the removal event (2).
|
|
1181
1226
|
|
|
1182
1227
|
```html
|
|
1183
1228
|
<foehn-form>
|
|
@@ -1191,7 +1236,7 @@ l'événement de suppression (2).
|
|
|
1191
1236
|
[required]="true"
|
|
1192
1237
|
(removedVehicule)="handleRemovedVehicule()"
|
|
1193
1238
|
/>
|
|
1194
|
-
<!-- (1)
|
|
1239
|
+
<!-- (1) Use of your list - (2) Call of the method saving the data in GESDEM to refresh the errors -->
|
|
1195
1240
|
|
|
1196
1241
|
<foehn-navigation
|
|
1197
1242
|
id="navigation"
|
|
@@ -1201,8 +1246,8 @@ l'événement de suppression (2).
|
|
|
1201
1246
|
</foehn-form>
|
|
1202
1247
|
```
|
|
1203
1248
|
|
|
1204
|
-
`demande-page.component.ts`
|
|
1205
|
-
|
|
1249
|
+
`demande-page.component.ts` calls GESDEM to generate the errors of the
|
|
1250
|
+
form again, and thus refresh the index.
|
|
1206
1251
|
|
|
1207
1252
|
```ts
|
|
1208
1253
|
@Component({
|
|
@@ -1224,20 +1269,20 @@ export class DemandePageComponent
|
|
|
1224
1269
|
}
|
|
1225
1270
|
```
|
|
1226
1271
|
|
|
1227
|
-
##
|
|
1272
|
+
## Form validation
|
|
1228
1273
|
|
|
1229
|
-
> ℹ️
|
|
1230
|
-
>
|
|
1274
|
+
> ℹ️ When your form is saved in GESDEM, the `AbstractPageComponent` handles the
|
|
1275
|
+
> display of the errors for you.
|
|
1231
1276
|
>
|
|
1232
|
-
>
|
|
1233
|
-
>
|
|
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.
|
|
1234
1279
|
|
|
1235
|
-
###
|
|
1280
|
+
### Standard handling
|
|
1236
1281
|
|
|
1237
|
-
> ℹ️
|
|
1238
|
-
>
|
|
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.
|
|
1239
1284
|
|
|
1240
|
-
|
|
1285
|
+
Let's take an example of a simple front-end model with two levels of depth:
|
|
1241
1286
|
|
|
1242
1287
|
`identification.ts`
|
|
1243
1288
|
|
|
@@ -1257,8 +1302,8 @@ export class BusinessForm {
|
|
|
1257
1302
|
}
|
|
1258
1303
|
```
|
|
1259
1304
|
|
|
1260
|
-
> ⚠️
|
|
1261
|
-
>
|
|
1305
|
+
> ⚠️ The model above must be reflected in the `name` of the `foehn-input-*`
|
|
1306
|
+
> components of your page.
|
|
1262
1307
|
|
|
1263
1308
|
`page-1.html`
|
|
1264
1309
|
|
|
@@ -1267,7 +1312,7 @@ export class BusinessForm {
|
|
|
1267
1312
|
[(model)]="form.identification.firstName"
|
|
1268
1313
|
name="identification.firstName"
|
|
1269
1314
|
/>
|
|
1270
|
-
<!-- 2
|
|
1315
|
+
<!-- 2 levels of depth -->
|
|
1271
1316
|
|
|
1272
1317
|
<foehn-input-text
|
|
1273
1318
|
[(model)]="form.identification.lastName"
|
|
@@ -1275,26 +1320,26 @@ export class BusinessForm {
|
|
|
1275
1320
|
/>
|
|
1276
1321
|
|
|
1277
1322
|
<foehn-boolean-checkbox [(model)]="form.acceptCookies" name="acceptCookies" />
|
|
1278
|
-
<!-- 1
|
|
1323
|
+
<!-- 1 level of depth -->
|
|
1279
1324
|
```
|
|
1280
1325
|
|
|
1281
|
-
-
|
|
1282
|
-
|
|
1283
|
-
-
|
|
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:
|
|
1284
1329
|
|
|
1285
1330
|
```java
|
|
1286
|
-
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")
|
|
1287
1332
|
```
|
|
1288
1333
|
|
|
1289
|
-
> ⚠️
|
|
1290
|
-
>
|
|
1334
|
+
> ⚠️ The name of the error must match the name of your component in your HTML
|
|
1335
|
+
> page.
|
|
1291
1336
|
|
|
1292
|
-
###
|
|
1337
|
+
### Manual handling
|
|
1293
1338
|
|
|
1294
1339
|
#### Back-end
|
|
1295
1340
|
|
|
1296
|
-
|
|
1297
|
-
|
|
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.
|
|
1298
1343
|
|
|
1299
1344
|
```java
|
|
1300
1345
|
@Getter
|
|
@@ -1308,23 +1353,23 @@ public class PostResponse<T> {
|
|
|
1308
1353
|
|
|
1309
1354
|
@Builder.Default
|
|
1310
1355
|
private List<AbstractSdkController.FormError> errors = new ArrayList<>();
|
|
1311
|
-
// AbstractSdkController.FormError
|
|
1356
|
+
// AbstractSdkController.FormError comes from prestations-be, the backend part of prestations-ng
|
|
1312
1357
|
}
|
|
1313
1358
|
```
|
|
1314
1359
|
|
|
1315
|
-
|
|
1360
|
+
Then, add your errors to your model before returning it to the front-end:
|
|
1316
1361
|
|
|
1317
1362
|
```java
|
|
1318
1363
|
List<AbstractSdkController.FormError> errors = new ArrayList<>();
|
|
1319
|
-
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"));
|
|
1320
1365
|
|
|
1321
1366
|
PostResponse.<MyObject>builder().dto(myObject).errors(errors).build();
|
|
1322
1367
|
```
|
|
1323
1368
|
|
|
1324
1369
|
#### Front-end
|
|
1325
1370
|
|
|
1326
|
-
|
|
1327
|
-
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.
|
|
1328
1373
|
|
|
1329
1374
|
```html
|
|
1330
1375
|
<foehn-input-text
|
|
@@ -1333,15 +1378,15 @@ page HTML et le nom de vos erreurs devront correspondre.
|
|
|
1333
1378
|
/>
|
|
1334
1379
|
```
|
|
1335
1380
|
|
|
1336
|
-
> ⚠️
|
|
1337
|
-
>
|
|
1381
|
+
> ⚠️ When your data has been saved, you will have to trigger the validation
|
|
1382
|
+
> manually.
|
|
1338
1383
|
|
|
1339
|
-
1.
|
|
1340
|
-
|
|
1341
|
-
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.
|
|
1342
1387
|
|
|
1343
|
-
> 💡
|
|
1344
|
-
> 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:
|
|
1345
1390
|
> `this.formWithModels()`.
|
|
1346
1391
|
|
|
1347
1392
|
`page.ts`
|
|
@@ -1358,41 +1403,41 @@ import {
|
|
|
1358
1403
|
// ...
|
|
1359
1404
|
})
|
|
1360
1405
|
export class ... extends AbstractPageComponent<YOUR_MODEL> implements OnInit {
|
|
1361
|
-
//
|
|
1406
|
+
// a viewChild signal instead of @ViewChild
|
|
1362
1407
|
readonly formWithModels = viewChild(FoehnFormComponent);
|
|
1363
1408
|
|
|
1364
|
-
//
|
|
1409
|
+
// your own dependencies with inject()
|
|
1365
1410
|
private readonly httpClient = inject(HttpClient);
|
|
1366
1411
|
|
|
1367
1412
|
ngOnInit(): void {
|
|
1368
1413
|
super.ngOnInit();
|
|
1369
|
-
//
|
|
1414
|
+
// Enable the display of the errors
|
|
1370
1415
|
this._validationHandlerService.shouldDisplayErrors(true);
|
|
1371
1416
|
}
|
|
1372
1417
|
|
|
1373
|
-
//
|
|
1418
|
+
// Save function
|
|
1374
1419
|
save(): void {
|
|
1375
|
-
//
|
|
1420
|
+
// Enable the display of the errors
|
|
1376
1421
|
this._validationHandlerService.shouldDisplayErrors(true);
|
|
1377
|
-
//
|
|
1378
|
-
//
|
|
1379
|
-
//
|
|
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.
|
|
1380
1425
|
this.httpClient
|
|
1381
|
-
.post<PostResponse>('
|
|
1426
|
+
.post<PostResponse>('api_url_here', your_object)
|
|
1382
1427
|
.subscribe((response: PostResponse) => {
|
|
1383
1428
|
if (!response.errors.length) {
|
|
1384
|
-
//
|
|
1429
|
+
// Tell the user that everything went well
|
|
1385
1430
|
this._growlService.addWithType(
|
|
1386
1431
|
GrowlType.SUCCESS,
|
|
1387
|
-
'
|
|
1432
|
+
'Saved successfully',
|
|
1388
1433
|
);
|
|
1389
1434
|
}
|
|
1390
|
-
//
|
|
1435
|
+
// Handling of the errors returned by the back-end
|
|
1391
1436
|
this.handleValidationErrors(response.errors); // point (2)
|
|
1392
1437
|
});
|
|
1393
1438
|
}
|
|
1394
1439
|
|
|
1395
|
-
//
|
|
1440
|
+
// Display of the errors
|
|
1396
1441
|
private handleValidationErrors(errors: ErrorTemplate[]): void {
|
|
1397
1442
|
// point (1)
|
|
1398
1443
|
const form = this.formWithModels();
|
|
@@ -1410,21 +1455,21 @@ export class ... extends AbstractPageComponent<YOUR_MODEL> implements OnInit {
|
|
|
1410
1455
|
|
|
1411
1456
|
```html
|
|
1412
1457
|
<foehn-form>
|
|
1413
|
-
<!-- ...
|
|
1458
|
+
<!-- ... your foehn-input-* components -->
|
|
1414
1459
|
</foehn-form>
|
|
1415
1460
|
```
|
|
1416
1461
|
|
|
1417
|
-
##
|
|
1462
|
+
## Manual validation of a component
|
|
1418
1463
|
|
|
1419
|
-
###
|
|
1464
|
+
### Hiding an error
|
|
1420
1465
|
|
|
1421
|
-
> ✅
|
|
1466
|
+
> ✅ The `markAsDirty()` function simulates an interaction with the component.
|
|
1422
1467
|
>
|
|
1423
|
-
> ℹ️
|
|
1468
|
+
> ℹ️ Hides the error of a foehn-input component in error.
|
|
1424
1469
|
|
|
1425
|
-
###
|
|
1470
|
+
### Displaying an error
|
|
1426
1471
|
|
|
1427
|
-
> ✅
|
|
1428
|
-
>
|
|
1472
|
+
> ✅ The `markAsPristine()` function puts the component back in its original state, as if
|
|
1473
|
+
> no interaction had happened.
|
|
1429
1474
|
>
|
|
1430
|
-
> ℹ️
|
|
1475
|
+
> ℹ️ Displays the error of a foehn-input component in error.
|