@ng-simplicity/forms-core 1.1.4 → 1.1.5
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 +15 -2
- package/fesm2022/ng-simplicity-forms-core.mjs +331 -10
- package/fesm2022/ng-simplicity-forms-core.mjs.map +1 -1
- package/index.d.ts +124 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ This core package is extended by styling integration packages that provide prebu
|
|
|
15
15
|
|
|
16
16
|
| Package | Package version | Angular support |
|
|
17
17
|
|---|---:|---|
|
|
18
|
-
| `@ng-simplicity/forms-core` | `1.1.
|
|
18
|
+
| `@ng-simplicity/forms-core` | `1.1.5` | `17.x`–`22.x` |
|
|
19
19
|
|
|
20
20
|
---
|
|
21
21
|
|
|
@@ -46,7 +46,8 @@ Each `registerAll…` helper also registers the core structural components, so i
|
|
|
46
46
|
### 1. The Public Boundary: `NgsFormsService`
|
|
47
47
|
- Injected by the user's component page.
|
|
48
48
|
- Exposes form values and statuses through Signals and observables (e.g. `formValue$`, `dirty`, `isValid`).
|
|
49
|
-
- Provides entry methods like `setFormConfig(config)` and `
|
|
49
|
+
- Provides entry methods like `setFormConfig(config)`, `patchValue(data)` and `setServerErrorMessages(errors)`.
|
|
50
|
+
- `patchValue(data)` can be called straight after `setFormConfig()`: it keeps the patch and re-applies it as controls render, including fields that the patched values reveal through `visible$`, until the form settles. Form arrays need enough rows (`initialItemCount`).
|
|
50
51
|
- Implements a retry loading mechanism in case child elements compile asynchronously.
|
|
51
52
|
|
|
52
53
|
### 2. The Internal Coordinator: `NgsFormsInternalService`
|
|
@@ -217,6 +218,18 @@ Each form control component that extends `NgsFormsFormItemWithVisibleAndValidato
|
|
|
217
218
|
}
|
|
218
219
|
```
|
|
219
220
|
3. **Displaying Errors**: Within your custom component template, bind your error display element to the `errorMessage` property inherited from the base class. The core engine dynamically updates `errorMessage` based on whether the control is invalid, touched, dirty, or if the form has been submitted.
|
|
221
|
+
4. **Server Errors**: Pass the server's errors to `NgsFormsService.setServerErrorMessages()` in the same shape as the form value. Each leaf is a key looked up in that field's `errorMessageMap` (a key with no entry is shown as text), and form array rows are matched by index. Each error clears when its field changes, and the call returns the paths that matched no control. `constructServerErrorMessageObject()` builds the object from a list of `{ jsonPath, validator }` pairs:
|
|
222
|
+
```typescript
|
|
223
|
+
errorMessageMap: { taken: 'User Name Not Available' }
|
|
224
|
+
|
|
225
|
+
this.formsService.setServerErrorMessages({ registration: { userName: 'taken', contacts: [{}, {}, { phone: 'invalidPhone' }] } });
|
|
226
|
+
|
|
227
|
+
// Same thing from a flat list:
|
|
228
|
+
this.formsService.setServerErrorMessages(constructServerErrorMessageObject([
|
|
229
|
+
{ jsonPath: 'registration.userName', validator: 'taken' },
|
|
230
|
+
{ jsonPath: 'registration.contacts[2].phone', validator: 'invalidPhone' },
|
|
231
|
+
]));
|
|
232
|
+
```
|
|
220
233
|
|
|
221
234
|
---
|
|
222
235
|
|
|
@@ -2,8 +2,8 @@ import { Subject, takeUntil, BehaviorSubject, switchMap, defer, map, distinctUnt
|
|
|
2
2
|
import * as i0 from '@angular/core';
|
|
3
3
|
import { Directive, InjectionToken, inject, Injectable, ChangeDetectorRef, Injector, ViewContainerRef, Input, computed, ChangeDetectionStrategy, Component, ElementRef, afterNextRender, Sanitizer, SecurityContext, NgModule } from '@angular/core';
|
|
4
4
|
import { v4 } from 'uuid';
|
|
5
|
+
import { FormArray, FormGroup, UntypedFormGroup, FormControl, Validators, UntypedFormArray, FormsModule, ReactiveFormsModule } from '@angular/forms';
|
|
5
6
|
import { toSignal } from '@angular/core/rxjs-interop';
|
|
6
|
-
import { UntypedFormGroup, FormControl, Validators, FormGroup, FormArray, UntypedFormArray, FormsModule, ReactiveFormsModule } from '@angular/forms';
|
|
7
7
|
import { CommonModule } from '@angular/common';
|
|
8
8
|
|
|
9
9
|
/** Base class that ties RxJS subscriptions to the component's lifetime. */
|
|
@@ -77,6 +77,202 @@ function ngsDefaults(target, ...sources) {
|
|
|
77
77
|
return result;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Builds the object for `NgsFormsService.setServerErrorMessages()` from a flat list of paths and error keys, for
|
|
82
|
+
* APIs that return their errors that way. Array indexes create arrays, and rows without errors are filled with `{}`.
|
|
83
|
+
* Two errors on the same path become an array of keys.
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```ts
|
|
87
|
+
* constructServerErrorMessageObject([
|
|
88
|
+
* { jsonPath: 'registration.userName', validator: 'taken' },
|
|
89
|
+
* { jsonPath: 'registration.contacts[2].phone', validator: 'invalidPhone' },
|
|
90
|
+
* ]);
|
|
91
|
+
* // { registration: { userName: 'taken', contacts: [{}, {}, { phone: 'invalidPhone' }] } }
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
function constructServerErrorMessageObject(errors) {
|
|
95
|
+
const result = {};
|
|
96
|
+
for (const { jsonPath, validator } of errors) {
|
|
97
|
+
const segments = parseJsonPath(jsonPath);
|
|
98
|
+
if (!segments.length || !validator)
|
|
99
|
+
continue;
|
|
100
|
+
let node = result;
|
|
101
|
+
segments.forEach((segment, i) => {
|
|
102
|
+
const isLast = i === segments.length - 1;
|
|
103
|
+
if (Array.isArray(node) && typeof segment === 'number') {
|
|
104
|
+
while (node.length < segment)
|
|
105
|
+
node.push({});
|
|
106
|
+
}
|
|
107
|
+
if (isLast) {
|
|
108
|
+
const existing = node[segment];
|
|
109
|
+
if (typeof existing === 'string') {
|
|
110
|
+
node[segment] = existing === validator ? existing : [existing, validator];
|
|
111
|
+
}
|
|
112
|
+
else if (Array.isArray(existing) && existing.every((e) => typeof e === 'string')) {
|
|
113
|
+
if (!existing.includes(validator))
|
|
114
|
+
existing.push(validator);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
node[segment] = validator;
|
|
118
|
+
}
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const nextIsIndex = typeof segments[i + 1] === 'number';
|
|
122
|
+
const existing = node[segment];
|
|
123
|
+
if (existing === undefined || existing === null || typeof existing !== 'object') {
|
|
124
|
+
node[segment] = nextIsIndex ? [] : {};
|
|
125
|
+
}
|
|
126
|
+
else if (nextIsIndex && !Array.isArray(existing) && Object.keys(existing).length === 0) {
|
|
127
|
+
// A `{}` placeholder for an array row that is itself an array.
|
|
128
|
+
node[segment] = [];
|
|
129
|
+
}
|
|
130
|
+
node = node[segment];
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
/** Splits `a.b[2]['c d']` into `['a', 'b', 2, 'c d']`. */
|
|
136
|
+
function parseJsonPath(jsonPath) {
|
|
137
|
+
const path = (jsonPath ?? '').trim().replace(/^\$\.?/, '');
|
|
138
|
+
const segments = [];
|
|
139
|
+
const pattern = /\[\s*(\d+)\s*\]|\[\s*(['"])(.*?)\2\s*\]|([^.[\]]+)/g;
|
|
140
|
+
let match;
|
|
141
|
+
while ((match = pattern.exec(path)) !== null) {
|
|
142
|
+
if (match[1] !== undefined)
|
|
143
|
+
segments.push(Number(match[1]));
|
|
144
|
+
else if (match[3] !== undefined)
|
|
145
|
+
segments.push(match[3]);
|
|
146
|
+
else if (match[4] !== undefined)
|
|
147
|
+
segments.push(match[4]);
|
|
148
|
+
}
|
|
149
|
+
return segments;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Marker on the error value of a server error (`control.errors[key][NGS_FORMS_SERVER_ERROR] === true`). When the
|
|
153
|
+
* key has no entry in `errorMessageMap`, the key itself is shown as the message, so servers can send message text.
|
|
154
|
+
*/
|
|
155
|
+
const NGS_FORMS_SERVER_ERROR = 'ngsServerError';
|
|
156
|
+
/**
|
|
157
|
+
* Applies server errors to a form and clears each one when its control's value changes.
|
|
158
|
+
*
|
|
159
|
+
* @internal Used by `NgsFormsService`.
|
|
160
|
+
*/
|
|
161
|
+
class NgsFormsServerErrorRegistry {
|
|
162
|
+
entries = new Map();
|
|
163
|
+
/**
|
|
164
|
+
* Validator that reports a control's server errors. Form items include it whenever they set their validators,
|
|
165
|
+
* so the errors survive re-validation until the control's value changes.
|
|
166
|
+
*/
|
|
167
|
+
validator = (control) => this.validate(control);
|
|
168
|
+
/** `true` while at least one server error is set. */
|
|
169
|
+
get hasErrors() {
|
|
170
|
+
return this.entries.size > 0;
|
|
171
|
+
}
|
|
172
|
+
/** Replaces every server error with `errors`. Returns the paths that matched no control. */
|
|
173
|
+
set(root, errors) {
|
|
174
|
+
this.clear();
|
|
175
|
+
const unmatched = [];
|
|
176
|
+
this.apply(root, errors, '', unmatched);
|
|
177
|
+
return unmatched;
|
|
178
|
+
}
|
|
179
|
+
/** Removes every server error. */
|
|
180
|
+
clear() {
|
|
181
|
+
if (!this.entries.size)
|
|
182
|
+
return;
|
|
183
|
+
const controls = Array.from(this.entries.keys());
|
|
184
|
+
this.entries.clear();
|
|
185
|
+
controls.forEach((control) => control.updateValueAndValidity());
|
|
186
|
+
}
|
|
187
|
+
/** Forgets every server error without re-validating, for when the controls are being thrown away. */
|
|
188
|
+
reset() {
|
|
189
|
+
this.entries.clear();
|
|
190
|
+
}
|
|
191
|
+
apply(control, errors, path, unmatched) {
|
|
192
|
+
if (errors === null || errors === undefined)
|
|
193
|
+
return;
|
|
194
|
+
if (typeof errors === 'string' || (Array.isArray(errors) && errors.length > 0 && errors.every((e) => typeof e === 'string'))) {
|
|
195
|
+
this.applyKeys(control, typeof errors === 'string' ? [errors] : errors);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (Array.isArray(errors)) {
|
|
199
|
+
if (!(control instanceof FormArray)) {
|
|
200
|
+
if (errors.some((e) => !isEmpty(e)))
|
|
201
|
+
unmatched.push(path);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
errors.forEach((rowErrors, index) => {
|
|
205
|
+
if (isEmpty(rowErrors))
|
|
206
|
+
return;
|
|
207
|
+
const row = control.at(index);
|
|
208
|
+
const rowPath = `${path}[${index}]`;
|
|
209
|
+
if (!row) {
|
|
210
|
+
unmatched.push(rowPath);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
this.apply(row, rowErrors, rowPath, unmatched);
|
|
214
|
+
});
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (!(control instanceof FormGroup)) {
|
|
218
|
+
if (!isEmpty(errors))
|
|
219
|
+
unmatched.push(path);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
for (const [name, childErrors] of Object.entries(errors)) {
|
|
223
|
+
if (isEmpty(childErrors))
|
|
224
|
+
continue;
|
|
225
|
+
const childPath = path ? `${path}.${name}` : name;
|
|
226
|
+
const child = control.get([name]);
|
|
227
|
+
if (!child) {
|
|
228
|
+
unmatched.push(childPath);
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
this.apply(child, childErrors, childPath, unmatched);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
applyKeys(control, keys) {
|
|
235
|
+
this.entries.set(control, { keys, value: control.value });
|
|
236
|
+
if (!control.hasValidator(this.validator)) {
|
|
237
|
+
control.addValidators(this.validator);
|
|
238
|
+
}
|
|
239
|
+
// Show the message straight away, without waiting for the user to touch the field.
|
|
240
|
+
control.markAsTouched();
|
|
241
|
+
control.updateValueAndValidity();
|
|
242
|
+
}
|
|
243
|
+
validate(control) {
|
|
244
|
+
const entry = this.entries.get(control);
|
|
245
|
+
if (!entry)
|
|
246
|
+
return null;
|
|
247
|
+
if (!sameValue(control.value, entry.value)) {
|
|
248
|
+
this.entries.delete(control);
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
return Object.fromEntries(entry.keys.map((key) => [key, { [NGS_FORMS_SERVER_ERROR]: true }]));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
function isEmpty(errors) {
|
|
255
|
+
if (errors === null || errors === undefined || errors === '')
|
|
256
|
+
return true;
|
|
257
|
+
if (Array.isArray(errors))
|
|
258
|
+
return errors.every(isEmpty);
|
|
259
|
+
if (typeof errors === 'object')
|
|
260
|
+
return Object.values(errors).every(isEmpty);
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
function sameValue(a, b) {
|
|
264
|
+
if (Object.is(a, b))
|
|
265
|
+
return true;
|
|
266
|
+
if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null)
|
|
267
|
+
return false;
|
|
268
|
+
try {
|
|
269
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
80
276
|
/**
|
|
81
277
|
* Root base class for every form item component. Reads the item's configuration from `NGS_FORMS_ITEM_DATA`.
|
|
82
278
|
*
|
|
@@ -175,6 +371,14 @@ class NgsFormsService {
|
|
|
175
371
|
lastConfig;
|
|
176
372
|
missingFormTimer;
|
|
177
373
|
destroyed = false;
|
|
374
|
+
/** Patches still being applied while the form renders, in call order. */
|
|
375
|
+
pendingPatches = [];
|
|
376
|
+
patchTimer;
|
|
377
|
+
/** The controls that existed when the pending patches were last applied. */
|
|
378
|
+
patchedStructure;
|
|
379
|
+
patchRounds = 0;
|
|
380
|
+
/** Render passes in a row in which no control was added or removed. */
|
|
381
|
+
stablePatchRounds = 0;
|
|
178
382
|
/** `true` once the user has changed any control in the form. */
|
|
179
383
|
get dirty() {
|
|
180
384
|
return this.formGroup?.dirty ?? false;
|
|
@@ -198,6 +402,8 @@ class NgsFormsService {
|
|
|
198
402
|
*/
|
|
199
403
|
setFormConfig(formConfig) {
|
|
200
404
|
this.lastConfig = formConfig;
|
|
405
|
+
// Patches made for the previous form do not carry over. Patches made after this call are kept until it renders.
|
|
406
|
+
this.cancelPendingPatches();
|
|
201
407
|
if (this.internalServiceIsSet) {
|
|
202
408
|
this.internalFormService.setFormData(formConfig);
|
|
203
409
|
return;
|
|
@@ -221,6 +427,9 @@ class NgsFormsService {
|
|
|
221
427
|
if (this.lastConfig) {
|
|
222
428
|
internalFormsService.setFormData(this.lastConfig);
|
|
223
429
|
}
|
|
430
|
+
if (this.pendingPatches.length) {
|
|
431
|
+
this.schedulePatchRound();
|
|
432
|
+
}
|
|
224
433
|
}
|
|
225
434
|
/**
|
|
226
435
|
* @internal
|
|
@@ -270,17 +479,103 @@ class NgsFormsService {
|
|
|
270
479
|
this.destroyed = true;
|
|
271
480
|
clearTimeout(this.missingFormTimer);
|
|
272
481
|
this.missingFormTimer = undefined;
|
|
482
|
+
this.cancelPendingPatches();
|
|
273
483
|
this.formGroupBinding?.unsubscribe();
|
|
274
484
|
this.formValue$.complete();
|
|
275
485
|
}
|
|
486
|
+
/**
|
|
487
|
+
* Shows errors from the server on the matching controls, replacing any server errors shown before.
|
|
488
|
+
*
|
|
489
|
+
* @param errors - An object shaped like the form value (including the root group name) whose leaves are error
|
|
490
|
+
* keys, for example `{ registration: { userName: 'taken', contacts: [{}, {}, { phone: 'invalid' }] } }`. Each key
|
|
491
|
+
* is looked up in that control's `errorMessageMap`; when it has no entry the key itself is shown, so the server
|
|
492
|
+
* can also send the message text. Use `constructServerErrorMessageObject()` to build it from a list of paths.
|
|
493
|
+
* @returns The paths that matched no rendered control (a hidden item or a missing array row, for example), so you
|
|
494
|
+
* can show those errors elsewhere.
|
|
495
|
+
*
|
|
496
|
+
* @remarks The controls are marked as touched so the messages show at once, and they stay invalid until their
|
|
497
|
+
* value changes. All server errors are cleared by the next call, by `clearServerErrorMessages()`, by
|
|
498
|
+
* `setFormConfig()`, and when rows are added to or removed from a form array, because rows are matched by index.
|
|
499
|
+
*/
|
|
500
|
+
setServerErrorMessages(errors) {
|
|
501
|
+
if (!this.internalFormService) {
|
|
502
|
+
console.error('setServerErrorMessages() was called before <ngs-form> connected.');
|
|
503
|
+
return [];
|
|
504
|
+
}
|
|
505
|
+
return this.internalFormService.serverErrors.set(this.formGroup, errors);
|
|
506
|
+
}
|
|
507
|
+
/** Removes every error set with `setServerErrorMessages()`. */
|
|
508
|
+
clearServerErrorMessages() {
|
|
509
|
+
this.internalFormService?.serverErrors.clear();
|
|
510
|
+
}
|
|
276
511
|
/**
|
|
277
512
|
* Patches values into the form, using the same shape as `formValue$` (including the root group name).
|
|
278
513
|
*
|
|
279
|
-
* @remarks
|
|
280
|
-
*
|
|
514
|
+
* @remarks It is safe to call straight after `setFormConfig()`, or before `<ngs-form>` has connected. The values
|
|
515
|
+
* are applied at once to the controls that exist, kept, and applied again each time new controls appear, until
|
|
516
|
+
* the form stops changing. That covers items that render later and items behind `visible$` that the patched values
|
|
517
|
+
* themselves reveal (and anything those reveal in turn). Later calls are applied after earlier ones. A form array
|
|
518
|
+
* only receives values for rows it has, so give it enough rows with `initialItemCount`. `setFormConfig()` drops
|
|
519
|
+
* patches that are still being applied to the previous form.
|
|
281
520
|
*/
|
|
282
521
|
patchValue(entityData) {
|
|
283
|
-
this.
|
|
522
|
+
this.pendingPatches.push(entityData);
|
|
523
|
+
this.applyPendingPatches();
|
|
524
|
+
this.patchRounds = 0;
|
|
525
|
+
this.stablePatchRounds = 0;
|
|
526
|
+
this.schedulePatchRound();
|
|
527
|
+
}
|
|
528
|
+
/** Maximum number of render passes a patch waits for new controls before it is dropped. */
|
|
529
|
+
static MAX_PATCH_ROUNDS = 20;
|
|
530
|
+
applyPendingPatches() {
|
|
531
|
+
for (const patch of this.pendingPatches) {
|
|
532
|
+
this.formGroup.patchValue(patch);
|
|
533
|
+
}
|
|
534
|
+
this.patchedStructure = this.internalServiceIsSet ? controlStructure(this.formGroup) : undefined;
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Waits one render pass, then re-applies the pending patches if controls were added or removed. Stops once two
|
|
538
|
+
* passes in a row change nothing (rendering can take a pass to start), or after `MAX_PATCH_ROUNDS` passes. Waits
|
|
539
|
+
* without counting while no form is connected.
|
|
540
|
+
*/
|
|
541
|
+
schedulePatchRound() {
|
|
542
|
+
clearTimeout(this.patchTimer);
|
|
543
|
+
if (this.destroyed || !this.pendingPatches.length)
|
|
544
|
+
return;
|
|
545
|
+
if (!this.internalServiceIsSet) {
|
|
546
|
+
// `setInternalService()` starts the rounds once <ngs-form> connects.
|
|
547
|
+
this.patchTimer = undefined;
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
this.patchTimer = setTimeout(() => {
|
|
551
|
+
this.patchTimer = undefined;
|
|
552
|
+
if (this.destroyed || !this.internalServiceIsSet)
|
|
553
|
+
return;
|
|
554
|
+
const structure = controlStructure(this.formGroup);
|
|
555
|
+
if (structure === this.patchedStructure) {
|
|
556
|
+
if (++this.stablePatchRounds >= 2) {
|
|
557
|
+
this.cancelPendingPatches();
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
this.schedulePatchRound();
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
this.stablePatchRounds = 0;
|
|
564
|
+
this.applyPendingPatches();
|
|
565
|
+
if (++this.patchRounds >= NgsFormsService.MAX_PATCH_ROUNDS) {
|
|
566
|
+
this.cancelPendingPatches();
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
this.schedulePatchRound();
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
cancelPendingPatches() {
|
|
573
|
+
clearTimeout(this.patchTimer);
|
|
574
|
+
this.patchTimer = undefined;
|
|
575
|
+
this.pendingPatches = [];
|
|
576
|
+
this.patchedStructure = undefined;
|
|
577
|
+
this.patchRounds = 0;
|
|
578
|
+
this.stablePatchRounds = 0;
|
|
284
579
|
}
|
|
285
580
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: NgsFormsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
286
581
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: NgsFormsService });
|
|
@@ -288,6 +583,18 @@ class NgsFormsService {
|
|
|
288
583
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: NgsFormsService, decorators: [{
|
|
289
584
|
type: Injectable
|
|
290
585
|
}] });
|
|
586
|
+
/** A string listing the paths of every control under `control`, so a change in the form's controls can be seen. */
|
|
587
|
+
function controlStructure(control, path = '') {
|
|
588
|
+
if (control instanceof FormGroup) {
|
|
589
|
+
return Object.keys(control.controls)
|
|
590
|
+
.map((name) => controlStructure(control.controls[name], `${path}.${name}`))
|
|
591
|
+
.join('|') + `|${path}{}`;
|
|
592
|
+
}
|
|
593
|
+
if (control instanceof FormArray) {
|
|
594
|
+
return control.controls.map((row, i) => controlStructure(row, `${path}[${i}]`)).join('|') + `|${path}[]`;
|
|
595
|
+
}
|
|
596
|
+
return path;
|
|
597
|
+
}
|
|
291
598
|
|
|
292
599
|
/**
|
|
293
600
|
* Per form state holder that connects `<ngs-form>`, its items and `NgsFormsService`.
|
|
@@ -300,6 +607,8 @@ class NgsFormsInternalService {
|
|
|
300
607
|
formConfig$ = new BehaviorSubject(undefined);
|
|
301
608
|
isSubmitted$ = new BehaviorSubject(false);
|
|
302
609
|
state = new BehaviorSubject({});
|
|
610
|
+
/** Server errors set with `NgsFormsService.setServerErrorMessages()`. */
|
|
611
|
+
serverErrors = new NgsFormsServerErrorRegistry();
|
|
303
612
|
setIsSubmitted(isSubmitted) {
|
|
304
613
|
this.isSubmitted$.next(isSubmitted);
|
|
305
614
|
}
|
|
@@ -314,6 +623,7 @@ class NgsFormsInternalService {
|
|
|
314
623
|
const state = { ...(formConfig.initialState ?? {}) };
|
|
315
624
|
state['global'] = { ...(formConfig.globalState ?? {}) };
|
|
316
625
|
this.state.next(state);
|
|
626
|
+
this.serverErrors.reset();
|
|
317
627
|
this.formGroup$.next(new UntypedFormGroup({}));
|
|
318
628
|
this.formConfig$.next(formConfig);
|
|
319
629
|
this.isSubmitted$.next(false);
|
|
@@ -421,25 +731,32 @@ class NgsFormsFormItemWithVisibleAndValidatorsBase extends NgsFormsBaseClassForm
|
|
|
421
731
|
}
|
|
422
732
|
const firstErrorKey = Object.keys(errors)[0];
|
|
423
733
|
const map = this.config.errorMessageMap;
|
|
424
|
-
|
|
734
|
+
const isServerError = errors[firstErrorKey]?.[NGS_FORMS_SERVER_ERROR] === true;
|
|
735
|
+
// A server error without a mapped message shows the server's text.
|
|
736
|
+
this.errorMessage = map?.[firstErrorKey] || (isServerError ? firstErrorKey : `Field is invalid: ${firstErrorKey}`);
|
|
425
737
|
}
|
|
426
738
|
ngOnDestroy() {
|
|
427
739
|
super.ngOnDestroy();
|
|
428
740
|
this.formAddRemoveFns.remove(this.control, this.config.name);
|
|
429
741
|
}
|
|
430
742
|
bindValidators() {
|
|
431
|
-
if (!this.config.validators && !this.config.validators$)
|
|
432
|
-
return;
|
|
433
743
|
const control = this.control;
|
|
744
|
+
// The server error validator is always kept, so server errors survive until the control's value changes.
|
|
745
|
+
// Optional so that tests which stub `NgsFormsInternalService` keep working.
|
|
746
|
+
const serverValidators = this.privateService.serverErrors ? [this.privateService.serverErrors.validator] : [];
|
|
747
|
+
if (!this.config.validators && !this.config.validators$) {
|
|
748
|
+
control.addValidators(serverValidators);
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
434
751
|
if (this.config.validators) {
|
|
435
|
-
control.setValidators(this.config.validators);
|
|
752
|
+
control.setValidators([...this.config.validators, ...serverValidators]);
|
|
436
753
|
// A group or array computed its status when it was created, before these validators were set.
|
|
437
754
|
control.updateValueAndValidity({ emitEvent: false });
|
|
438
755
|
return;
|
|
439
756
|
}
|
|
440
757
|
if (this.config.validators$) {
|
|
441
758
|
this.subscribe(this.config.validators$, (validators) => {
|
|
442
|
-
control.setValidators(validators);
|
|
759
|
+
control.setValidators([...validators, ...serverValidators]);
|
|
443
760
|
control.updateValueAndValidity();
|
|
444
761
|
});
|
|
445
762
|
}
|
|
@@ -861,6 +1178,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImpo
|
|
|
861
1178
|
class NgsFormsFormArrayInternalService {
|
|
862
1179
|
config = inject(NGS_FORMS_ITEM_DATA).config;
|
|
863
1180
|
formArray = new UntypedFormArray([]);
|
|
1181
|
+
formsInternalService = inject(NgsFormsInternalService, { optional: true });
|
|
864
1182
|
/** Emits after a row is added or removed. */
|
|
865
1183
|
itemsChanged = new Subject();
|
|
866
1184
|
/** Emits the index of a row right after it is removed. */
|
|
@@ -884,6 +1202,8 @@ class NgsFormsFormArrayInternalService {
|
|
|
884
1202
|
if (index < 0 || index >= this.formArray.controls.length) {
|
|
885
1203
|
return;
|
|
886
1204
|
}
|
|
1205
|
+
// Server errors for rows are matched by index, so they are cleared once the rows move.
|
|
1206
|
+
this.formsInternalService?.serverErrors?.clear();
|
|
887
1207
|
this.formArray.removeAt(index);
|
|
888
1208
|
this.rowRemoved.next(index);
|
|
889
1209
|
this.itemsChanged.next();
|
|
@@ -893,6 +1213,7 @@ class NgsFormsFormArrayInternalService {
|
|
|
893
1213
|
if (this.config?.maxItems && this.formArray.controls.length >= this.config.maxItems) {
|
|
894
1214
|
return;
|
|
895
1215
|
}
|
|
1216
|
+
this.formsInternalService?.serverErrors?.clear();
|
|
896
1217
|
this.formArray.push(new UntypedFormGroup({}));
|
|
897
1218
|
this.itemsChanged.next();
|
|
898
1219
|
}
|
|
@@ -1230,5 +1551,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImpo
|
|
|
1230
1551
|
* Generated bundle index. Do not edit.
|
|
1231
1552
|
*/
|
|
1232
1553
|
|
|
1233
|
-
export { NGS_FORMS_ARRAY_ROW, NGS_FORMS_CONTROL_ADD_REMOVE_FN, NGS_FORMS_ITEM_DATA, NGS_FORMS_ITEM_INDEX, NgsFormComponent, NgsFormsBaseClassFormComponent, NgsFormsBaseClassFormInputComponent, NgsFormsBaseClassFormItemInputWithOptionsComponent, NgsFormsBaseClassItemsContainerBase, NgsFormsColumnComponent, NgsFormsComponentRegistryService, NgsFormsCoreModule, NgsFormsFormArrayAddItemComponent, NgsFormsFormArrayContainerComponent, NgsFormsFormArrayInternalService, NgsFormsFormArrayListComponent, NgsFormsFormArrayRemoveItemComponent, NgsFormsFormControlAddRemoveFunctions, NgsFormsFormGroupComponent, NgsFormsFormItemDirective, NgsFormsFormItemWithVisibleAndValidatorsBase, NgsFormsFormSectionComponent, NgsFormsHtmlContentComponent, NgsFormsInternalService, NgsFormsRowComponent, NgsFormsService, NgsFormsTextDivComponent, NgsSubscriber, ngsDefaults, sectionConfigDefaults };
|
|
1554
|
+
export { NGS_FORMS_ARRAY_ROW, NGS_FORMS_CONTROL_ADD_REMOVE_FN, NGS_FORMS_ITEM_DATA, NGS_FORMS_ITEM_INDEX, NGS_FORMS_SERVER_ERROR, NgsFormComponent, NgsFormsBaseClassFormComponent, NgsFormsBaseClassFormInputComponent, NgsFormsBaseClassFormItemInputWithOptionsComponent, NgsFormsBaseClassItemsContainerBase, NgsFormsColumnComponent, NgsFormsComponentRegistryService, NgsFormsCoreModule, NgsFormsFormArrayAddItemComponent, NgsFormsFormArrayContainerComponent, NgsFormsFormArrayInternalService, NgsFormsFormArrayListComponent, NgsFormsFormArrayRemoveItemComponent, NgsFormsFormControlAddRemoveFunctions, NgsFormsFormGroupComponent, NgsFormsFormItemDirective, NgsFormsFormItemWithVisibleAndValidatorsBase, NgsFormsFormSectionComponent, NgsFormsHtmlContentComponent, NgsFormsInternalService, NgsFormsRowComponent, NgsFormsServerErrorRegistry, NgsFormsService, NgsFormsTextDivComponent, NgsSubscriber, constructServerErrorMessageObject, ngsDefaults, sectionConfigDefaults };
|
|
1234
1555
|
//# sourceMappingURL=ng-simplicity-forms-core.mjs.map
|