@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.
@@ -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, applyComputedValues, validateFields, resolveOptions, resolveDisabled, resolveReadOnly, validateField, canAddGroupItem, canRemoveGroupItem, createGroupItem } from '@dynamic-field-kit/core';
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: "19.2.25", ngImport: i0, type: BaseInputComponent, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
34
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.25", type: BaseInputComponent, isStandalone: true, selector: "ng-component", inputs: { value: "value", label: "label", placeholder: "placeholder", required: "required", disabled: "disabled", readOnly: "readOnly", error: "error", options: "options", className: "className", description: "description" }, outputs: { valueChange: "valueChange" }, usesOnChanges: true, ngImport: i0, template: '', isInline: true });
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: "19.2.25", ngImport: i0, type: BaseInputComponent, decorators: [{
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: "19.2.25", ngImport: i0, type: DynamicInput, deps: null, target: i0.ɵɵFactoryTarget.Component });
405
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.25", 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 });
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: "19.2.25", ngImport: i0, type: DynamicInput, decorators: [{
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
- shouldRender = false;
445
- get resolvedOptions() {
446
- if (this.options) {
447
- return this.options;
448
- }
449
- if (!this.fieldDescription?.options) {
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.shouldRender = !!this.fieldDescription;
549
+ this.rendererProps = this.buildProps();
462
550
  this.cdr.markForCheck();
463
551
  }
464
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: FieldInput, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
465
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.25", type: FieldInput, isStandalone: true, selector: "dfk-field-input", inputs: { fieldDescription: "fieldDescription", value: "value", options: "options", disabled: "disabled", readOnly: "readOnly", error: "error" }, outputs: { onValueChangeField: "onValueChangeField", onBlurField: "onBlurField" }, usesOnChanges: true, ngImport: i0, template: `
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="shouldRender"
468
- [type]="fieldDescription!.type"
469
- [value]="value"
470
- [label]="fieldDescription!.label"
471
- [placeholder]="fieldDescription!.placeholder"
472
- [required]="fieldDescription!.required"
473
- [description]="$any(fieldDescription!.description)"
474
- [options]="resolvedOptions"
475
- [className]="fieldDescription!.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
- [disabled]="disabled"
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: "19.2.25", ngImport: i0, type: FieldInput, decorators: [{
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="shouldRender"
497
- [type]="fieldDescription!.type"
498
- [value]="value"
499
- [label]="fieldDescription!.label"
500
- [placeholder]="fieldDescription!.placeholder"
501
- [required]="fieldDescription!.required"
502
- [description]="$any(fieldDescription!.description)"
503
- [options]="resolvedOptions"
504
- [className]="fieldDescription!.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
- [disabled]="disabled"
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
- this.touchedFields = { ...this.touchedFields, [fieldName]: true };
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.touchedFields[fieldName]);
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
- getResolvedOptions(field) {
656
- return resolveOptions(field, this.data, this.rootData);
657
- }
658
- getDisabled(field) {
659
- return resolveDisabled(field, this.data, this.rootData);
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: "19.2.25", ngImport: i0, type: MultiFieldInput, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
707
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.25", type: MultiFieldInput, isStandalone: true, selector: "dfk-multi-field-input", inputs: { fieldDescriptions: "fieldDescriptions", properties: "properties", layout: "layout", rootData: "rootData" }, outputs: { onChange: "onChange", validityChange: "validityChange", onBlurField: "onBlurField" }, host: { listeners: { "window:resize": "onWindowResize()" } }, usesOnChanges: true, ngImport: i0, template: `
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
- [value]="data[field.name]"
725
- [options]="getResolvedOptions(field)"
726
- [disabled]="getDisabled(field)"
727
- [readOnly]="getReadOnly(field)"
728
- [error]="getError(field)"
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: "19.2.25", ngImport: i0, type: MultiFieldInput, decorators: [{
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
- [value]="data[field.name]"
812
- [options]="getResolvedOptions(field)"
813
- [disabled]="getDisabled(field)"
814
- [readOnly]="getReadOnly(field)"
815
- [error]="getError(field)"
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: "19.2.25", ngImport: i0, type: DynamicFormDevToolsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
906
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.25", type: DynamicFormDevToolsComponent, isStandalone: true, selector: "dfk-dev-tools", inputs: { data: "data", errors: "errors", touched: "touched", isDirty: "isDirty", fields: "fields" }, ngImport: i0, template: `
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: "19.2.25", ngImport: i0, type: DynamicFormDevToolsComponent, decorators: [{
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 isValid = computed(() => Object.keys(errors()).length === 0);
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.valid;
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
- const res = validateFields(fields, data());
1255
- errors.set(res.errors);
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(data());
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: "19.2.25", ngImport: i0, type: ColumnLayout, deps: [], target: i0.ɵɵFactoryTarget.Component });
1308
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.25", type: ColumnLayout, isStandalone: true, selector: "dfk-column-layout", inputs: { config: "config", template: "template" }, ngImport: i0, template: `<div
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: "19.2.25", ngImport: i0, type: ColumnLayout, decorators: [{
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: "19.2.25", ngImport: i0, type: RowLayout, deps: [], target: i0.ɵɵFactoryTarget.Component });
1342
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.25", type: RowLayout, isStandalone: true, selector: "dfk-row-layout", inputs: { config: "config", template: "template" }, ngImport: i0, template: `<div
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: "19.2.25", ngImport: i0, type: RowLayout, decorators: [{
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: "19.2.25", ngImport: i0, type: GridLayout, deps: [], target: i0.ɵɵFactoryTarget.Component });
1379
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.25", type: GridLayout, isStandalone: true, selector: "dfk-grid-layout", inputs: { config: "config", template: "template" }, ngImport: i0, template: `<div
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: "19.2.25", ngImport: i0, type: GridLayout, decorators: [{
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: "19.2.25", ngImport: i0, type: DynamicFieldKitModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
1412
- static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.25", ngImport: i0, type: DynamicFieldKitModule, imports: [CommonModule,
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: "19.2.25", ngImport: i0, type: DynamicFieldKitModule, imports: [CommonModule,
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: "19.2.25", ngImport: i0, type: DynamicFieldKitModule, decorators: [{
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: [