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