ngx-vest-forms 1.0.1
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/README.md +586 -0
- package/esm2022/lib/components/control-wrapper/control-wrapper.component.mjs +56 -0
- package/esm2022/lib/constants.mjs +2 -0
- package/esm2022/lib/directives/form-model-group.directive.mjs +34 -0
- package/esm2022/lib/directives/form-model.directive.mjs +34 -0
- package/esm2022/lib/directives/form.directive.mjs +167 -0
- package/esm2022/lib/directives/validate-root-form.directive.mjs +70 -0
- package/esm2022/lib/exports.mjs +63 -0
- package/esm2022/lib/utils/array-to-object.mjs +4 -0
- package/esm2022/lib/utils/deep-partial.mjs +2 -0
- package/esm2022/lib/utils/deep-required.mjs +2 -0
- package/esm2022/lib/utils/form-utils.mjs +163 -0
- package/esm2022/lib/utils/shape-validation.mjs +59 -0
- package/esm2022/ngx-vest-forms.mjs +5 -0
- package/esm2022/public-api.mjs +14 -0
- package/fesm2022/ngx-vest-forms.mjs +629 -0
- package/fesm2022/ngx-vest-forms.mjs.map +1 -0
- package/index.d.ts +5 -0
- package/lib/components/control-wrapper/control-wrapper.component.d.ts +17 -0
- package/lib/constants.d.ts +1 -0
- package/lib/directives/form-model-group.directive.d.ts +13 -0
- package/lib/directives/form-model.directive.d.ts +13 -0
- package/lib/directives/form.directive.d.ts +81 -0
- package/lib/directives/validate-root-form.directive.d.ts +22 -0
- package/lib/exports.d.ts +17 -0
- package/lib/utils/array-to-object.d.ts +3 -0
- package/lib/utils/deep-partial.d.ts +8 -0
- package/lib/utils/deep-required.d.ts +7 -0
- package/lib/utils/form-utils.d.ts +32 -0
- package/lib/utils/shape-validation.d.ts +15 -0
- package/package.json +36 -0
- package/public-api.d.ts +12 -0
package/README.md
ADDED
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
# ngx-vest-forms
|
|
2
|
+
|
|
3
|
+
### Introduction
|
|
4
|
+
|
|
5
|
+
This is a very lightweight adapter for Angular template-driven forms and [vestjs](https://vestjs.dev).
|
|
6
|
+
This package gives us the ability to create unidirectional forms without any boilerplate.
|
|
7
|
+
It is meant for complex forms with a high focus on complex validations and conditionals.
|
|
8
|
+
|
|
9
|
+
All the validations are asynchronous and use [vestjs](https://vestjs.dev) suites that can be re-used
|
|
10
|
+
across different frameworks and technologies.
|
|
11
|
+
|
|
12
|
+
### Installation
|
|
13
|
+
|
|
14
|
+
You can install the package by running:
|
|
15
|
+
|
|
16
|
+
```shell
|
|
17
|
+
npm i ngx-vest-forms
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### Creating a simple form
|
|
21
|
+
|
|
22
|
+
Let's start by explaining how to create a simple form.
|
|
23
|
+
I want a form with a form group called `general` info that has 2 properties:
|
|
24
|
+
- `firstName`
|
|
25
|
+
- `lastName`
|
|
26
|
+
|
|
27
|
+
We need to import the `vestForms` const in the imports section of the `@Component` decorator.
|
|
28
|
+
Now we can apply the `scVestForm` directive to the `form` tag and listen to the `formValueChange` output to feed our signal.
|
|
29
|
+
In the form we create a form group for `generalInfo` with the `ngModelGroup` directive.
|
|
30
|
+
And we crate 2 inputs with the `name` attribute and the `[ngModel]` input.
|
|
31
|
+
**Do note that we are not using the banana in the box syntax but only tha square brackets, resulting in a unidirectional dataflow**
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import { vestForms, DeepPartial } from 'ngx-vest-forms';
|
|
35
|
+
|
|
36
|
+
// A form model is always deep partial because angular will create it over time organically
|
|
37
|
+
type MyFormModel = DeepPartial<{
|
|
38
|
+
generalInfo: {
|
|
39
|
+
firstName: string;
|
|
40
|
+
lastName: string;
|
|
41
|
+
}
|
|
42
|
+
}>
|
|
43
|
+
|
|
44
|
+
@Component({
|
|
45
|
+
imports: [vestForms],
|
|
46
|
+
template: `
|
|
47
|
+
<form scVestForm
|
|
48
|
+
(formValueChange)="formValue.set($event)"
|
|
49
|
+
(ngSubmit)="onSubmit()">
|
|
50
|
+
<div ngModelGroup="generalInfo">
|
|
51
|
+
<label>First name</label>
|
|
52
|
+
<input type="text" name="firstName" [ngModel]="formValue().generalInformation?.firstName"/>
|
|
53
|
+
|
|
54
|
+
<label>Last name</label>
|
|
55
|
+
<input type="text" name="lastName" [ngModel]="formValue().generalInformation?.lastName"/>
|
|
56
|
+
</div>
|
|
57
|
+
</form>
|
|
58
|
+
`
|
|
59
|
+
})
|
|
60
|
+
export class MyComponent {
|
|
61
|
+
// This signal will hold the state of our form
|
|
62
|
+
protected readonly formValue = signal<MyFormModel>({});
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Note: Template-driven forms are deep partial, so always use the `?` operator in your templates.**
|
|
67
|
+
|
|
68
|
+
That's it! This will feed the `formValue` signal and angular will create a form group and 2 form controls for us automatically.
|
|
69
|
+
The object that will be fed in the `formValue` signal will look like this:
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
formValue = {
|
|
73
|
+
generalInfo: {
|
|
74
|
+
firstName: '',
|
|
75
|
+
lastName: ''
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The ngForm will contain automatically created FormGroups and FormControls.
|
|
81
|
+
This does not have anything to do with this package. It's just Angular:
|
|
82
|
+
```typescript
|
|
83
|
+
form = {
|
|
84
|
+
controls: {
|
|
85
|
+
generalInformation: { // FormGroup
|
|
86
|
+
controls: {
|
|
87
|
+
firstName: {...}, // FormControl
|
|
88
|
+
lastName: {...} //FormControl
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The `scVestForm` directive offers some basic outputs for us though:
|
|
96
|
+
|
|
97
|
+
| Output | Description |
|
|
98
|
+
|----------------------|-----------------------------------------------------------------------------------------------------------------------------------------|
|
|
99
|
+
| formValueChange<T> | Emits when the form value changes. But debounces<br/> the events since template-driven forms are created by the<br/>framework over time |
|
|
100
|
+
| dirtyChange<boolean> | Emits when the dirty state of the form changes |
|
|
101
|
+
| validChange<boolean> | Emits when the form becomes dirty or pristine |
|
|
102
|
+
| errorsChange | Emits an entire list of the form and all its form groups and controls |
|
|
103
|
+
|
|
104
|
+
### Avoiding typo's
|
|
105
|
+
|
|
106
|
+
Template-driven forms are type-safe, but not in the `name` attributes or `ngModelGroup` attributes.
|
|
107
|
+
Making a typo in those can result in a time-consuming endeavor. For this we have introduced shapes.
|
|
108
|
+
A shape is an object where the `scVestForm` can validate to. It is a deep required of the form model:
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
import { DeepPartial, DeepRequired, vestForms } from 'ngx-vest-forms';
|
|
112
|
+
|
|
113
|
+
type MyFormModel = DeepPartial<{
|
|
114
|
+
generalInfo: {
|
|
115
|
+
firstName: string;
|
|
116
|
+
lastName: string;
|
|
117
|
+
}
|
|
118
|
+
}>
|
|
119
|
+
|
|
120
|
+
export const myFormModelShape: DeepRequired<MyFormModel> = {
|
|
121
|
+
generalInfo: {
|
|
122
|
+
firstName: '',
|
|
123
|
+
lastName: ''
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
@Component({
|
|
128
|
+
imports: [vestForms],
|
|
129
|
+
template: `
|
|
130
|
+
<form scVestForm
|
|
131
|
+
[formShape]="shape"
|
|
132
|
+
(formValueChange)="formValue.set($event)"
|
|
133
|
+
(ngSubmit)="onSubmit()">
|
|
134
|
+
|
|
135
|
+
<div ngModelGroup="generalInfo">
|
|
136
|
+
<label>First name</label>
|
|
137
|
+
<input type="text" name="firstName" [ngModel]="formValue().generalInformation?.firstName"/>
|
|
138
|
+
|
|
139
|
+
<label>Last name</label>
|
|
140
|
+
<input type="text" name="lastName" [ngModel]="formValue().generalInformation?.lastName"/>
|
|
141
|
+
</div>
|
|
142
|
+
</form>
|
|
143
|
+
`
|
|
144
|
+
})
|
|
145
|
+
export class MyComponent {
|
|
146
|
+
protected readonly formValue = signal<MyFormModel>({});
|
|
147
|
+
protected readonly shape = myFormModelShape;
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
By passing the shape to the `formShape` input the `scVestForm` will validate the actual form value
|
|
152
|
+
against the form shape every time the form changes, but only when Angular is in devMode.
|
|
153
|
+
|
|
154
|
+
Making a typo in the name attribute or an ngModelGroup attribute would result in runtime errors.
|
|
155
|
+
The console would look like this:
|
|
156
|
+
|
|
157
|
+
```chatinput
|
|
158
|
+
Error: Shape mismatch:
|
|
159
|
+
|
|
160
|
+
[ngModel] Mismatch 'firstame'
|
|
161
|
+
[ngModelGroup] Mismatch: 'addresses.billingddress'
|
|
162
|
+
[ngModel] Mismatch 'addresses.billingddress.steet'
|
|
163
|
+
[ngModel] Mismatch 'addresses.billingddress.number'
|
|
164
|
+
[ngModel] Mismatch 'addresses.billingddress.city'
|
|
165
|
+
[ngModel] Mismatch 'addresses.billingddress.zipcode'
|
|
166
|
+
[ngModel] Mismatch 'addresses.billingddress.country'
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
at validateShape (shape-validation.ts:28:19)
|
|
170
|
+
at Object.next (form.directive.ts:178:17)
|
|
171
|
+
at ConsumerObserver.next (Subscriber.js:91:33)
|
|
172
|
+
at SafeSubscriber._next (Subscriber.js:60:26)
|
|
173
|
+
at SafeSubscriber.next (Subscriber.js:31:18)
|
|
174
|
+
at subscribe.innerSubscriber (switchMap.js:14:144)
|
|
175
|
+
at OperatorSubscriber._next (OperatorSubscriber.js:13:21)
|
|
176
|
+
at OperatorSubscriber.next (Subscriber.js:31:18)
|
|
177
|
+
at map.js:7:24
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Conditional fields
|
|
181
|
+
|
|
182
|
+
What if we want to remove a form control or form group? With reactive forms that would require a lot of work
|
|
183
|
+
but since Template driven forms do all the hard work for us, we can simply create a computed signal for that and
|
|
184
|
+
bind that in the template. Having logic in the template is considered a bad practice, so we can do all
|
|
185
|
+
the calculations in our class.
|
|
186
|
+
|
|
187
|
+
Let's hide `lastName` if `firstName` is not filled in:
|
|
188
|
+
|
|
189
|
+
```html
|
|
190
|
+
<div ngModelGroup="generalInfo">
|
|
191
|
+
<label>First name</label>
|
|
192
|
+
<input type="text" name="firstName" [ngModel]="formValue().generalInformation?.firstName"/>
|
|
193
|
+
|
|
194
|
+
@if(lastNameAvailable()){
|
|
195
|
+
<label>Last name</label>
|
|
196
|
+
<input type="text" name="lastName" [ngModel]="formValue().generalInformation?.lastName"/>
|
|
197
|
+
}
|
|
198
|
+
</div>
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
class MyComponent {
|
|
203
|
+
...
|
|
204
|
+
protected readonly lastNameAvailable =
|
|
205
|
+
computed(() => !!this.formValue().generalInformation?.firstName);
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
This will automatically add and remove the form control from our form model.
|
|
210
|
+
This also works for a form group:
|
|
211
|
+
|
|
212
|
+
```html
|
|
213
|
+
@if(showGeneralInfo()){
|
|
214
|
+
<div ngModelGroup="generalInfo">
|
|
215
|
+
<label>First name</label>
|
|
216
|
+
<input type="text" name="firstName" [ngModel]="formValue().generalInformation?.firstName"/>
|
|
217
|
+
|
|
218
|
+
<label>Last name</label>
|
|
219
|
+
<input type="text" name="lastName" [ngModel]="formValue().generalInformation?.lastName"/>
|
|
220
|
+
</div>
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Reactive disabling
|
|
225
|
+
|
|
226
|
+
To achieve reactive disabling, we just have to take advantage of computed signals as well:
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
class MyComponent {
|
|
230
|
+
protected readonly lastNameDisabled =
|
|
231
|
+
computed(() => !this.formValue().generalInformation?.firstName);
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
We can bind the computed signal to the `disabled` directive of Angular.
|
|
236
|
+
```html
|
|
237
|
+
<input type="text" name="lastName"
|
|
238
|
+
[disabled]="lastNameDisabled()"
|
|
239
|
+
[ngModel]="formValue().generalInformation?.lastName"/>
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Validations
|
|
243
|
+
|
|
244
|
+
The absolute gem in ngx-vest-forms is the flexibility in validations without writing any boilerplate.
|
|
245
|
+
The only dependency this lib has is [vest.js](https://vestjs.dev). An awesome lightweight validation framework.
|
|
246
|
+
You can use it on the backend/frontend/Angular/react etc...
|
|
247
|
+
|
|
248
|
+
We use vest because it introduces the concept of vest suites. These are suites that kind of look like unit-tests
|
|
249
|
+
but that are highly flexible:
|
|
250
|
+
* [X] Write validations on forms
|
|
251
|
+
* [X] Write validations on form groups
|
|
252
|
+
* [X] Write validations on form controls
|
|
253
|
+
* [X] Composable/reuse-able different validation suites
|
|
254
|
+
* [X] Write conditional validations
|
|
255
|
+
|
|
256
|
+
This is how you write a simple Vest suite:
|
|
257
|
+
```typescript
|
|
258
|
+
import { enforce, only, staticSuite, test } from 'vest';
|
|
259
|
+
import { MyFormModel } from '../models/my-form.model'
|
|
260
|
+
|
|
261
|
+
export const myFormModelSuite = staticSuite(
|
|
262
|
+
(model: MyformModel, field?: string) => {
|
|
263
|
+
if (field) {
|
|
264
|
+
// Needed to not run every validation every time
|
|
265
|
+
only(field);
|
|
266
|
+
}
|
|
267
|
+
test('firstName', 'First name is required', () => {
|
|
268
|
+
enforce(model.firstName).isNotBlank();
|
|
269
|
+
});
|
|
270
|
+
test('lastName', 'Last name is required', () => {
|
|
271
|
+
enforce(model.lastName).isNotBlank();
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
);
|
|
275
|
+
};
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
In the `test` function the first parameter is the field, the second is the validation error.
|
|
279
|
+
The field is separated with the `.` syntax. So if we would have an `addresses` form group with an `billingAddress` form group inside
|
|
280
|
+
and a form control `street` the field would be: `addresses.billingAddress.street`.
|
|
281
|
+
|
|
282
|
+
This syntax should be self-explanatory and the entire enforcements guidelines can be found on [vest.js](https://vestjs.dev).
|
|
283
|
+
|
|
284
|
+
Now let's connect this to our form. This is the biggest pain that ngx-vest-forms will fix for you: **Connecting Vest suites to Angular**
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
class MyComponent {
|
|
288
|
+
protected readonly formValue = signal<MyFormModel>({});
|
|
289
|
+
protected readonly suite = myFormModelSuite;
|
|
290
|
+
}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
```html
|
|
294
|
+
|
|
295
|
+
<form scVestForm
|
|
296
|
+
[formShape]="shape"
|
|
297
|
+
[formValue]="formValue"
|
|
298
|
+
[suite]="suite"
|
|
299
|
+
(formValueChange)="formValue.set($event)"
|
|
300
|
+
(ngSubmit)="onSubmit()">
|
|
301
|
+
...
|
|
302
|
+
</form>
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
That's it. Validations are completely wired now. Because ngx-vest-forms will hook into the
|
|
306
|
+
`[ngModel]` and `ngModelGroup` attributes, and create ngValidators automatically.
|
|
307
|
+
|
|
308
|
+
It goes like this:
|
|
309
|
+
- Control gets created, Angular recognizes the `ngModel` and `ngModelGroup` directives
|
|
310
|
+
- These directives implement `AsyncValidator` and will connect to a vest suite
|
|
311
|
+
- User types into control
|
|
312
|
+
- The validate function gets called
|
|
313
|
+
- Vest gets called for one field
|
|
314
|
+
- Vest returns the errors
|
|
315
|
+
- @simpilfied/forms puts those errors on the angular form control
|
|
316
|
+
|
|
317
|
+
This means that `valid`, `invalid`, `errors`, `statusChanges` etc will keep on working
|
|
318
|
+
just like it would with a regular angular form.
|
|
319
|
+
|
|
320
|
+
#### Showing validation errors
|
|
321
|
+
|
|
322
|
+
Now we want to show the validation errors in a consistent way.
|
|
323
|
+
For that we have provided the `sc-control-wrapper` attribute component.
|
|
324
|
+
|
|
325
|
+
You can use it on:
|
|
326
|
+
- elements that hold `ngModelGroup`
|
|
327
|
+
- elements that have an `ngModel` (or form control) inside of them.
|
|
328
|
+
|
|
329
|
+
This will show errors automatically on:
|
|
330
|
+
- form submit
|
|
331
|
+
- blur
|
|
332
|
+
|
|
333
|
+
**Note:** If those requirements don't fill your need, you can write a custom control-wrapper by copy-pasting the
|
|
334
|
+
`control-wrapper` and adjusting the code.
|
|
335
|
+
|
|
336
|
+
Let's update our form:
|
|
337
|
+
|
|
338
|
+
```html
|
|
339
|
+
|
|
340
|
+
<div ngModelGroup="generalInfo" sc-control-wrapper>
|
|
341
|
+
<div sc-control-wrapper>
|
|
342
|
+
<label>First name</label
|
|
343
|
+
<input type="text" name="firstName" [ngModel]="formValue().generalInformation?.firstName"/>
|
|
344
|
+
</div>
|
|
345
|
+
|
|
346
|
+
<div sc-control-wrapper>
|
|
347
|
+
<label>Last name</label>
|
|
348
|
+
<input type="text" name="lastName" [ngModel]="formValue().generalInformation?.lastName"/>
|
|
349
|
+
</div>
|
|
350
|
+
</div>
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
This is the only thing we need to do to create a form that is completely wired with vest.
|
|
354
|
+
* [x] Automatic creation of form controls and form groups
|
|
355
|
+
* [x] Automatic connection to vest suites
|
|
356
|
+
* [x] Automatic typo validation
|
|
357
|
+
* [x] Automatic adding of css error classes and showing validation messages
|
|
358
|
+
* [x] On blur
|
|
359
|
+
* [x] On submit
|
|
360
|
+
|
|
361
|
+
### Conditional validations
|
|
362
|
+
|
|
363
|
+
Vest makes it extremely easy to create conditional validations.
|
|
364
|
+
Assume we have a form model that has `age` and `emergencyContact`.
|
|
365
|
+
The `emergencyContact` is required, but only when the person is not of legal age.
|
|
366
|
+
|
|
367
|
+
We can use the `omitWhen` so that when the person is below 18, the assertion
|
|
368
|
+
will not be done.
|
|
369
|
+
|
|
370
|
+
```typescript
|
|
371
|
+
import { enforce, omitWhen, only, staticSuite, test } from 'vest';
|
|
372
|
+
|
|
373
|
+
...
|
|
374
|
+
omitWhen((model.age || 0) >= 18, () => {
|
|
375
|
+
test('emergencyContact', 'Emergency contact is required', () => {
|
|
376
|
+
enforce(model.emergencyContact).isNotBlank();
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
You can put those validations on every field that you want. On form group fields and on form control fields.
|
|
382
|
+
Check this interesting example below:
|
|
383
|
+
|
|
384
|
+
* [x] Password is always required
|
|
385
|
+
* [x] Confirm password is only required when there is a password
|
|
386
|
+
* [x] The passwords should match, but only when they are both filled in
|
|
387
|
+
|
|
388
|
+
```typescript
|
|
389
|
+
test('passwords.password', 'Password is not filled in', () => {
|
|
390
|
+
enforce(model.passwords?.password).isNotBlank();
|
|
391
|
+
});
|
|
392
|
+
omitWhen(!model.passwords?.password, () => {
|
|
393
|
+
test('passwords.confirmPassword', 'Confirm password is not filled in', () => {
|
|
394
|
+
enforce(model.passwords?.confirmPassword).isNotBlank();
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
omitWhen(!model.passwords?.password || !model.passwords?.confirmPassword, () => {
|
|
398
|
+
test('passwords', 'Passwords do not match', () => {
|
|
399
|
+
enforce(model.passwords?.confirmPassword).equals(model.passwords?.password);
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
Forget about manually adding, removing validators on reactive forms and not being able to
|
|
405
|
+
re-use them. This code is easy to test, easy to re-use on frontend, backend, angular, react, etc...
|
|
406
|
+
**Oh, it's also pretty readable**
|
|
407
|
+
|
|
408
|
+
### Composable validations
|
|
409
|
+
|
|
410
|
+
We can compose validations suites with sub suites. After all, we want to re-use certain pieces of our
|
|
411
|
+
validation logic and we don't want one huge unreadable suite.
|
|
412
|
+
This is quite straightforward with Vest.
|
|
413
|
+
|
|
414
|
+
Let's take this simple function that validates an address:
|
|
415
|
+
|
|
416
|
+
```typescript
|
|
417
|
+
export function addressValidations(model: AddressModel | undefined, field: string): void {
|
|
418
|
+
test(`${field}.street`, 'Street is required', () => {
|
|
419
|
+
enforce(model?.street).isNotBlank();
|
|
420
|
+
});
|
|
421
|
+
test(`${field}.city`, 'City is required', () => {
|
|
422
|
+
enforce(model?.city).isNotBlank();
|
|
423
|
+
});
|
|
424
|
+
test(`${field}.zipcode`, 'Zipcode is required', () => {
|
|
425
|
+
enforce(model?.zipcode).isNotBlank();
|
|
426
|
+
});
|
|
427
|
+
test(`${field}.number`, 'Number is required', () => {
|
|
428
|
+
enforce(model?.number).isNotBlank();
|
|
429
|
+
});
|
|
430
|
+
test(`${field}.country`, 'Country is required', () => {
|
|
431
|
+
enforce(model?.country).isNotBlank();
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
Our suite would consume it like this:
|
|
437
|
+
|
|
438
|
+
```typescript
|
|
439
|
+
import { enforce, omitWhen, only, staticSuite, test } from 'vest';
|
|
440
|
+
import { PurchaseFormModel } from '../models/purchaseFormModel';
|
|
441
|
+
|
|
442
|
+
export const mySuite = staticSuite(
|
|
443
|
+
(model: PurchaseFormModel, field?: string) => {
|
|
444
|
+
if (field) {
|
|
445
|
+
only(field);
|
|
446
|
+
}
|
|
447
|
+
addressValidations(model.addresses?.billingAddress, 'addresses.billingAddress');
|
|
448
|
+
addressValidations(model.addresses?.shippingAddress, 'addresses.shippingAddress');
|
|
449
|
+
}
|
|
450
|
+
);
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
We achieved decoupling, readability and reuse of our addressValidations.
|
|
454
|
+
|
|
455
|
+
#### A more complex example
|
|
456
|
+
|
|
457
|
+
Let's combine the conditional part with the reusable part.
|
|
458
|
+
We have 2 addresses, but the shippingAddress is only required when the `shippingAddressIsDifferentFromBillingAddress`
|
|
459
|
+
Checkbox is checked. But if it is checked, all fields are required.
|
|
460
|
+
And if both addresses are filled in, they should be different.
|
|
461
|
+
|
|
462
|
+
This gives us validation on:
|
|
463
|
+
* [x] The addresses form field (they can't be equal)
|
|
464
|
+
* [x] The shipping Address field (only required when checkbox is checked)
|
|
465
|
+
* [x] validation on all the address fields (street, number, etc) on both addresses
|
|
466
|
+
|
|
467
|
+
```typescript
|
|
468
|
+
addressValidations(
|
|
469
|
+
model.addresses?.billingAddress,
|
|
470
|
+
'addresses.billingAddress'
|
|
471
|
+
);
|
|
472
|
+
omitWhen(
|
|
473
|
+
!model.addresses?.shippingAddressDifferentFromBillingAddress,
|
|
474
|
+
() => {
|
|
475
|
+
addressValidations(
|
|
476
|
+
model.addresses?.shippingAddress,
|
|
477
|
+
'addresses.shippingAddress'
|
|
478
|
+
);
|
|
479
|
+
test('addresses', 'The addresses appear to be the same', () => {
|
|
480
|
+
enforce(JSON.stringify(model.addresses?.billingAddress)).notEquals(
|
|
481
|
+
JSON.stringify(model.addresses?.shippingAddress)
|
|
482
|
+
);
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
);
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
### Validations on the root form
|
|
489
|
+
|
|
490
|
+
When we want to validate multiple fields that are depending on each other,
|
|
491
|
+
it is a best practice to wrap them in a parent form group.
|
|
492
|
+
If `password` and `confirmPassword` have to be equal the validation should not happen on
|
|
493
|
+
`password` nor on `confirmPassword`, it should happen on `passwords`:
|
|
494
|
+
|
|
495
|
+
```typescript
|
|
496
|
+
const form = {
|
|
497
|
+
// validation happens here
|
|
498
|
+
passwords: {
|
|
499
|
+
password: '',
|
|
500
|
+
confirmPassword: ''
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
Sometimes we don't have the ability to create a form group for 2 depending fields, or sometimes we just
|
|
506
|
+
want to create validation rules on portions of the form. For that we can use `validateRootForm`.
|
|
507
|
+
Use the `errorsChange` output to keep the errors as state in a signal that we can use in the template
|
|
508
|
+
wherever we want.
|
|
509
|
+
|
|
510
|
+
```html
|
|
511
|
+
{{ errors()?.['rootForm'] }} <!-- render the errors on the rootForm -->
|
|
512
|
+
{{ errors() }} <!-- render all the errors -->
|
|
513
|
+
<form scVestForm
|
|
514
|
+
[formValue]="formValue()"
|
|
515
|
+
[validateRootForm]="true"
|
|
516
|
+
[formShape]="shape"
|
|
517
|
+
[suite]="suite"
|
|
518
|
+
(errorsChange)="errors.set($event)"
|
|
519
|
+
...>
|
|
520
|
+
</form>
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
```typescript
|
|
524
|
+
export class MyformComponent {
|
|
525
|
+
protected readonly formValue = signal<MyFormModel>({});
|
|
526
|
+
protected readonly suite = myFormModelSuite;
|
|
527
|
+
// Keep the errors in state
|
|
528
|
+
protected readonly errors = signal<Record<string, string>>({ });
|
|
529
|
+
}
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
When setting the `[validateRootForm]` directive to true, the form will
|
|
533
|
+
also create an ngValidator on root level, that listens to the ROOT_FORM field.
|
|
534
|
+
|
|
535
|
+
To make this work we need to use the field in the vest suite like this:
|
|
536
|
+
|
|
537
|
+
```typescript
|
|
538
|
+
import { ROOT_FORM } from 'ngx-vest-forms';
|
|
539
|
+
|
|
540
|
+
test(ROOT_FORM, 'Brecht is not 30 anymore', () => {
|
|
541
|
+
enforce(
|
|
542
|
+
model.firstName === 'Brecht' &&
|
|
543
|
+
model.lastName === 'Billiet' &&
|
|
544
|
+
model.age === 30).isFalsy();
|
|
545
|
+
});
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
### Validation dependencies
|
|
549
|
+
|
|
550
|
+
Sometimes we need to re-trigger validations of form controls or form groups because they are dependant on other form controls or form groups.
|
|
551
|
+
For instance: A `confirmPassword` field is not required unless the `password` is filled in. Which means that when the `password` field gets a new value,
|
|
552
|
+
we need to run the validations on `confirmPassword`.
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
### Form arrays
|
|
556
|
+
|
|
557
|
+
Todo
|
|
558
|
+
|
|
559
|
+
#### Form array validations
|
|
560
|
+
|
|
561
|
+
An example can be found [in this simplified courses article](https://blog.simplified.courses/template-driven-forms-with-form-arrays/)
|
|
562
|
+
|
|
563
|
+
We can look in `projects/examples/src/app/validations/phonenumber.validations.ts` to see an example on the validations part.
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
### Child form components
|
|
567
|
+
|
|
568
|
+
Big forms result in big files. It makes sense to split them up.
|
|
569
|
+
For instance an address form can be reused, so we want to create a child component for that.
|
|
570
|
+
We have to make sure that this child component can access the ngForm.
|
|
571
|
+
For that we have to use the `vestFormViewProviders` from `ngx-vest-forms`
|
|
572
|
+
|
|
573
|
+
```typescript
|
|
574
|
+
...
|
|
575
|
+
import { vestForms, vestFormsViewProviders } from 'ngx-vest-forms';
|
|
576
|
+
|
|
577
|
+
@Component({
|
|
578
|
+
...
|
|
579
|
+
viewProviders: [vestFormsViewProviders]
|
|
580
|
+
})
|
|
581
|
+
export class AddressComponent {
|
|
582
|
+
@Input() address?: AddressModel;
|
|
583
|
+
}
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
You can check the examples in the github repo [here](https://github.com/simplifiedcourses/ngx-vest-forms/blob/master/projects/examples).
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, DestroyRef, HostBinding, inject } from '@angular/core';
|
|
2
|
+
import { NgModel, NgModelGroup } from '@angular/forms';
|
|
3
|
+
import { mergeWith, of, switchMap } from 'rxjs';
|
|
4
|
+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
5
|
+
import { FormDirective } from '../../directives/form.directive';
|
|
6
|
+
import * as i0 from "@angular/core";
|
|
7
|
+
export class ControlWrapperComponent {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.cdRef = inject(ChangeDetectorRef);
|
|
10
|
+
this.formDirective = inject(FormDirective);
|
|
11
|
+
this.destroyRef = inject(DestroyRef);
|
|
12
|
+
this.ngModelGroup = inject(NgModelGroup, {
|
|
13
|
+
optional: true,
|
|
14
|
+
self: true,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
get control() {
|
|
18
|
+
return this.ngModelGroup ? this.ngModelGroup.control : this.ngModel?.control;
|
|
19
|
+
}
|
|
20
|
+
get invalid() {
|
|
21
|
+
return this.control?.touched && this.errors;
|
|
22
|
+
}
|
|
23
|
+
get errors() {
|
|
24
|
+
if (this.control?.pending) {
|
|
25
|
+
return this.previousError;
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
this.previousError = this.control?.errors?.['errors'];
|
|
29
|
+
}
|
|
30
|
+
return this.control?.errors?.['errors'];
|
|
31
|
+
}
|
|
32
|
+
ngAfterViewInit() {
|
|
33
|
+
// Wait until the form is idle
|
|
34
|
+
// Then, listen to all events of the ngModelGroup or ngModel
|
|
35
|
+
// and mark the component and its ancestors as dirty
|
|
36
|
+
// This allows us to use the OnPush ChangeDetection Strategy
|
|
37
|
+
this.formDirective.idle$
|
|
38
|
+
.pipe(switchMap(() => this.ngModelGroup?.control?.events || of(null)), mergeWith(this.control?.events || of(null)), takeUntilDestroyed(this.destroyRef))
|
|
39
|
+
.subscribe(() => {
|
|
40
|
+
this.cdRef.markForCheck();
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.1", ngImport: i0, type: ControlWrapperComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
44
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.0.1", type: ControlWrapperComponent, isStandalone: true, selector: "[sc-control-wrapper]", host: { properties: { "class.sc-control-wrapper--invalid": "this.invalid" } }, queries: [{ propertyName: "ngModel", first: true, predicate: NgModel, descendants: true }], ngImport: i0, template: "<div class=\"sc-control-wrapper\">\n <div class=\"sc-control-wrapper__content\">\n <ng-content></ng-content>\n </div>\n <div class=\"sc-control-wrapper__errors\">\n <ul [hidden]=\"!invalid\">\n @for (error of errors; track error) {\n <li>{{ error }}</li>\n }\n </ul>\n </div>\n</div>\n", styles: [""], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
45
|
+
}
|
|
46
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.1", ngImport: i0, type: ControlWrapperComponent, decorators: [{
|
|
47
|
+
type: Component,
|
|
48
|
+
args: [{ selector: '[sc-control-wrapper]', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"sc-control-wrapper\">\n <div class=\"sc-control-wrapper__content\">\n <ng-content></ng-content>\n </div>\n <div class=\"sc-control-wrapper__errors\">\n <ul [hidden]=\"!invalid\">\n @for (error of errors; track error) {\n <li>{{ error }}</li>\n }\n </ul>\n </div>\n</div>\n" }]
|
|
49
|
+
}], propDecorators: { ngModel: [{
|
|
50
|
+
type: ContentChild,
|
|
51
|
+
args: [NgModel]
|
|
52
|
+
}], invalid: [{
|
|
53
|
+
type: HostBinding,
|
|
54
|
+
args: ['class.sc-control-wrapper--invalid']
|
|
55
|
+
}] } });
|
|
56
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29udHJvbC13cmFwcGVyLmNvbXBvbmVudC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uLy4uL3Byb2plY3RzL25neC12ZXN0LWZvcm1zL3NyYy9saWIvY29tcG9uZW50cy9jb250cm9sLXdyYXBwZXIvY29udHJvbC13cmFwcGVyLmNvbXBvbmVudC50cyIsIi4uLy4uLy4uLy4uLy4uLy4uL3Byb2plY3RzL25neC12ZXN0LWZvcm1zL3NyYy9saWIvY29tcG9uZW50cy9jb250cm9sLXdyYXBwZXIvY29udHJvbC13cmFwcGVyLmNvbXBvbmVudC5odG1sIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFFSCx1QkFBdUIsRUFDdkIsaUJBQWlCLEVBQ2pCLFNBQVMsRUFDVCxZQUFZLEVBQ1osVUFBVSxFQUNWLFdBQVcsRUFDWCxNQUFNLEVBQ1QsTUFBTSxlQUFlLENBQUM7QUFFdkIsT0FBTyxFQUFtQixPQUFPLEVBQUUsWUFBWSxFQUFFLE1BQU0sZ0JBQWdCLENBQUM7QUFDeEUsT0FBTyxFQUFFLFNBQVMsRUFBRSxFQUFFLEVBQUUsU0FBUyxFQUFFLE1BQU0sTUFBTSxDQUFDO0FBQ2hELE9BQU8sRUFBRSxrQkFBa0IsRUFBRSxNQUFNLDRCQUE0QixDQUFDO0FBQ2hFLE9BQU8sRUFBRSxhQUFhLEVBQUUsTUFBTSxpQ0FBaUMsQ0FBQzs7QUFTaEUsTUFBTSxPQUFPLHVCQUF1QjtJQVBwQztRQVFxQixVQUFLLEdBQUcsTUFBTSxDQUFDLGlCQUFpQixDQUFDLENBQUM7UUFDbEMsa0JBQWEsR0FBRyxNQUFNLENBQUMsYUFBYSxDQUFDLENBQUM7UUFDdEMsZUFBVSxHQUFHLE1BQU0sQ0FBQyxVQUFVLENBQUMsQ0FBQztRQUdqQyxpQkFBWSxHQUF3QixNQUFNLENBQUMsWUFBWSxFQUFFO1lBQ3JFLFFBQVEsRUFBRSxJQUFJO1lBQ2QsSUFBSSxFQUFFLElBQUk7U0FDYixDQUFDLENBQUM7S0F1Q047SUFqQ0csSUFBWSxPQUFPO1FBQ2YsT0FBTyxJQUFJLENBQUMsWUFBWSxDQUFDLENBQUMsQ0FBQyxJQUFJLENBQUMsWUFBWSxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDLE9BQU8sRUFBRSxPQUFPLENBQUM7SUFDakYsQ0FBQztJQUVELElBQ1csT0FBTztRQUNkLE9BQU8sSUFBSSxDQUFDLE9BQU8sRUFBRSxPQUFPLElBQUksSUFBSSxDQUFDLE1BQU0sQ0FBQztJQUNoRCxDQUFDO0lBRUQsSUFBVyxNQUFNO1FBQ2IsSUFBSSxJQUFJLENBQUMsT0FBTyxFQUFFLE9BQU8sRUFBRSxDQUFDO1lBQ3hCLE9BQU8sSUFBSSxDQUFDLGFBQWEsQ0FBQztRQUM5QixDQUFDO2FBQU0sQ0FBQztZQUNKLElBQUksQ0FBQyxhQUFhLEdBQUcsSUFBSSxDQUFDLE9BQU8sRUFBRSxNQUFNLEVBQUUsQ0FBQyxRQUFRLENBQUMsQ0FBQztRQUMxRCxDQUFDO1FBQ0QsT0FBTyxJQUFJLENBQUMsT0FBTyxFQUFFLE1BQU0sRUFBRSxDQUFDLFFBQVEsQ0FBQyxDQUFDO0lBQzVDLENBQUM7SUFFTSxlQUFlO1FBQ2xCLDhCQUE4QjtRQUM5Qiw0REFBNEQ7UUFDNUQsb0RBQW9EO1FBQ3BELDREQUE0RDtRQUM1RCxJQUFJLENBQUMsYUFBYSxDQUFDLEtBQUs7YUFDbkIsSUFBSSxDQUNELFNBQVMsQ0FBQyxHQUFHLEVBQUUsQ0FBQyxJQUFJLENBQUMsWUFBWSxFQUFFLE9BQU8sRUFBRSxNQUFNLElBQUksRUFBRSxDQUFDLElBQUksQ0FBQyxDQUFDLEVBQy9ELFNBQVMsQ0FBQyxJQUFJLENBQUMsT0FBTyxFQUFFLE1BQU0sSUFBSSxFQUFFLENBQUMsSUFBSSxDQUFDLENBQUMsRUFDM0Msa0JBQWtCLENBQUMsSUFBSSxDQUFDLFVBQVUsQ0FBQyxDQUN0QzthQUNBLFNBQVMsQ0FBQyxHQUFHLEVBQUU7WUFDWixJQUFJLENBQUMsS0FBSyxDQUFDLFlBQVksRUFBRSxDQUFDO1FBQzlCLENBQUMsQ0FBQyxDQUFDO0lBQ1gsQ0FBQzs4R0EvQ1EsdUJBQXVCO2tHQUF2Qix1QkFBdUIsb01BS2xCLE9BQU8sZ0RDNUJ6Qiw2VEFZQTs7MkZEV2EsdUJBQXVCO2tCQVBuQyxTQUFTOytCQUNJLHNCQUFzQixjQUNwQixJQUFJLG1CQUdDLHVCQUF1QixDQUFDLE1BQU07OEJBT2pCLE9BQU87c0JBQXBDLFlBQVk7dUJBQUMsT0FBTztnQkFlVixPQUFPO3NCQURqQixXQUFXO3VCQUFDLG1DQUFtQyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7XG4gICAgQWZ0ZXJWaWV3SW5pdCxcbiAgICBDaGFuZ2VEZXRlY3Rpb25TdHJhdGVneSxcbiAgICBDaGFuZ2VEZXRlY3RvclJlZixcbiAgICBDb21wb25lbnQsXG4gICAgQ29udGVudENoaWxkLFxuICAgIERlc3Ryb3lSZWYsXG4gICAgSG9zdEJpbmRpbmcsXG4gICAgaW5qZWN0XG59IGZyb20gJ0Bhbmd1bGFyL2NvcmUnO1xuXG5pbXBvcnQgeyBBYnN0cmFjdENvbnRyb2wsIE5nTW9kZWwsIE5nTW9kZWxHcm91cCB9IGZyb20gJ0Bhbmd1bGFyL2Zvcm1zJztcbmltcG9ydCB7IG1lcmdlV2l0aCwgb2YsIHN3aXRjaE1hcCB9IGZyb20gJ3J4anMnO1xuaW1wb3J0IHsgdGFrZVVudGlsRGVzdHJveWVkIH0gZnJvbSAnQGFuZ3VsYXIvY29yZS9yeGpzLWludGVyb3AnO1xuaW1wb3J0IHsgRm9ybURpcmVjdGl2ZSB9IGZyb20gJy4uLy4uL2RpcmVjdGl2ZXMvZm9ybS5kaXJlY3RpdmUnO1xuXG5AQ29tcG9uZW50KHtcbiAgICBzZWxlY3RvcjogJ1tzYy1jb250cm9sLXdyYXBwZXJdJyxcbiAgICBzdGFuZGFsb25lOiB0cnVlLFxuICAgIHRlbXBsYXRlVXJsOiAnLi9jb250cm9sLXdyYXBwZXIuY29tcG9uZW50Lmh0bWwnLFxuICAgIHN0eWxlVXJsczogWycuL2NvbnRyb2wtd3JhcHBlci5jb21wb25lbnQuc2NzcyddLFxuICAgIGNoYW5nZURldGVjdGlvbjogQ2hhbmdlRGV0ZWN0aW9uU3RyYXRlZ3kuT25QdXNoXG59KVxuZXhwb3J0IGNsYXNzIENvbnRyb2xXcmFwcGVyQ29tcG9uZW50IGltcGxlbWVudHMgQWZ0ZXJWaWV3SW5pdCB7XG4gICAgcHJpdmF0ZSByZWFkb25seSBjZFJlZiA9IGluamVjdChDaGFuZ2VEZXRlY3RvclJlZik7XG4gICAgcHJpdmF0ZSByZWFkb25seSBmb3JtRGlyZWN0aXZlID0gaW5qZWN0KEZvcm1EaXJlY3RpdmUpO1xuICAgIHByaXZhdGUgcmVhZG9ubHkgZGVzdHJveVJlZiA9IGluamVjdChEZXN0cm95UmVmKTtcblxuICAgIEBDb250ZW50Q2hpbGQoTmdNb2RlbCkgcHVibGljIG5nTW9kZWw/OiBOZ01vZGVsOyAvLyBPcHRpb25hbCBuZ01vZGVsXG4gICAgcHVibGljIHJlYWRvbmx5IG5nTW9kZWxHcm91cDogTmdNb2RlbEdyb3VwIHwgbnVsbCA9IGluamVjdChOZ01vZGVsR3JvdXAsIHtcbiAgICAgICAgb3B0aW9uYWw6IHRydWUsXG4gICAgICAgIHNlbGY6IHRydWUsXG4gICAgfSk7XG5cblxuICAgIC8vIENhY2hlIHRoZSBwcmV2aW91cyBlcnJvciB0byBhdm9pZCAnZmxpY2tlcmluZydcbiAgICBwcml2YXRlIHByZXZpb3VzRXJyb3I/OiBzdHJpbmdbXTtcblxuICAgIHByaXZhdGUgZ2V0IGNvbnRyb2woKTogQWJzdHJhY3RDb250cm9sIHwgdW5kZWZpbmVkIHtcbiAgICAgICAgcmV0dXJuIHRoaXMubmdNb2RlbEdyb3VwID8gdGhpcy5uZ01vZGVsR3JvdXAuY29udHJvbCA6IHRoaXMubmdNb2RlbD8uY29udHJvbDtcbiAgICB9XG5cbiAgICBASG9zdEJpbmRpbmcoJ2NsYXNzLnNjLWNvbnRyb2wtd3JhcHBlci0taW52YWxpZCcpXG4gICAgcHVibGljIGdldCBpbnZhbGlkKCkge1xuICAgICAgICByZXR1cm4gdGhpcy5jb250cm9sPy50b3VjaGVkICYmIHRoaXMuZXJyb3JzO1xuICAgIH1cblxuICAgIHB1YmxpYyBnZXQgZXJyb3JzKCk6IHN0cmluZ1tdIHwgdW5kZWZpbmVkIHtcbiAgICAgICAgaWYgKHRoaXMuY29udHJvbD8ucGVuZGluZykge1xuICAgICAgICAgICAgcmV0dXJuIHRoaXMucHJldmlvdXNFcnJvcjtcbiAgICAgICAgfSBlbHNlIHtcbiAgICAgICAgICAgIHRoaXMucHJldmlvdXNFcnJvciA9IHRoaXMuY29udHJvbD8uZXJyb3JzPy5bJ2Vycm9ycyddO1xuICAgICAgICB9XG4gICAgICAgIHJldHVybiB0aGlzLmNvbnRyb2w/LmVycm9ycz8uWydlcnJvcnMnXTtcbiAgICB9XG5cbiAgICBwdWJsaWMgbmdBZnRlclZpZXdJbml0KCk6IHZvaWQge1xuICAgICAgICAvLyBXYWl0IHVudGlsIHRoZSBmb3JtIGlzIGlkbGVcbiAgICAgICAgLy8gVGhlbiwgbGlzdGVuIHRvIGFsbCBldmVudHMgb2YgdGhlIG5nTW9kZWxHcm91cCBvciBuZ01vZGVsXG4gICAgICAgIC8vIGFuZCBtYXJrIHRoZSBjb21wb25lbnQgYW5kIGl0cyBhbmNlc3RvcnMgYXMgZGlydHlcbiAgICAgICAgLy8gVGhpcyBhbGxvd3MgdXMgdG8gdXNlIHRoZSBPblB1c2ggQ2hhbmdlRGV0ZWN0aW9uIFN0cmF0ZWd5XG4gICAgICAgIHRoaXMuZm9ybURpcmVjdGl2ZS5pZGxlJFxuICAgICAgICAgICAgLnBpcGUoXG4gICAgICAgICAgICAgICAgc3dpdGNoTWFwKCgpID0+IHRoaXMubmdNb2RlbEdyb3VwPy5jb250cm9sPy5ldmVudHMgfHwgb2YobnVsbCkpLFxuICAgICAgICAgICAgICAgIG1lcmdlV2l0aCh0aGlzLmNvbnRyb2w/LmV2ZW50cyB8fCBvZihudWxsKSksXG4gICAgICAgICAgICAgICAgdGFrZVVudGlsRGVzdHJveWVkKHRoaXMuZGVzdHJveVJlZilcbiAgICAgICAgICAgIClcbiAgICAgICAgICAgIC5zdWJzY3JpYmUoKCkgPT4ge1xuICAgICAgICAgICAgICAgIHRoaXMuY2RSZWYubWFya0ZvckNoZWNrKCk7XG4gICAgICAgICAgICB9KTtcbiAgICB9XG59XG4iLCI8ZGl2IGNsYXNzPVwic2MtY29udHJvbC13cmFwcGVyXCI+XG4gIDxkaXYgY2xhc3M9XCJzYy1jb250cm9sLXdyYXBwZXJfX2NvbnRlbnRcIj5cbiAgICA8bmctY29udGVudD48L25nLWNvbnRlbnQ+XG4gIDwvZGl2PlxuICA8ZGl2IGNsYXNzPVwic2MtY29udHJvbC13cmFwcGVyX19lcnJvcnNcIj5cbiAgICA8dWwgW2hpZGRlbl09XCIhaW52YWxpZFwiPlxuICAgICAgQGZvciAoZXJyb3Igb2YgZXJyb3JzOyB0cmFjayBlcnJvcikge1xuICAgICAgICA8bGk+e3sgZXJyb3IgfX08L2xpPlxuICAgICAgfVxuICAgIDwvdWw+XG4gIDwvZGl2PlxuPC9kaXY+XG4iXX0=
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export const ROOT_FORM = 'rootForm';
|
|
2
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29uc3RhbnRzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vcHJvamVjdHMvbmd4LXZlc3QtZm9ybXMvc3JjL2xpYi9jb25zdGFudHMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsTUFBTSxDQUFDLE1BQU0sU0FBUyxHQUFHLFVBQVUsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbImV4cG9ydCBjb25zdCBST09UX0ZPUk0gPSAncm9vdEZvcm0nOyJdfQ==
|