@dynamic-field-kit/angular 1.5.0 → 1.6.0
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 +190 -0
- package/README.md +67 -9
- package/dist/fesm2022/dynamic-field-kit-angular.mjs +489 -113
- package/dist/types/dynamic-field-kit-angular.d.ts +339 -0
- package/package.json +44 -30
- package/dist/components/BaseInput.d.ts +0 -34
- package/dist/components/DynamicFormDevTools.d.ts +0 -16
- package/dist/components/DynamicInput.d.ts +0 -36
- package/dist/components/FieldInput.d.ts +0 -28
- package/dist/components/MultiFieldInput.d.ts +0 -60
- package/dist/fieldRegistryToken.d.ts +0 -3
- package/dist/index.d.ts +0 -5
- package/dist/layout/defaultLayouts.d.ts +0 -31
- package/dist/layout/index.d.ts +0 -2
- package/dist/layout/layoutRegistry.d.ts +0 -12
- package/dist/lib/dynamic-field-kit.module.d.ts +0 -11
- package/dist/lib/dynamic-form.store.d.ts +0 -23
- package/dist/public-api.d.ts +0 -14
- package/dist/types/layout.d.ts +0 -4
|
@@ -2,8 +2,8 @@ import * as i0 from '@angular/core';
|
|
|
2
2
|
import { EventEmitter, Output, Input, Component, InjectionToken, inject, ViewContainerRef, ViewChild, ChangeDetectionStrategy, HostListener, signal, computed, NgModule } from '@angular/core';
|
|
3
3
|
import * as i1 from '@angular/common';
|
|
4
4
|
import { CommonModule, NgIf, NgClass, NgFor, JsonPipe } from '@angular/common';
|
|
5
|
-
import { fieldRegistry,
|
|
6
|
-
export { FieldRegistry, fieldRegistry, resolveDisabled, resolveOptions, resolveReadOnly, validateField, validateFieldAsync, validateFields, validateFieldsAsync, validators } from '@dynamic-field-kit/core';
|
|
5
|
+
import { fieldRegistry, buildFieldRendererProps, makeFieldId, indexGroupPathMap, applyComputedValues, validateFields, canAddGroupItem, canRemoveGroupItem, createGroupItem, validateFieldsAsync, collectFieldPaths } from '@dynamic-field-kit/core';
|
|
6
|
+
export { FieldRegistry, collectFieldPaths, fieldRegistry, indexGroupPathMap, resolveDisabled, resolveOptions, resolveReadOnly, validateField, validateFieldAsync, validateFields, validateFieldsAsync, validators } from '@dynamic-field-kit/core';
|
|
7
7
|
|
|
8
8
|
class BaseInputComponent {
|
|
9
9
|
cdr;
|
|
@@ -13,10 +13,27 @@ class BaseInputComponent {
|
|
|
13
13
|
required;
|
|
14
14
|
disabled;
|
|
15
15
|
readOnly;
|
|
16
|
+
/**
|
|
17
|
+
* Whether the field has been blurred (or marked touched by the form store).
|
|
18
|
+
* Renderers gate error display on this to avoid shouting at a user who has
|
|
19
|
+
* not reached the field yet.
|
|
20
|
+
*/
|
|
21
|
+
touched;
|
|
22
|
+
/** Whether the value differs from the one the form opened with. */
|
|
23
|
+
dirty;
|
|
16
24
|
error;
|
|
17
25
|
options;
|
|
18
26
|
className;
|
|
19
27
|
description;
|
|
28
|
+
id;
|
|
29
|
+
ariaInvalid;
|
|
30
|
+
ariaDescribedBy;
|
|
31
|
+
ariaRequired;
|
|
32
|
+
min;
|
|
33
|
+
max;
|
|
34
|
+
step;
|
|
35
|
+
accept;
|
|
36
|
+
multiple;
|
|
20
37
|
valueChange = new EventEmitter();
|
|
21
38
|
constructor(cdr) {
|
|
22
39
|
this.cdr = cdr;
|
|
@@ -30,10 +47,10 @@ class BaseInputComponent {
|
|
|
30
47
|
hasChanges(changes, prop) {
|
|
31
48
|
return changes[prop] !== undefined;
|
|
32
49
|
}
|
|
33
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "
|
|
34
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "
|
|
50
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: BaseInputComponent, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
|
|
51
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.22", type: BaseInputComponent, isStandalone: true, selector: "ng-component", inputs: { value: "value", label: "label", placeholder: "placeholder", required: "required", disabled: "disabled", readOnly: "readOnly", touched: "touched", dirty: "dirty", error: "error", options: "options", className: "className", description: "description", id: "id", ariaInvalid: "ariaInvalid", ariaDescribedBy: "ariaDescribedBy", ariaRequired: "ariaRequired", min: "min", max: "max", step: "step", accept: "accept", multiple: "multiple" }, outputs: { valueChange: "valueChange" }, usesOnChanges: true, ngImport: i0, template: '', isInline: true });
|
|
35
52
|
}
|
|
36
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "
|
|
53
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: BaseInputComponent, decorators: [{
|
|
37
54
|
type: Component,
|
|
38
55
|
args: [{
|
|
39
56
|
template: '',
|
|
@@ -50,6 +67,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImpo
|
|
|
50
67
|
type: Input
|
|
51
68
|
}], readOnly: [{
|
|
52
69
|
type: Input
|
|
70
|
+
}], touched: [{
|
|
71
|
+
type: Input
|
|
72
|
+
}], dirty: [{
|
|
73
|
+
type: Input
|
|
53
74
|
}], error: [{
|
|
54
75
|
type: Input
|
|
55
76
|
}], options: [{
|
|
@@ -58,6 +79,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImpo
|
|
|
58
79
|
type: Input
|
|
59
80
|
}], description: [{
|
|
60
81
|
type: Input
|
|
82
|
+
}], id: [{
|
|
83
|
+
type: Input
|
|
84
|
+
}], ariaInvalid: [{
|
|
85
|
+
type: Input
|
|
86
|
+
}], ariaDescribedBy: [{
|
|
87
|
+
type: Input
|
|
88
|
+
}], ariaRequired: [{
|
|
89
|
+
type: Input
|
|
90
|
+
}], min: [{
|
|
91
|
+
type: Input
|
|
92
|
+
}], max: [{
|
|
93
|
+
type: Input
|
|
94
|
+
}], step: [{
|
|
95
|
+
type: Input
|
|
96
|
+
}], accept: [{
|
|
97
|
+
type: Input
|
|
98
|
+
}], multiple: [{
|
|
99
|
+
type: Input
|
|
61
100
|
}], valueChange: [{
|
|
62
101
|
type: Output
|
|
63
102
|
}] } });
|
|
@@ -74,6 +113,10 @@ const FIELD_REGISTRY = new InjectionToken('dfk.FieldRegistry', {
|
|
|
74
113
|
|
|
75
114
|
// Only the framework-agnostic FieldRendererProps keys. Domain-specific props
|
|
76
115
|
// reach the renderer through `extraProps` (FieldDescription.props) instead.
|
|
116
|
+
// Must stay in step with core's FIELD_RENDERER_PROP_KEYS - a key missing here
|
|
117
|
+
// is a key no Angular renderer can ever receive, which is how `touched`,
|
|
118
|
+
// `dirty`, `id` and the aria flags used to be unreachable on this adapter
|
|
119
|
+
// alone. `packages/core/test/adapterParity.test.ts` asserts the two lists match.
|
|
77
120
|
const KNOWN_PROPS = [
|
|
78
121
|
'value',
|
|
79
122
|
'label',
|
|
@@ -81,10 +124,21 @@ const KNOWN_PROPS = [
|
|
|
81
124
|
'required',
|
|
82
125
|
'disabled',
|
|
83
126
|
'readOnly',
|
|
127
|
+
'touched',
|
|
128
|
+
'dirty',
|
|
84
129
|
'error',
|
|
85
130
|
'options',
|
|
86
131
|
'className',
|
|
87
132
|
'description',
|
|
133
|
+
'id',
|
|
134
|
+
'ariaInvalid',
|
|
135
|
+
'ariaDescribedBy',
|
|
136
|
+
'ariaRequired',
|
|
137
|
+
'min',
|
|
138
|
+
'max',
|
|
139
|
+
'step',
|
|
140
|
+
'accept',
|
|
141
|
+
'multiple',
|
|
88
142
|
];
|
|
89
143
|
class DynamicInput extends BaseInputComponent {
|
|
90
144
|
type;
|
|
@@ -224,10 +278,21 @@ class DynamicInput extends BaseInputComponent {
|
|
|
224
278
|
required: this.required ?? false,
|
|
225
279
|
disabled: this.disabled ?? false,
|
|
226
280
|
readOnly: this.readOnly ?? false,
|
|
281
|
+
touched: this.touched ?? false,
|
|
282
|
+
dirty: this.dirty ?? false,
|
|
227
283
|
error: this.error,
|
|
228
284
|
options: this.options ?? [],
|
|
229
285
|
className: this.className ?? '',
|
|
230
286
|
description: this.description ?? '',
|
|
287
|
+
id: this.id ?? '',
|
|
288
|
+
ariaInvalid: this.ariaInvalid ?? false,
|
|
289
|
+
ariaDescribedBy: this.ariaDescribedBy,
|
|
290
|
+
ariaRequired: this.ariaRequired ?? false,
|
|
291
|
+
min: this.min,
|
|
292
|
+
max: this.max,
|
|
293
|
+
step: this.step,
|
|
294
|
+
accept: this.accept,
|
|
295
|
+
multiple: this.multiple ?? false,
|
|
231
296
|
};
|
|
232
297
|
}
|
|
233
298
|
// Gates on `this.supplied` (populated from SimpleChanges in ngOnChanges),
|
|
@@ -291,6 +356,9 @@ class DynamicInput extends BaseInputComponent {
|
|
|
291
356
|
type === 'number') {
|
|
292
357
|
const input = document.createElement('input');
|
|
293
358
|
input.type = type;
|
|
359
|
+
if (this.id) {
|
|
360
|
+
input.id = this.id;
|
|
361
|
+
}
|
|
294
362
|
if (this.className) {
|
|
295
363
|
input.className = this.className;
|
|
296
364
|
}
|
|
@@ -317,6 +385,9 @@ class DynamicInput extends BaseInputComponent {
|
|
|
317
385
|
}
|
|
318
386
|
if (type === 'textarea') {
|
|
319
387
|
const textarea = document.createElement('textarea');
|
|
388
|
+
if (this.id) {
|
|
389
|
+
textarea.id = this.id;
|
|
390
|
+
}
|
|
320
391
|
if (this.className) {
|
|
321
392
|
textarea.className = this.className;
|
|
322
393
|
}
|
|
@@ -342,6 +413,9 @@ class DynamicInput extends BaseInputComponent {
|
|
|
342
413
|
if (type === 'checkbox') {
|
|
343
414
|
const checkbox = document.createElement('input');
|
|
344
415
|
checkbox.type = 'checkbox';
|
|
416
|
+
if (this.id) {
|
|
417
|
+
checkbox.id = this.id;
|
|
418
|
+
}
|
|
345
419
|
if (this.className) {
|
|
346
420
|
checkbox.className = this.className;
|
|
347
421
|
}
|
|
@@ -360,6 +434,9 @@ class DynamicInput extends BaseInputComponent {
|
|
|
360
434
|
}
|
|
361
435
|
if (type === 'select') {
|
|
362
436
|
const select = document.createElement('select');
|
|
437
|
+
if (this.id) {
|
|
438
|
+
select.id = this.id;
|
|
439
|
+
}
|
|
363
440
|
if (this.className) {
|
|
364
441
|
select.className = this.className;
|
|
365
442
|
}
|
|
@@ -401,10 +478,10 @@ class DynamicInput extends BaseInputComponent {
|
|
|
401
478
|
el.style.color = 'red';
|
|
402
479
|
this.host.element.nativeElement.appendChild(el);
|
|
403
480
|
}
|
|
404
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "
|
|
405
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "
|
|
481
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: DynamicInput, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
482
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.22", type: DynamicInput, isStandalone: true, selector: "dfk-dynamic-input", inputs: { type: "type", extraProps: "extraProps" }, outputs: { valueChange: "valueChange", onChange: "onChange" }, viewQueries: [{ propertyName: "host", first: true, predicate: ["host"], descendants: true, read: ViewContainerRef }], usesInheritance: true, usesOnChanges: true, ngImport: i0, template: `<div #host style="display: contents;"></div>`, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
406
483
|
}
|
|
407
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "
|
|
484
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: DynamicInput, decorators: [{
|
|
408
485
|
type: Component,
|
|
409
486
|
args: [{
|
|
410
487
|
selector: 'dfk-dynamic-input',
|
|
@@ -429,11 +506,29 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImpo
|
|
|
429
506
|
class FieldInput {
|
|
430
507
|
cdr;
|
|
431
508
|
fieldDescription;
|
|
509
|
+
/**
|
|
510
|
+
* Data at this field's own level. Preferred over `value`: the shared
|
|
511
|
+
* `buildFieldRendererProps` needs the surrounding data to resolve
|
|
512
|
+
* `appearCondition`, dynamic options and cross-field validation. When only
|
|
513
|
+
* `value` is bound (mounting this component directly), a one-key object is
|
|
514
|
+
* synthesised from it.
|
|
515
|
+
*/
|
|
516
|
+
data;
|
|
517
|
+
/** Top-level form data, for fields nested inside a repeatable group. */
|
|
518
|
+
rootData;
|
|
432
519
|
value;
|
|
433
520
|
options;
|
|
434
521
|
disabled;
|
|
435
522
|
readOnly;
|
|
436
523
|
error;
|
|
524
|
+
/** Distinguishes a controlled empty error from an omitted error input. */
|
|
525
|
+
validationControlled = false;
|
|
526
|
+
/** Whether the field has been blurred, or marked touched by a form store. */
|
|
527
|
+
touched;
|
|
528
|
+
/** Whether the value differs from the one the form opened with. */
|
|
529
|
+
dirty;
|
|
530
|
+
/** Per-form-instance id namespace; see core's `makeFieldId`. */
|
|
531
|
+
idPrefix = 'dfk-field';
|
|
437
532
|
onValueChangeField = new EventEmitter();
|
|
438
533
|
/**
|
|
439
534
|
* Emits this field's name when focus leaves it. Driven by `focusout`, which
|
|
@@ -441,50 +536,82 @@ class FieldInput {
|
|
|
441
536
|
* declare a blur output of its own.
|
|
442
537
|
*/
|
|
443
538
|
onBlurField = new EventEmitter();
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
return undefined;
|
|
451
|
-
}
|
|
452
|
-
if (typeof this.fieldDescription.options === 'function') {
|
|
453
|
-
return undefined;
|
|
454
|
-
}
|
|
455
|
-
return this.fieldDescription.options;
|
|
456
|
-
}
|
|
539
|
+
/**
|
|
540
|
+
* The full renderer prop bag, built once per change-detection pass rather
|
|
541
|
+
* than from one getter per binding - `buildFieldRendererProps` validates the
|
|
542
|
+
* field, and running that ~20 times per pass would be wasteful.
|
|
543
|
+
*/
|
|
544
|
+
rendererProps = null;
|
|
457
545
|
constructor(cdr) {
|
|
458
546
|
this.cdr = cdr;
|
|
459
547
|
}
|
|
460
548
|
ngOnChanges(_changes) {
|
|
461
|
-
this.
|
|
549
|
+
this.rendererProps = this.buildProps();
|
|
462
550
|
this.cdr.markForCheck();
|
|
463
551
|
}
|
|
464
|
-
|
|
465
|
-
|
|
552
|
+
buildProps() {
|
|
553
|
+
const field = this.fieldDescription;
|
|
554
|
+
if (!field) {
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
const data = this.data ?? { [field.name]: this.value };
|
|
558
|
+
const base = buildFieldRendererProps({
|
|
559
|
+
fieldDescription: field,
|
|
560
|
+
data,
|
|
561
|
+
rootData: this.rootData,
|
|
562
|
+
id: makeFieldId(field, this.idPrefix),
|
|
563
|
+
touched: this.touched,
|
|
564
|
+
dirty: this.dirty,
|
|
565
|
+
});
|
|
566
|
+
// Explicitly bound inputs override what the field description resolves to,
|
|
567
|
+
// so a host can still drive options/disabled/error itself.
|
|
568
|
+
const error = this.validationControlled
|
|
569
|
+
? this.error
|
|
570
|
+
: (this.error ?? base.error);
|
|
571
|
+
return {
|
|
572
|
+
...base,
|
|
573
|
+
options: this.options ?? base.options,
|
|
574
|
+
disabled: this.disabled ?? base.disabled,
|
|
575
|
+
readOnly: this.readOnly ?? base.readOnly,
|
|
576
|
+
error,
|
|
577
|
+
ariaInvalid: Boolean(error),
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: FieldInput, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
|
|
581
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.22", type: FieldInput, isStandalone: true, selector: "dfk-field-input", inputs: { fieldDescription: "fieldDescription", data: "data", rootData: "rootData", value: "value", options: "options", disabled: "disabled", readOnly: "readOnly", error: "error", validationControlled: "validationControlled", touched: "touched", dirty: "dirty", idPrefix: "idPrefix" }, outputs: { onValueChangeField: "onValueChangeField", onBlurField: "onBlurField" }, usesOnChanges: true, ngImport: i0, template: `
|
|
466
582
|
<dfk-dynamic-input
|
|
467
|
-
*ngIf="
|
|
468
|
-
[type]="
|
|
469
|
-
[value]="value"
|
|
470
|
-
[label]="
|
|
471
|
-
[placeholder]="
|
|
472
|
-
[required]="
|
|
473
|
-
[description]="$any(
|
|
474
|
-
[options]="
|
|
475
|
-
[className]="
|
|
583
|
+
*ngIf="rendererProps as p"
|
|
584
|
+
[type]="p.type"
|
|
585
|
+
[value]="p.value"
|
|
586
|
+
[label]="p.label"
|
|
587
|
+
[placeholder]="p.placeholder"
|
|
588
|
+
[required]="p.required"
|
|
589
|
+
[description]="$any(p.description)"
|
|
590
|
+
[options]="$any(p.options)"
|
|
591
|
+
[className]="p.className"
|
|
592
|
+
[disabled]="p.disabled"
|
|
593
|
+
[readOnly]="p.readOnly"
|
|
594
|
+
[touched]="p.touched"
|
|
595
|
+
[dirty]="p.dirty"
|
|
596
|
+
[error]="$any(p.error)"
|
|
597
|
+
[id]="p.id"
|
|
598
|
+
[ariaInvalid]="p.ariaInvalid"
|
|
599
|
+
[ariaDescribedBy]="p.ariaDescribedBy"
|
|
600
|
+
[ariaRequired]="p.ariaRequired"
|
|
601
|
+
[min]="p.min"
|
|
602
|
+
[max]="p.max"
|
|
603
|
+
[step]="p.step"
|
|
604
|
+
[accept]="p.accept"
|
|
605
|
+
[multiple]="p.multiple"
|
|
476
606
|
(valueChange)="
|
|
477
607
|
onValueChangeField.emit({ value: $event, key: fieldDescription!.name })
|
|
478
608
|
"
|
|
479
609
|
(focusout)="onBlurField.emit(fieldDescription!.name)"
|
|
480
|
-
[
|
|
481
|
-
[readOnly]="readOnly"
|
|
482
|
-
[error]="$any(error)"
|
|
483
|
-
[extraProps]="fieldDescription!.props"
|
|
610
|
+
[extraProps]="p.extraProps"
|
|
484
611
|
></dfk-dynamic-input>
|
|
485
612
|
`, isInline: true, dependencies: [{ kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: DynamicInput, selector: "dfk-dynamic-input", inputs: ["type", "extraProps"], outputs: ["valueChange", "onChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
486
613
|
}
|
|
487
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "
|
|
614
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: FieldInput, decorators: [{
|
|
488
615
|
type: Component,
|
|
489
616
|
args: [{
|
|
490
617
|
selector: 'dfk-field-input',
|
|
@@ -493,28 +620,43 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImpo
|
|
|
493
620
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
494
621
|
template: `
|
|
495
622
|
<dfk-dynamic-input
|
|
496
|
-
*ngIf="
|
|
497
|
-
[type]="
|
|
498
|
-
[value]="value"
|
|
499
|
-
[label]="
|
|
500
|
-
[placeholder]="
|
|
501
|
-
[required]="
|
|
502
|
-
[description]="$any(
|
|
503
|
-
[options]="
|
|
504
|
-
[className]="
|
|
623
|
+
*ngIf="rendererProps as p"
|
|
624
|
+
[type]="p.type"
|
|
625
|
+
[value]="p.value"
|
|
626
|
+
[label]="p.label"
|
|
627
|
+
[placeholder]="p.placeholder"
|
|
628
|
+
[required]="p.required"
|
|
629
|
+
[description]="$any(p.description)"
|
|
630
|
+
[options]="$any(p.options)"
|
|
631
|
+
[className]="p.className"
|
|
632
|
+
[disabled]="p.disabled"
|
|
633
|
+
[readOnly]="p.readOnly"
|
|
634
|
+
[touched]="p.touched"
|
|
635
|
+
[dirty]="p.dirty"
|
|
636
|
+
[error]="$any(p.error)"
|
|
637
|
+
[id]="p.id"
|
|
638
|
+
[ariaInvalid]="p.ariaInvalid"
|
|
639
|
+
[ariaDescribedBy]="p.ariaDescribedBy"
|
|
640
|
+
[ariaRequired]="p.ariaRequired"
|
|
641
|
+
[min]="p.min"
|
|
642
|
+
[max]="p.max"
|
|
643
|
+
[step]="p.step"
|
|
644
|
+
[accept]="p.accept"
|
|
645
|
+
[multiple]="p.multiple"
|
|
505
646
|
(valueChange)="
|
|
506
647
|
onValueChangeField.emit({ value: $event, key: fieldDescription!.name })
|
|
507
648
|
"
|
|
508
649
|
(focusout)="onBlurField.emit(fieldDescription!.name)"
|
|
509
|
-
[
|
|
510
|
-
[readOnly]="readOnly"
|
|
511
|
-
[error]="$any(error)"
|
|
512
|
-
[extraProps]="fieldDescription!.props"
|
|
650
|
+
[extraProps]="p.extraProps"
|
|
513
651
|
></dfk-dynamic-input>
|
|
514
652
|
`,
|
|
515
653
|
}]
|
|
516
654
|
}], ctorParameters: () => [{ type: i0.ChangeDetectorRef }], propDecorators: { fieldDescription: [{
|
|
517
655
|
type: Input
|
|
656
|
+
}], data: [{
|
|
657
|
+
type: Input
|
|
658
|
+
}], rootData: [{
|
|
659
|
+
type: Input
|
|
518
660
|
}], value: [{
|
|
519
661
|
type: Input
|
|
520
662
|
}], options: [{
|
|
@@ -525,6 +667,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImpo
|
|
|
525
667
|
type: Input
|
|
526
668
|
}], error: [{
|
|
527
669
|
type: Input
|
|
670
|
+
}], validationControlled: [{
|
|
671
|
+
type: Input
|
|
672
|
+
}], touched: [{
|
|
673
|
+
type: Input
|
|
674
|
+
}], dirty: [{
|
|
675
|
+
type: Input
|
|
676
|
+
}], idPrefix: [{
|
|
677
|
+
type: Input
|
|
528
678
|
}], onValueChangeField: [{
|
|
529
679
|
type: Output
|
|
530
680
|
}], onBlurField: [{
|
|
@@ -532,6 +682,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImpo
|
|
|
532
682
|
}] } });
|
|
533
683
|
|
|
534
684
|
const DEFAULT_BREAKPOINT = 768;
|
|
685
|
+
// Per-instance id counter, so two forms rendering the same field name emit
|
|
686
|
+
// different DOM ids.
|
|
687
|
+
/**
|
|
688
|
+
* Shared empty maps. A fresh object literal in a template binding is a new
|
|
689
|
+
* identity on every change detection pass, which defeats the per-item caching
|
|
690
|
+
* below and trips dev-mode checkNoChanges (NG0100).
|
|
691
|
+
*/
|
|
692
|
+
const EMPTY_ERRORS = Object.freeze({});
|
|
693
|
+
const EMPTY_TOUCHED = Object.freeze({});
|
|
694
|
+
let instanceCounter = 0;
|
|
695
|
+
function nextInstanceId() {
|
|
696
|
+
instanceCounter += 1;
|
|
697
|
+
return instanceCounter;
|
|
698
|
+
}
|
|
535
699
|
class MultiFieldInput {
|
|
536
700
|
cdr;
|
|
537
701
|
fieldDescriptions = [];
|
|
@@ -545,14 +709,112 @@ class MultiFieldInput {
|
|
|
545
709
|
* `validateOnBlur` behaviour.
|
|
546
710
|
*/
|
|
547
711
|
onBlurField = new EventEmitter();
|
|
712
|
+
/**
|
|
713
|
+
* Controlled touched map. When bound it is the single source of truth and
|
|
714
|
+
* the internal tracker is bypassed, so `createDynamicFormStore`'s `touched`
|
|
715
|
+
* signal (updated by setFieldTouched/touchAll/handleSubmit, cleared by
|
|
716
|
+
* reset) is what renderers actually see. Leave it unbound to keep the
|
|
717
|
+
* internal, blur-only tracker.
|
|
718
|
+
*/
|
|
719
|
+
touched;
|
|
720
|
+
/** Controlled error map; bind the form store's `errors()` signal here. */
|
|
721
|
+
errors;
|
|
722
|
+
/** Emits the next touched map whenever a field is blurred. */
|
|
723
|
+
touchedChange = new EventEmitter();
|
|
724
|
+
/**
|
|
725
|
+
* Namespace for generated field ids: a field renders with
|
|
726
|
+
* `${idPrefix}-${name}`. Defaults to a value unique to this component
|
|
727
|
+
* instance, so two forms containing the same field name do not emit
|
|
728
|
+
* duplicate DOM ids. Bind a fixed string to pin ids (`idPrefix="dfk-field"`
|
|
729
|
+
* restores the pre-1.6 ids), or set `FieldDescription.id` per field.
|
|
730
|
+
*/
|
|
731
|
+
idPrefix;
|
|
548
732
|
touchedFields = {};
|
|
733
|
+
// Unique per component instance. Angular has no useId equivalent, and this
|
|
734
|
+
// component is client-rendered by the time ids matter, so a module counter
|
|
735
|
+
// is enough.
|
|
736
|
+
instanceId = nextInstanceId();
|
|
737
|
+
initialProperties = {};
|
|
738
|
+
indexedErrorsSource;
|
|
739
|
+
indexedErrors = new Map();
|
|
740
|
+
indexedTouchedSource;
|
|
741
|
+
indexedTouched = new Map();
|
|
742
|
+
get effectiveIdPrefix() {
|
|
743
|
+
return this.idPrefix ?? `dfk-${this.instanceId}`;
|
|
744
|
+
}
|
|
745
|
+
/** The touched map in effect: the controlled input, else the internal one. */
|
|
746
|
+
get effectiveTouched() {
|
|
747
|
+
return this.touched ?? this.touchedFields;
|
|
748
|
+
}
|
|
549
749
|
handleBlurField(fieldName) {
|
|
550
|
-
|
|
750
|
+
if (this.touched === undefined) {
|
|
751
|
+
this.touchedFields = { ...this.touchedFields, [fieldName]: true };
|
|
752
|
+
}
|
|
753
|
+
this.touchedChange.emit({ ...this.effectiveTouched, [fieldName]: true });
|
|
551
754
|
this.onBlurField.emit(fieldName);
|
|
755
|
+
this.cdr.markForCheck();
|
|
552
756
|
}
|
|
553
757
|
/** Whether this field has been blurred at least once. */
|
|
554
758
|
isTouched(fieldName) {
|
|
555
|
-
return Boolean(this.
|
|
759
|
+
return Boolean(this.effectiveTouched[fieldName]);
|
|
760
|
+
}
|
|
761
|
+
/** Whether this field's value differs from the one the form opened with. */
|
|
762
|
+
isFieldDirty(fieldName) {
|
|
763
|
+
return this.data[fieldName] !== this.initialProperties[fieldName];
|
|
764
|
+
}
|
|
765
|
+
fieldErrors(fieldName) {
|
|
766
|
+
return this.errors?.[fieldName];
|
|
767
|
+
}
|
|
768
|
+
errorsForItem(fieldName, index) {
|
|
769
|
+
if (this.indexedErrorsSource !== this.errors) {
|
|
770
|
+
this.indexedErrorsSource = this.errors;
|
|
771
|
+
this.indexedErrors.clear();
|
|
772
|
+
}
|
|
773
|
+
if (!this.indexedErrors.has(fieldName)) {
|
|
774
|
+
this.indexedErrors.set(fieldName, indexGroupPathMap(this.errors, fieldName));
|
|
775
|
+
}
|
|
776
|
+
// An item with no errors still gets a map when the parent has one, so a
|
|
777
|
+
// nested input is never handed `undefined` for a controlled error map.
|
|
778
|
+
return this.errors === undefined
|
|
779
|
+
? undefined
|
|
780
|
+
: (this.indexedErrors.get(fieldName)?.[index] ?? EMPTY_ERRORS);
|
|
781
|
+
}
|
|
782
|
+
touchedForItem(fieldName, index) {
|
|
783
|
+
const current = this.effectiveTouched;
|
|
784
|
+
if (this.indexedTouchedSource !== current) {
|
|
785
|
+
this.indexedTouchedSource = current;
|
|
786
|
+
this.indexedTouched.clear();
|
|
787
|
+
}
|
|
788
|
+
if (!this.indexedTouched.has(fieldName)) {
|
|
789
|
+
this.indexedTouched.set(fieldName, indexGroupPathMap(current, fieldName));
|
|
790
|
+
}
|
|
791
|
+
// A fresh {} here would be a new binding identity on every change
|
|
792
|
+
// detection pass, re-running the nested input's ngOnChanges each time and
|
|
793
|
+
// risking NG0100 in dev mode.
|
|
794
|
+
return this.indexedTouched.get(fieldName)?.[index] ?? EMPTY_TOUCHED;
|
|
795
|
+
}
|
|
796
|
+
handleGroupBlur(fieldName, index, key) {
|
|
797
|
+
this.handleBlurField(`${fieldName}[${index}].${key}`);
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* Clears the internally tracked touched state. Only meaningful in
|
|
801
|
+
* uncontrolled mode - when `touched` is bound, resetting the form store
|
|
802
|
+
* (e.g. `createDynamicFormStore().reset()`) already clears it.
|
|
803
|
+
*/
|
|
804
|
+
resetTouched() {
|
|
805
|
+
this.touchedFields = {};
|
|
806
|
+
this.touchedChange.emit(this.effectiveTouched);
|
|
807
|
+
this.cdr.markForCheck();
|
|
808
|
+
}
|
|
809
|
+
setFieldTouched(fieldName, isTouched = true) {
|
|
810
|
+
if (this.touched === undefined) {
|
|
811
|
+
this.touchedFields = { ...this.touchedFields, [fieldName]: isTouched };
|
|
812
|
+
}
|
|
813
|
+
this.touchedChange.emit({
|
|
814
|
+
...this.effectiveTouched,
|
|
815
|
+
[fieldName]: isTouched,
|
|
816
|
+
});
|
|
817
|
+
this.cdr.markForCheck();
|
|
556
818
|
}
|
|
557
819
|
layout = 'column';
|
|
558
820
|
// Top-level form data, threaded down through repeatable groups so a nested
|
|
@@ -561,6 +823,7 @@ class MultiFieldInput {
|
|
|
561
823
|
rootData;
|
|
562
824
|
data = {};
|
|
563
825
|
visibleFields = [];
|
|
826
|
+
initialised = false;
|
|
564
827
|
isMobile = false;
|
|
565
828
|
constructor(cdr) {
|
|
566
829
|
this.cdr = cdr;
|
|
@@ -637,6 +900,12 @@ class MultiFieldInput {
|
|
|
637
900
|
init() {
|
|
638
901
|
if (this.properties) {
|
|
639
902
|
this.data = applyComputedValues(this.fieldDescriptions, { ...this.properties }, this.rootData);
|
|
903
|
+
// Baseline for the `dirty` flag: the values the form opened with, not
|
|
904
|
+
// whatever `properties` happens to hold after later edits.
|
|
905
|
+
if (!this.initialised) {
|
|
906
|
+
this.initialProperties = { ...this.properties };
|
|
907
|
+
this.initialised = true;
|
|
908
|
+
}
|
|
640
909
|
}
|
|
641
910
|
this.updateVisibleFields();
|
|
642
911
|
this.validityChange.emit(validateFields(this.fieldDescriptions, this.data, this.rootData));
|
|
@@ -652,22 +921,11 @@ class MultiFieldInput {
|
|
|
652
921
|
const value = this.data[field.name];
|
|
653
922
|
return Array.isArray(value) ? value : [];
|
|
654
923
|
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
}
|
|
661
|
-
getReadOnly(field) {
|
|
662
|
-
return resolveReadOnly(field, this.data, this.rootData);
|
|
663
|
-
}
|
|
664
|
-
getError(field) {
|
|
665
|
-
if (this.getDisabled(field)) {
|
|
666
|
-
return undefined;
|
|
667
|
-
}
|
|
668
|
-
const errors = validateField(field, this.data[field.name], this.data, this.rootData);
|
|
669
|
-
return errors.length > 0 ? errors : undefined;
|
|
670
|
-
}
|
|
924
|
+
// The per-field getResolvedOptions/getDisabled/getReadOnly/getError helpers
|
|
925
|
+
// that used to live here are gone: FieldInput now resolves all of it through
|
|
926
|
+
// core's shared buildFieldRendererProps. Keeping a second copy of that logic
|
|
927
|
+
// beside the shared one is exactly how the adapters drifted apart in the
|
|
928
|
+
// first place.
|
|
671
929
|
canAddItem(field) {
|
|
672
930
|
return canAddGroupItem(field, this.getItems(field));
|
|
673
931
|
}
|
|
@@ -703,8 +961,8 @@ class MultiFieldInput {
|
|
|
703
961
|
this.validityChange.emit(validateFields(this.fieldDescriptions, this.data, this.rootData));
|
|
704
962
|
this.cdr.markForCheck();
|
|
705
963
|
}
|
|
706
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "
|
|
707
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "
|
|
964
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MultiFieldInput, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
|
|
965
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.22", type: MultiFieldInput, isStandalone: true, selector: "dfk-multi-field-input", inputs: { fieldDescriptions: "fieldDescriptions", properties: "properties", touched: "touched", errors: "errors", idPrefix: "idPrefix", layout: "layout", rootData: "rootData" }, outputs: { onChange: "onChange", validityChange: "validityChange", onBlurField: "onBlurField", touchedChange: "touchedChange" }, host: { listeners: { "window:resize": "onWindowResize()" } }, usesOnChanges: true, ngImport: i0, template: `
|
|
708
966
|
<div
|
|
709
967
|
class="p-4 border rounded-lg bg-gray-50"
|
|
710
968
|
[ngClass]="{
|
|
@@ -721,11 +979,13 @@ class MultiFieldInput {
|
|
|
721
979
|
<dfk-field-input
|
|
722
980
|
*ngIf="!field.fields"
|
|
723
981
|
[fieldDescription]="field"
|
|
724
|
-
[
|
|
725
|
-
[
|
|
726
|
-
[
|
|
727
|
-
[
|
|
728
|
-
[
|
|
982
|
+
[data]="data"
|
|
983
|
+
[rootData]="rootData"
|
|
984
|
+
[idPrefix]="effectiveIdPrefix"
|
|
985
|
+
[touched]="isTouched(field.name)"
|
|
986
|
+
[dirty]="isFieldDirty(field.name)"
|
|
987
|
+
[error]="fieldErrors(field.name)"
|
|
988
|
+
[validationControlled]="errors !== undefined"
|
|
729
989
|
(onValueChangeField)="onFieldChange($event)"
|
|
730
990
|
(onBlurField)="handleBlurField($event)"
|
|
731
991
|
></dfk-field-input>
|
|
@@ -745,6 +1005,9 @@ class MultiFieldInput {
|
|
|
745
1005
|
[fieldDescriptions]="field.fields"
|
|
746
1006
|
[properties]="item"
|
|
747
1007
|
[rootData]="rootData ?? data"
|
|
1008
|
+
[errors]="errorsForItem(field.name, i)"
|
|
1009
|
+
[touched]="touchedForItem(field.name, i)"
|
|
1010
|
+
(onBlurField)="handleGroupBlur(field.name, i, $event)"
|
|
748
1011
|
(onChange)="onGroupItemChange(field, i, $event)"
|
|
749
1012
|
></dfk-multi-field-input>
|
|
750
1013
|
</div>
|
|
@@ -776,9 +1039,9 @@ class MultiFieldInput {
|
|
|
776
1039
|
</div>
|
|
777
1040
|
</ng-container>
|
|
778
1041
|
</div>
|
|
779
|
-
`, isInline: true, dependencies: [{ kind: "component", type: MultiFieldInput, selector: "dfk-multi-field-input", inputs: ["fieldDescriptions", "properties", "layout", "rootData"], outputs: ["onChange", "validityChange", "onBlurField"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgFor, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: FieldInput, selector: "dfk-field-input", inputs: ["fieldDescription", "value", "options", "disabled", "readOnly", "error"], outputs: ["onValueChangeField", "onBlurField"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1042
|
+
`, isInline: true, dependencies: [{ kind: "component", type: MultiFieldInput, selector: "dfk-multi-field-input", inputs: ["fieldDescriptions", "properties", "touched", "errors", "idPrefix", "layout", "rootData"], outputs: ["onChange", "validityChange", "onBlurField", "touchedChange"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgFor, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: FieldInput, selector: "dfk-field-input", inputs: ["fieldDescription", "data", "rootData", "value", "options", "disabled", "readOnly", "error", "validationControlled", "touched", "dirty", "idPrefix"], outputs: ["onValueChangeField", "onBlurField"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
780
1043
|
}
|
|
781
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "
|
|
1044
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MultiFieldInput, decorators: [{
|
|
782
1045
|
type: Component,
|
|
783
1046
|
args: [{
|
|
784
1047
|
selector: 'dfk-multi-field-input',
|
|
@@ -808,11 +1071,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImpo
|
|
|
808
1071
|
<dfk-field-input
|
|
809
1072
|
*ngIf="!field.fields"
|
|
810
1073
|
[fieldDescription]="field"
|
|
811
|
-
[
|
|
812
|
-
[
|
|
813
|
-
[
|
|
814
|
-
[
|
|
815
|
-
[
|
|
1074
|
+
[data]="data"
|
|
1075
|
+
[rootData]="rootData"
|
|
1076
|
+
[idPrefix]="effectiveIdPrefix"
|
|
1077
|
+
[touched]="isTouched(field.name)"
|
|
1078
|
+
[dirty]="isFieldDirty(field.name)"
|
|
1079
|
+
[error]="fieldErrors(field.name)"
|
|
1080
|
+
[validationControlled]="errors !== undefined"
|
|
816
1081
|
(onValueChangeField)="onFieldChange($event)"
|
|
817
1082
|
(onBlurField)="handleBlurField($event)"
|
|
818
1083
|
></dfk-field-input>
|
|
@@ -832,6 +1097,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImpo
|
|
|
832
1097
|
[fieldDescriptions]="field.fields"
|
|
833
1098
|
[properties]="item"
|
|
834
1099
|
[rootData]="rootData ?? data"
|
|
1100
|
+
[errors]="errorsForItem(field.name, i)"
|
|
1101
|
+
[touched]="touchedForItem(field.name, i)"
|
|
1102
|
+
(onBlurField)="handleGroupBlur(field.name, i, $event)"
|
|
835
1103
|
(onChange)="onGroupItemChange(field, i, $event)"
|
|
836
1104
|
></dfk-multi-field-input>
|
|
837
1105
|
</div>
|
|
@@ -875,6 +1143,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImpo
|
|
|
875
1143
|
type: Output
|
|
876
1144
|
}], onBlurField: [{
|
|
877
1145
|
type: Output
|
|
1146
|
+
}], touched: [{
|
|
1147
|
+
type: Input
|
|
1148
|
+
}], errors: [{
|
|
1149
|
+
type: Input
|
|
1150
|
+
}], touchedChange: [{
|
|
1151
|
+
type: Output
|
|
1152
|
+
}], idPrefix: [{
|
|
1153
|
+
type: Input
|
|
878
1154
|
}], layout: [{
|
|
879
1155
|
type: Input
|
|
880
1156
|
}], rootData: [{
|
|
@@ -894,16 +1170,16 @@ class DynamicFormDevToolsComponent {
|
|
|
894
1170
|
errorCount() {
|
|
895
1171
|
return Object.keys(this.errors || {}).length;
|
|
896
1172
|
}
|
|
897
|
-
isOpen = signal(false);
|
|
898
|
-
activeTab = signal('data');
|
|
1173
|
+
isOpen = signal(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : /* istanbul ignore next */ []));
|
|
1174
|
+
activeTab = signal('data', ...(ngDevMode ? [{ debugName: "activeTab" }] : /* istanbul ignore next */ []));
|
|
899
1175
|
tabs = [
|
|
900
1176
|
'data',
|
|
901
1177
|
'errors',
|
|
902
1178
|
'meta',
|
|
903
1179
|
'fields',
|
|
904
1180
|
];
|
|
905
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "
|
|
906
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "
|
|
1181
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: DynamicFormDevToolsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1182
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.22", type: DynamicFormDevToolsComponent, isStandalone: true, selector: "dfk-dev-tools", inputs: { data: "data", errors: "errors", touched: "touched", isDirty: "isDirty", fields: "fields" }, ngImport: i0, template: `
|
|
907
1183
|
<button
|
|
908
1184
|
*ngIf="!isOpen()"
|
|
909
1185
|
type="button"
|
|
@@ -1037,7 +1313,7 @@ class DynamicFormDevToolsComponent {
|
|
|
1037
1313
|
</div>
|
|
1038
1314
|
`, isInline: true, dependencies: [{ kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: NgFor, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "pipe", type: JsonPipe, name: "json" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1039
1315
|
}
|
|
1040
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "
|
|
1316
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: DynamicFormDevToolsComponent, decorators: [{
|
|
1041
1317
|
type: Component,
|
|
1042
1318
|
args: [{
|
|
1043
1319
|
selector: 'dfk-dev-tools',
|
|
@@ -1195,24 +1471,66 @@ function createDynamicFormStore(options) {
|
|
|
1195
1471
|
const initialValues = options.initialValues || {};
|
|
1196
1472
|
const validateOnBlur = options.validateOnBlur ?? true;
|
|
1197
1473
|
const validateOnChange = options.validateOnChange ?? false;
|
|
1198
|
-
const data = signal(applyComputedValues(fields, initialValues));
|
|
1199
|
-
const errors = signal({});
|
|
1200
|
-
const isDirty = signal(false);
|
|
1201
|
-
const touched = signal({});
|
|
1202
|
-
const isSubmitting = signal(false);
|
|
1203
|
-
const isSubmitted = signal(false);
|
|
1204
|
-
const
|
|
1474
|
+
const data = signal(applyComputedValues(fields, initialValues), ...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
|
|
1475
|
+
const errors = signal({}, ...(ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []));
|
|
1476
|
+
const isDirty = signal(false, ...(ngDevMode ? [{ debugName: "isDirty" }] : /* istanbul ignore next */ []));
|
|
1477
|
+
const touched = signal({}, ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
|
|
1478
|
+
const isSubmitting = signal(false, ...(ngDevMode ? [{ debugName: "isSubmitting" }] : /* istanbul ignore next */ []));
|
|
1479
|
+
const isSubmitted = signal(false, ...(ngDevMode ? [{ debugName: "isSubmitted" }] : /* istanbul ignore next */ []));
|
|
1480
|
+
const validationResult = signal(validateFields(fields, data()), ...(ngDevMode ? [{ debugName: "validationResult" }] : /* istanbul ignore next */ []));
|
|
1481
|
+
const isValidating = signal(false, ...(ngDevMode ? [{ debugName: "isValidating" }] : /* istanbul ignore next */ []));
|
|
1482
|
+
let validationRun = 0;
|
|
1483
|
+
let validationController;
|
|
1484
|
+
// A submit gets its own run counter and controller. Typing aborts the live
|
|
1485
|
+
// validation run, and a submit must not be collateral damage of that.
|
|
1486
|
+
let submitRun = 0;
|
|
1487
|
+
let submitController;
|
|
1488
|
+
const isValid = computed(() => validationResult().valid, ...(ngDevMode ? [{ debugName: "isValid" }] : /* istanbul ignore next */ []));
|
|
1489
|
+
const isValidationComplete = computed(() => validationResult().complete && !isValidating(), ...(ngDevMode ? [{ debugName: "isValidationComplete" }] : /* istanbul ignore next */ []));
|
|
1490
|
+
const validationStatus = computed(() => isValidating() ? 'pending' : validationResult().status, ...(ngDevMode ? [{ debugName: "validationStatus" }] : /* istanbul ignore next */ []));
|
|
1491
|
+
function commitSyncResult(res) {
|
|
1492
|
+
validationResult.set(res);
|
|
1493
|
+
return res.valid;
|
|
1494
|
+
}
|
|
1205
1495
|
function validate() {
|
|
1206
1496
|
const res = validateFields(fields, data());
|
|
1207
1497
|
errors.set(res.errors);
|
|
1208
|
-
return res
|
|
1498
|
+
return commitSyncResult(res);
|
|
1499
|
+
}
|
|
1500
|
+
async function validateAsync() {
|
|
1501
|
+
const run = ++validationRun;
|
|
1502
|
+
validationController?.abort();
|
|
1503
|
+
const controller = new AbortController();
|
|
1504
|
+
validationController = controller;
|
|
1505
|
+
const snapshot = data();
|
|
1506
|
+
isValidating.set(true);
|
|
1507
|
+
try {
|
|
1508
|
+
const res = await validateFieldsAsync(fields, snapshot, snapshot, {
|
|
1509
|
+
signal: controller.signal,
|
|
1510
|
+
});
|
|
1511
|
+
if (run !== validationRun || data() !== snapshot) {
|
|
1512
|
+
return res.valid;
|
|
1513
|
+
}
|
|
1514
|
+
errors.set(res.errors);
|
|
1515
|
+
validationResult.set(res);
|
|
1516
|
+
return res.valid;
|
|
1517
|
+
}
|
|
1518
|
+
finally {
|
|
1519
|
+
if (run === validationRun) {
|
|
1520
|
+
isValidating.set(false);
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1209
1523
|
}
|
|
1210
1524
|
function handleChange(newData) {
|
|
1211
1525
|
const next = applyComputedValues(fields, newData);
|
|
1212
1526
|
data.set(next);
|
|
1213
1527
|
isDirty.set(true);
|
|
1528
|
+
validationController?.abort();
|
|
1529
|
+
validationRun += 1;
|
|
1530
|
+
isValidating.set(false);
|
|
1531
|
+
const res = validateFields(fields, next);
|
|
1532
|
+
commitSyncResult(res);
|
|
1214
1533
|
if (validateOnChange) {
|
|
1215
|
-
const res = validateFields(fields, next);
|
|
1216
1534
|
errors.set(res.errors);
|
|
1217
1535
|
}
|
|
1218
1536
|
}
|
|
@@ -1222,11 +1540,24 @@ function createDynamicFormStore(options) {
|
|
|
1222
1540
|
function setFieldTouched(name, isTouched = true) {
|
|
1223
1541
|
touched.set({ ...touched(), [name]: isTouched });
|
|
1224
1542
|
}
|
|
1543
|
+
/**
|
|
1544
|
+
* Marks every field touched at once. `handleSubmit` calls this for you, so
|
|
1545
|
+
* an invalid submit surfaces errors on fields the user never focused - bind
|
|
1546
|
+
* `[touched]` on `<dfk-multi-field-input>` for it to take effect.
|
|
1547
|
+
*/
|
|
1548
|
+
function touchAll() {
|
|
1549
|
+
touched.set(Object.fromEntries(collectFieldPaths(fields, data()).map((path) => [path, true])));
|
|
1550
|
+
}
|
|
1551
|
+
/** Clears the touched map without touching data, errors or dirty state. */
|
|
1552
|
+
function resetTouched() {
|
|
1553
|
+
touched.set({});
|
|
1554
|
+
}
|
|
1225
1555
|
function handleBlur(fieldName) {
|
|
1226
1556
|
setFieldTouched(fieldName, true);
|
|
1227
1557
|
if (validateOnBlur) {
|
|
1228
1558
|
const res = validateFields(fields, data());
|
|
1229
1559
|
errors.set(res.errors);
|
|
1560
|
+
commitSyncResult(res);
|
|
1230
1561
|
}
|
|
1231
1562
|
}
|
|
1232
1563
|
function reset(newValues) {
|
|
@@ -1238,6 +1569,10 @@ function createDynamicFormStore(options) {
|
|
|
1238
1569
|
touched.set({});
|
|
1239
1570
|
isSubmitting.set(false);
|
|
1240
1571
|
isSubmitted.set(false);
|
|
1572
|
+
validationController?.abort();
|
|
1573
|
+
validationRun += 1;
|
|
1574
|
+
isValidating.set(false);
|
|
1575
|
+
commitSyncResult(validateFields(fields, next));
|
|
1241
1576
|
}
|
|
1242
1577
|
/**
|
|
1243
1578
|
* Returns a submit handler, mirroring the React and Vue `useDynamicForm`
|
|
@@ -1250,18 +1585,53 @@ function createDynamicFormStore(options) {
|
|
|
1250
1585
|
e.preventDefault();
|
|
1251
1586
|
}
|
|
1252
1587
|
isSubmitting.set(true);
|
|
1588
|
+
const thisSubmit = ++submitRun;
|
|
1253
1589
|
try {
|
|
1254
|
-
|
|
1255
|
-
|
|
1590
|
+
// Touch everything before validating: a submit is the user asserting
|
|
1591
|
+
// the form is finished, so a field they never focused should still
|
|
1592
|
+
// show its error. Without this, submitting an untouched form appears
|
|
1593
|
+
// to do nothing at all.
|
|
1594
|
+
touchAll();
|
|
1595
|
+
const run = ++validationRun;
|
|
1596
|
+
// Cancel any live run so its (older) result cannot land on top of this
|
|
1597
|
+
// one, but validate under a controller of the submit's own.
|
|
1598
|
+
validationController?.abort();
|
|
1599
|
+
submitController?.abort();
|
|
1600
|
+
const controller = new AbortController();
|
|
1601
|
+
submitController = controller;
|
|
1602
|
+
const snapshot = data();
|
|
1603
|
+
isValidating.set(true);
|
|
1604
|
+
const res = await validateFieldsAsync(fields, snapshot, snapshot, {
|
|
1605
|
+
signal: controller.signal,
|
|
1606
|
+
});
|
|
1607
|
+
if (thisSubmit !== submitRun) {
|
|
1608
|
+
return;
|
|
1609
|
+
}
|
|
1610
|
+
// Editing during the submit does not cancel it - the user submitted
|
|
1611
|
+
// this snapshot and is owed an answer for it. What the form *shows*
|
|
1612
|
+
// still has to describe the data on screen, so when it moved on, the
|
|
1613
|
+
// displayed state is re-derived instead of showing the old pass.
|
|
1614
|
+
if (data() === snapshot && run === validationRun) {
|
|
1615
|
+
errors.set(res.errors);
|
|
1616
|
+
validationResult.set(res);
|
|
1617
|
+
}
|
|
1618
|
+
else {
|
|
1619
|
+
const live = validateFields(fields, data());
|
|
1620
|
+
errors.set(live.errors);
|
|
1621
|
+
validationResult.set(live);
|
|
1622
|
+
}
|
|
1256
1623
|
isSubmitted.set(true);
|
|
1257
1624
|
if (res.valid) {
|
|
1258
|
-
await onValid(
|
|
1625
|
+
await onValid(snapshot);
|
|
1259
1626
|
}
|
|
1260
1627
|
else if (onInvalid) {
|
|
1261
1628
|
onInvalid(res.errors);
|
|
1262
1629
|
}
|
|
1263
1630
|
}
|
|
1264
1631
|
finally {
|
|
1632
|
+
if (thisSubmit === submitRun) {
|
|
1633
|
+
isValidating.set(false);
|
|
1634
|
+
}
|
|
1265
1635
|
isSubmitting.set(false);
|
|
1266
1636
|
}
|
|
1267
1637
|
};
|
|
@@ -1270,16 +1640,22 @@ function createDynamicFormStore(options) {
|
|
|
1270
1640
|
data,
|
|
1271
1641
|
errors,
|
|
1272
1642
|
isValid,
|
|
1643
|
+
isValidating,
|
|
1644
|
+
isValidationComplete,
|
|
1645
|
+
validationStatus,
|
|
1273
1646
|
isDirty,
|
|
1274
1647
|
touched,
|
|
1275
1648
|
isSubmitting,
|
|
1276
1649
|
isSubmitted,
|
|
1277
1650
|
setFieldValue,
|
|
1278
1651
|
setFieldTouched,
|
|
1652
|
+
touchAll,
|
|
1653
|
+
resetTouched,
|
|
1279
1654
|
handleChange,
|
|
1280
1655
|
handleBlur,
|
|
1281
1656
|
reset,
|
|
1282
1657
|
validate,
|
|
1658
|
+
validateAsync,
|
|
1283
1659
|
handleSubmit,
|
|
1284
1660
|
};
|
|
1285
1661
|
}
|
|
@@ -1304,8 +1680,8 @@ class ColumnLayout {
|
|
|
1304
1680
|
get gap() {
|
|
1305
1681
|
return this.config?.gap ?? 12;
|
|
1306
1682
|
}
|
|
1307
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "
|
|
1308
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "
|
|
1683
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: ColumnLayout, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1684
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.22", type: ColumnLayout, isStandalone: true, selector: "dfk-column-layout", inputs: { config: "config", template: "template" }, ngImport: i0, template: `<div
|
|
1309
1685
|
[style.display]="'flex'"
|
|
1310
1686
|
[style.flexDirection]="'column'"
|
|
1311
1687
|
[style.gap]="gap + 'px'"
|
|
@@ -1313,7 +1689,7 @@ class ColumnLayout {
|
|
|
1313
1689
|
<ng-container *ngTemplateOutlet="template"></ng-container>
|
|
1314
1690
|
</div>`, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] });
|
|
1315
1691
|
}
|
|
1316
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "
|
|
1692
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: ColumnLayout, decorators: [{
|
|
1317
1693
|
type: Component,
|
|
1318
1694
|
args: [{
|
|
1319
1695
|
selector: 'dfk-column-layout',
|
|
@@ -1338,8 +1714,8 @@ class RowLayout {
|
|
|
1338
1714
|
get gap() {
|
|
1339
1715
|
return this.config?.gap ?? 12;
|
|
1340
1716
|
}
|
|
1341
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "
|
|
1342
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "
|
|
1717
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: RowLayout, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1718
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.22", type: RowLayout, isStandalone: true, selector: "dfk-row-layout", inputs: { config: "config", template: "template" }, ngImport: i0, template: `<div
|
|
1343
1719
|
[style.display]="'flex'"
|
|
1344
1720
|
[style.flexDirection]="'row'"
|
|
1345
1721
|
[style.gap]="gap + 'px'"
|
|
@@ -1347,7 +1723,7 @@ class RowLayout {
|
|
|
1347
1723
|
<ng-container *ngTemplateOutlet="template"></ng-container>
|
|
1348
1724
|
</div>`, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] });
|
|
1349
1725
|
}
|
|
1350
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "
|
|
1726
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: RowLayout, decorators: [{
|
|
1351
1727
|
type: Component,
|
|
1352
1728
|
args: [{
|
|
1353
1729
|
selector: 'dfk-row-layout',
|
|
@@ -1375,8 +1751,8 @@ class GridLayout {
|
|
|
1375
1751
|
get gap() {
|
|
1376
1752
|
return this.config?.gap ?? 12;
|
|
1377
1753
|
}
|
|
1378
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "
|
|
1379
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "
|
|
1754
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: GridLayout, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1755
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.22", type: GridLayout, isStandalone: true, selector: "dfk-grid-layout", inputs: { config: "config", template: "template" }, ngImport: i0, template: `<div
|
|
1380
1756
|
[style.display]="'grid'"
|
|
1381
1757
|
[style.gridTemplateColumns]="'repeat(' + columns + ', 1fr)'"
|
|
1382
1758
|
[style.gap]="gap + 'px'"
|
|
@@ -1384,7 +1760,7 @@ class GridLayout {
|
|
|
1384
1760
|
<ng-container *ngTemplateOutlet="template"></ng-container>
|
|
1385
1761
|
</div>`, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] });
|
|
1386
1762
|
}
|
|
1387
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "
|
|
1763
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: GridLayout, decorators: [{
|
|
1388
1764
|
type: Component,
|
|
1389
1765
|
args: [{
|
|
1390
1766
|
selector: 'dfk-grid-layout',
|
|
@@ -1408,8 +1784,8 @@ layoutRegistry.register('row', RowLayout);
|
|
|
1408
1784
|
layoutRegistry.register('grid', GridLayout);
|
|
1409
1785
|
|
|
1410
1786
|
class DynamicFieldKitModule {
|
|
1411
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "
|
|
1412
|
-
static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "
|
|
1787
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: DynamicFieldKitModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
|
|
1788
|
+
static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.2.22", ngImport: i0, type: DynamicFieldKitModule, imports: [CommonModule,
|
|
1413
1789
|
DynamicInput,
|
|
1414
1790
|
FieldInput,
|
|
1415
1791
|
MultiFieldInput,
|
|
@@ -1421,7 +1797,7 @@ class DynamicFieldKitModule {
|
|
|
1421
1797
|
ColumnLayout,
|
|
1422
1798
|
RowLayout,
|
|
1423
1799
|
GridLayout] });
|
|
1424
|
-
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "
|
|
1800
|
+
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: DynamicFieldKitModule, imports: [CommonModule,
|
|
1425
1801
|
DynamicInput,
|
|
1426
1802
|
FieldInput,
|
|
1427
1803
|
MultiFieldInput,
|
|
@@ -1429,7 +1805,7 @@ class DynamicFieldKitModule {
|
|
|
1429
1805
|
RowLayout,
|
|
1430
1806
|
GridLayout] });
|
|
1431
1807
|
}
|
|
1432
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "
|
|
1808
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: DynamicFieldKitModule, decorators: [{
|
|
1433
1809
|
type: NgModule,
|
|
1434
1810
|
args: [{
|
|
1435
1811
|
imports: [
|