@dynamic-field-kit/angular 1.3.3 → 1.4.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.
Files changed (43) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +50 -3
  3. package/dist/README.md +50 -3
  4. package/dist/components/BaseInput.d.ts +5 -11
  5. package/dist/components/DynamicInput.d.ts +6 -2
  6. package/dist/components/FieldInput.d.ts +6 -2
  7. package/dist/components/MultiFieldInput.d.ts +10 -1
  8. package/dist/esm2022/components/BaseInput.mjs +8 -17
  9. package/dist/esm2022/components/DynamicInput.mjs +67 -14
  10. package/dist/esm2022/components/FieldInput.mjs +39 -20
  11. package/dist/esm2022/components/MultiFieldInput.mjs +87 -13
  12. package/dist/esm2022/fieldRegistryToken.mjs +12 -0
  13. package/dist/esm2022/public-api.mjs +6 -2
  14. package/dist/esm2022/types/layout.mjs +1 -1
  15. package/dist/fesm2022/dynamic-field-kit-angular.mjs +208 -61
  16. package/dist/fesm2022/dynamic-field-kit-angular.mjs.map +1 -1
  17. package/dist/fieldRegistryToken.d.ts +3 -0
  18. package/dist/public-api.d.ts +4 -1
  19. package/dist/types/layout.d.ts +4 -25
  20. package/package.json +14 -16
  21. package/src/components/BaseInput.ts +7 -10
  22. package/src/components/DynamicInput.ts +67 -11
  23. package/src/components/FieldInput.ts +23 -16
  24. package/src/components/MultiFieldInput.ts +101 -8
  25. package/src/fieldRegistryToken.ts +15 -0
  26. package/src/public-api.ts +40 -24
  27. package/src/types/layout.ts +14 -36
  28. package/test/DynamicInput.spec.ts +217 -17
  29. package/test/FieldInput.spec.ts +131 -29
  30. package/test/MultiFieldInput.spec.ts +256 -0
  31. package/test/helpers/renderers.ts +89 -0
  32. package/test/layout.spec.ts +119 -0
  33. package/test/publicApi.spec.ts +64 -0
  34. package/{test.ts → test/setup.ts} +2 -2
  35. package/test/smoke.spec.ts +27 -0
  36. package/tsconfig.json +13 -13
  37. package/tsconfig.spec.json +4 -16
  38. package/vitest.config.ts +30 -0
  39. package/angular.json +0 -27
  40. package/karma.conf.js +0 -37
  41. package/test/angular.spec.ts +0 -102
  42. package/test/integration.spec.ts +0 -57
  43. package/test/layouts.spec.ts +0 -26
@@ -1,9 +1,9 @@
1
1
  import * as i0 from '@angular/core';
2
- import { EventEmitter, Output, Input, Component, ViewContainerRef, ViewChild, ChangeDetectionStrategy, HostListener, NgModule } from '@angular/core';
2
+ import { EventEmitter, Output, Input, Component, InjectionToken, inject, ViewContainerRef, ViewChild, ChangeDetectionStrategy, HostListener, NgModule } from '@angular/core';
3
3
  import * as i1 from '@angular/common';
4
4
  import { CommonModule, NgIf, NgClass, NgFor } from '@angular/common';
5
- import { fieldRegistry, applyComputedValues, canAddGroupItem, canRemoveGroupItem, createGroupItem } from '@dynamic-field-kit/core';
6
- export { fieldRegistry } from '@dynamic-field-kit/core';
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';
7
7
 
8
8
  class BaseInputComponent {
9
9
  cdr;
@@ -12,14 +12,11 @@ class BaseInputComponent {
12
12
  placeholder;
13
13
  required;
14
14
  disabled;
15
+ readOnly;
16
+ error;
15
17
  options;
16
18
  className;
17
19
  description;
18
- errorMessage;
19
- acceptFile;
20
- maxLength;
21
- minNumber;
22
- maxNumber;
23
20
  valueChange = new EventEmitter();
24
21
  constructor(cdr) {
25
22
  this.cdr = cdr;
@@ -34,7 +31,7 @@ class BaseInputComponent {
34
31
  return changes[prop] !== undefined;
35
32
  }
36
33
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: BaseInputComponent, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
37
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: BaseInputComponent, isStandalone: true, selector: "ng-component", inputs: { value: "value", label: "label", placeholder: "placeholder", required: "required", disabled: "disabled", options: "options", className: "className", description: "description", errorMessage: "errorMessage", acceptFile: "acceptFile", maxLength: "maxLength", minNumber: "minNumber", maxNumber: "maxNumber" }, outputs: { valueChange: "valueChange" }, usesOnChanges: true, ngImport: i0, template: '', isInline: true });
34
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", 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 });
38
35
  }
39
36
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: BaseInputComponent, decorators: [{
40
37
  type: Component,
@@ -51,51 +48,65 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
51
48
  type: Input
52
49
  }], disabled: [{
53
50
  type: Input
51
+ }], readOnly: [{
52
+ type: Input
53
+ }], error: [{
54
+ type: Input
54
55
  }], options: [{
55
56
  type: Input
56
57
  }], className: [{
57
58
  type: Input
58
59
  }], description: [{
59
60
  type: Input
60
- }], errorMessage: [{
61
- type: Input
62
- }], acceptFile: [{
63
- type: Input
64
- }], maxLength: [{
65
- type: Input
66
- }], minNumber: [{
67
- type: Input
68
- }], maxNumber: [{
69
- type: Input
70
61
  }], valueChange: [{
71
62
  type: Output
72
63
  }] } });
73
64
 
65
+ // Injecting this token yields the process-wide singleton by default, so code
66
+ // that never provides it keeps working. Provide it on a component/route to give
67
+ // that subtree an isolated set of renderers:
68
+ //
69
+ // providers: [{ provide: FIELD_REGISTRY, useValue: myScopedRegistry }]
70
+ const FIELD_REGISTRY = new InjectionToken('dfk.FieldRegistry', {
71
+ providedIn: 'root',
72
+ factory: () => fieldRegistry,
73
+ });
74
+
75
+ // Only the framework-agnostic FieldRendererProps keys. Domain-specific props
76
+ // reach the renderer through `extraProps` (FieldDescription.props) instead.
74
77
  const KNOWN_PROPS = [
75
78
  'value',
76
79
  'label',
77
80
  'placeholder',
78
81
  'required',
79
82
  'disabled',
83
+ 'readOnly',
84
+ 'error',
80
85
  'options',
81
86
  'className',
82
87
  'description',
83
- 'errorMessage',
84
- 'acceptFile',
85
- 'maxLength',
86
- 'minNumber',
87
- 'maxNumber',
88
88
  ];
89
89
  class DynamicInput extends BaseInputComponent {
90
90
  type;
91
+ // Extra, framework-agnostic props forwarded verbatim to the renderer.
92
+ extraProps;
91
93
  valueChange = new EventEmitter();
92
94
  onChange = new EventEmitter();
95
+ registry = inject(FIELD_REGISTRY);
93
96
  host;
94
97
  compRef;
95
98
  inputInstance;
96
99
  subscriptions = [];
100
+ // Tracks which KNOWN_PROPS were actually supplied to DynamicInput (via
101
+ // SimpleChanges), independent of TS class-field emit. See applyProps.
102
+ supplied = new Set();
97
103
  ngOnChanges(changes) {
98
104
  super.ngOnChanges(changes);
105
+ for (const prop of KNOWN_PROPS) {
106
+ if (changes[prop]) {
107
+ this.supplied.add(prop);
108
+ }
109
+ }
99
110
  if (this.inputInstance) {
100
111
  this.syncPropsToInstance(changes);
101
112
  }
@@ -121,6 +132,9 @@ class DynamicInput extends BaseInputComponent {
121
132
  this.inputInstance[prop] = this[prop];
122
133
  }
123
134
  }
135
+ if (changes['extraProps']) {
136
+ this.applyExtraProps(this.inputInstance);
137
+ }
124
138
  this.compRef?.changeDetectorRef?.detectChanges();
125
139
  }
126
140
  cleanup() {
@@ -137,7 +151,7 @@ class DynamicInput extends BaseInputComponent {
137
151
  this.inputInstance = undefined;
138
152
  }
139
153
  render() {
140
- const Renderer = fieldRegistry.get(this.type);
154
+ const Renderer = this.registry.get(this.type);
141
155
  this.cleanup();
142
156
  this.host.clear();
143
157
  if (!Renderer) {
@@ -154,8 +168,16 @@ class DynamicInput extends BaseInputComponent {
154
168
  this.renderError(`Invalid renderer for type: ${this.type}`);
155
169
  }
156
170
  }
171
+ // Angular component classes carry a static ɵcmp. Checked instead of
172
+ // reflectComponentType() because the peer range starts at Angular 13 and
173
+ // reflectComponentType is v14+. Plain function renderers have no ɵcmp and
174
+ // fall through to renderFallback. Uses hasOwnProperty (not `in`) so a
175
+ // class that merely extends a component without its own @Component
176
+ // decorator - and would otherwise inherit the parent's static ɵcmp via the
177
+ // prototype chain - is not mistaken for a component in its own right.
157
178
  isComponentType(renderer) {
158
- return (typeof renderer === 'object' && renderer !== null && 'cmp' in renderer);
179
+ return (typeof renderer === 'function' &&
180
+ Object.prototype.hasOwnProperty.call(renderer, 'ɵcmp'));
159
181
  }
160
182
  renderComponent(compType) {
161
183
  try {
@@ -191,25 +213,55 @@ class DynamicInput extends BaseInputComponent {
191
213
  }
192
214
  getFallbackProps() {
193
215
  return {
216
+ ...this.extraProps,
194
217
  value: this.value,
195
218
  onValueChange: (v) => this.emitValue(v),
196
219
  label: this.label ?? '',
197
220
  placeholder: this.placeholder ?? '',
198
221
  required: this.required ?? false,
222
+ disabled: this.disabled ?? false,
223
+ readOnly: this.readOnly ?? false,
224
+ error: this.error,
199
225
  options: this.options ?? [],
200
226
  className: this.className ?? '',
201
227
  description: this.description ?? '',
202
- disabled: this.disabled ?? false,
203
- errorMessage: this.errorMessage ?? '',
204
228
  };
205
229
  }
230
+ // Gates on `this.supplied` (populated from SimpleChanges in ngOnChanges),
231
+ // not on `prop in instanceObj` or `prop in this`. Both `in` checks test a
232
+ // TypeScript class-field emit artifact rather than intent: whether a
233
+ // property slot exists at runtime depends on the compile target
234
+ // (useDefineForClassFields is false below ES2022, true at ES2022+), so the
235
+ // same source can behave oppositely between this package's test build and
236
+ // its shipped ES2022 bundle, where every declared field is always an own
237
+ // property. `this.supplied` instead reflects only whether Angular ever
238
+ // fired a SimpleChange for that input, i.e. whether the prop was *bound*
239
+ // at all - it skips only props DynamicInput was never given a binding
240
+ // for. It is NOT a general "renderer defaults survive" guarantee: Angular
241
+ // fires a firstChange SimpleChange for every bound input even when the
242
+ // bound value is undefined, and FieldInput's template binds all
243
+ // KNOWN_PROPS unconditionally, so on the real
244
+ // MultiFieldInput -> FieldInput -> DynamicInput path every prop counts as
245
+ // supplied and renderer-side defaults (e.g. `@Input() label = 'None'`)
246
+ // are still overwritten with undefined. This only helps when DynamicInput
247
+ // is mounted directly with some inputs left unbound.
206
248
  applyProps(instance) {
207
249
  const instanceObj = instance;
208
250
  for (const prop of KNOWN_PROPS) {
209
- if (prop in this && prop in instanceObj) {
251
+ if (this.supplied.has(prop)) {
210
252
  instanceObj[prop] = this[prop];
211
253
  }
212
254
  }
255
+ this.applyExtraProps(instance);
256
+ }
257
+ applyExtraProps(instance) {
258
+ if (!instance || !this.extraProps) {
259
+ return;
260
+ }
261
+ const instanceObj = instance;
262
+ for (const [key, value] of Object.entries(this.extraProps)) {
263
+ instanceObj[key] = value;
264
+ }
213
265
  }
214
266
  bindOutputs(instance) {
215
267
  const outputNames = [
@@ -235,7 +287,7 @@ class DynamicInput extends BaseInputComponent {
235
287
  this.host.element.nativeElement.appendChild(el);
236
288
  }
237
289
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: DynamicInput, deps: null, target: i0.ɵɵFactoryTarget.Component });
238
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: DynamicInput, isStandalone: true, selector: "dfk-dynamic-input", inputs: { type: "type" }, 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 });
290
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", 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 });
239
291
  }
240
292
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: DynamicInput, decorators: [{
241
293
  type: Component,
@@ -248,6 +300,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
248
300
  }]
249
301
  }], propDecorators: { type: [{
250
302
  type: Input
303
+ }], extraProps: [{
304
+ type: Input
251
305
  }], valueChange: [{
252
306
  type: Output
253
307
  }], onChange: [{
@@ -261,8 +315,24 @@ class FieldInput {
261
315
  cdr;
262
316
  fieldDescription;
263
317
  value;
318
+ options;
319
+ disabled;
320
+ readOnly;
321
+ error;
264
322
  onValueChangeField = new EventEmitter();
265
323
  shouldRender = false;
324
+ get resolvedOptions() {
325
+ if (this.options) {
326
+ return this.options;
327
+ }
328
+ if (!this.fieldDescription?.options) {
329
+ return undefined;
330
+ }
331
+ if (typeof this.fieldDescription.options === 'function') {
332
+ return undefined;
333
+ }
334
+ return this.fieldDescription.options;
335
+ }
266
336
  constructor(cdr) {
267
337
  this.cdr = cdr;
268
338
  }
@@ -270,34 +340,27 @@ class FieldInput {
270
340
  this.shouldRender = !!this.fieldDescription;
271
341
  this.cdr.markForCheck();
272
342
  }
273
- getFieldValue() {
274
- if (!this.fieldDescription) {
275
- return undefined;
276
- }
277
- if (this.value === undefined && this.fieldDescription.type === 'text') {
278
- return '';
279
- }
280
- return this.value;
281
- }
282
343
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: FieldInput, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
283
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: FieldInput, isStandalone: true, selector: "dfk-field-input", inputs: { fieldDescription: "fieldDescription", value: "value" }, outputs: { onValueChangeField: "onValueChangeField" }, usesOnChanges: true, ngImport: i0, template: `
344
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: FieldInput, isStandalone: true, selector: "dfk-field-input", inputs: { fieldDescription: "fieldDescription", value: "value", options: "options", disabled: "disabled", readOnly: "readOnly", error: "error" }, outputs: { onValueChangeField: "onValueChangeField" }, usesOnChanges: true, ngImport: i0, template: `
284
345
  <dfk-dynamic-input
285
346
  *ngIf="shouldRender"
286
347
  [type]="fieldDescription!.type"
287
- [value]="getFieldValue()"
348
+ [value]="value"
288
349
  [label]="fieldDescription!.label"
289
350
  [placeholder]="fieldDescription!.placeholder"
290
351
  [required]="fieldDescription!.required"
291
352
  [description]="$any(fieldDescription!.description)"
292
- [options]="fieldDescription!.options"
353
+ [options]="resolvedOptions"
293
354
  [className]="fieldDescription!.className"
294
355
  (valueChange)="
295
356
  onValueChangeField.emit({ value: $event, key: fieldDescription!.name })
296
357
  "
297
- [disabled]="false"
298
- [errorMessage]="''"
358
+ [disabled]="disabled"
359
+ [readOnly]="readOnly"
360
+ [error]="$any(error)"
361
+ [extraProps]="fieldDescription!.props"
299
362
  ></dfk-dynamic-input>
300
- `, isInline: true, dependencies: [{ kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: DynamicInput, selector: "dfk-dynamic-input", inputs: ["type"], outputs: ["valueChange", "onChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
363
+ `, 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 });
301
364
  }
302
365
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: FieldInput, decorators: [{
303
366
  type: Component,
@@ -310,18 +373,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
310
373
  <dfk-dynamic-input
311
374
  *ngIf="shouldRender"
312
375
  [type]="fieldDescription!.type"
313
- [value]="getFieldValue()"
376
+ [value]="value"
314
377
  [label]="fieldDescription!.label"
315
378
  [placeholder]="fieldDescription!.placeholder"
316
379
  [required]="fieldDescription!.required"
317
380
  [description]="$any(fieldDescription!.description)"
318
- [options]="fieldDescription!.options"
381
+ [options]="resolvedOptions"
319
382
  [className]="fieldDescription!.className"
320
383
  (valueChange)="
321
384
  onValueChangeField.emit({ value: $event, key: fieldDescription!.name })
322
385
  "
323
- [disabled]="false"
324
- [errorMessage]="''"
386
+ [disabled]="disabled"
387
+ [readOnly]="readOnly"
388
+ [error]="$any(error)"
389
+ [extraProps]="fieldDescription!.props"
325
390
  ></dfk-dynamic-input>
326
391
  `,
327
392
  }]
@@ -329,6 +394,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
329
394
  type: Input
330
395
  }], value: [{
331
396
  type: Input
397
+ }], options: [{
398
+ type: Input
399
+ }], disabled: [{
400
+ type: Input
401
+ }], readOnly: [{
402
+ type: Input
403
+ }], error: [{
404
+ type: Input
332
405
  }], onValueChangeField: [{
333
406
  type: Output
334
407
  }] } });
@@ -339,7 +412,12 @@ class MultiFieldInput {
339
412
  fieldDescriptions = [];
340
413
  properties;
341
414
  onChange = new EventEmitter();
415
+ validityChange = new EventEmitter();
342
416
  layout = 'column';
417
+ // Top-level form data, threaded down through repeatable groups so a nested
418
+ // field's appearCondition/computeValue can read the root form. Omitted at
419
+ // the top level, where the form's own data is the root.
420
+ rootData;
343
421
  data = {};
344
422
  visibleFields = [];
345
423
  isMobile = false;
@@ -383,6 +461,18 @@ class MultiFieldInput {
383
461
  trackByIndex(index) {
384
462
  return index;
385
463
  }
464
+ groupTrackByCache = new WeakMap();
465
+ // Returns a stable trackBy per group field: item[keyField] when configured,
466
+ // else the index. Cached by field reference so the template hands Angular the
467
+ // same function each change-detection pass.
468
+ groupTrackBy(field) {
469
+ let fn = this.groupTrackByCache.get(field);
470
+ if (!fn) {
471
+ fn = (index, item) => field.keyField ? item[field.keyField] ?? index : index;
472
+ this.groupTrackByCache.set(field, fn);
473
+ }
474
+ return fn;
475
+ }
386
476
  ngOnInit() {
387
477
  this.updateIsMobile();
388
478
  this.init();
@@ -394,19 +484,25 @@ class MultiFieldInput {
394
484
  const breakpoint = typeof this.layout === 'object' && this.layout.type === 'responsive'
395
485
  ? this.layout.breakpoint ?? DEFAULT_BREAKPOINT
396
486
  : DEFAULT_BREAKPOINT;
397
- this.isMobile =
398
- typeof window !== 'undefined' && window.innerWidth < breakpoint;
487
+ if (typeof window !== 'undefined' &&
488
+ typeof window.matchMedia === 'function') {
489
+ this.isMobile = window.matchMedia(`(max-width: ${breakpoint - 1}px)`).matches;
490
+ }
491
+ else {
492
+ this.isMobile =
493
+ typeof window !== 'undefined' && window.innerWidth < breakpoint;
494
+ }
399
495
  }
400
496
  init() {
401
497
  if (this.properties) {
402
- this.data = applyComputedValues(this.fieldDescriptions, {
403
- ...this.properties,
404
- });
498
+ this.data = applyComputedValues(this.fieldDescriptions, { ...this.properties }, this.rootData);
405
499
  }
406
500
  this.updateVisibleFields();
501
+ this.validityChange.emit(validateFields(this.fieldDescriptions, this.data, this.rootData));
407
502
  }
408
503
  updateVisibleFields() {
409
- this.visibleFields = this.fieldDescriptions.filter((f) => !f.appearCondition || f.appearCondition(this.data));
504
+ const root = this.rootData ?? this.data;
505
+ this.visibleFields = this.fieldDescriptions.filter((f) => !f.appearCondition || f.appearCondition(this.data, root));
410
506
  }
411
507
  onFieldChange(event) {
412
508
  this.commitData({ ...this.data, [event.key]: event.value });
@@ -415,6 +511,22 @@ class MultiFieldInput {
415
511
  const value = this.data[field.name];
416
512
  return Array.isArray(value) ? value : [];
417
513
  }
514
+ getResolvedOptions(field) {
515
+ return resolveOptions(field, this.data, this.rootData);
516
+ }
517
+ getDisabled(field) {
518
+ return resolveDisabled(field, this.data, this.rootData);
519
+ }
520
+ getReadOnly(field) {
521
+ return resolveReadOnly(field, this.data, this.rootData);
522
+ }
523
+ getError(field) {
524
+ if (this.getDisabled(field)) {
525
+ return undefined;
526
+ }
527
+ const errors = validateField(field, this.data[field.name], this.data, this.rootData);
528
+ return errors.length > 0 ? errors : undefined;
529
+ }
418
530
  canAddItem(field) {
419
531
  return canAddGroupItem(field, this.getItems(field));
420
532
  }
@@ -444,13 +556,14 @@ class MultiFieldInput {
444
556
  this.commitData({ ...this.data, [field.name]: items });
445
557
  }
446
558
  commitData(nextData) {
447
- this.data = applyComputedValues(this.fieldDescriptions, nextData);
559
+ this.data = applyComputedValues(this.fieldDescriptions, nextData, this.rootData);
448
560
  this.updateVisibleFields();
449
561
  this.onChange.emit(this.data);
562
+ this.validityChange.emit(validateFields(this.fieldDescriptions, this.data, this.rootData));
450
563
  this.cdr.markForCheck();
451
564
  }
452
565
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: MultiFieldInput, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
453
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: MultiFieldInput, isStandalone: true, selector: "dfk-multi-field-input", inputs: { fieldDescriptions: "fieldDescriptions", properties: "properties", layout: "layout" }, outputs: { onChange: "onChange" }, host: { listeners: { "window:resize": "onWindowResize()" } }, usesOnChanges: true, ngImport: i0, template: `
566
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: MultiFieldInput, isStandalone: true, selector: "dfk-multi-field-input", inputs: { fieldDescriptions: "fieldDescriptions", properties: "properties", layout: "layout", rootData: "rootData" }, outputs: { onChange: "onChange", validityChange: "validityChange" }, host: { listeners: { "window:resize": "onWindowResize()" } }, usesOnChanges: true, ngImport: i0, template: `
454
567
  <div
455
568
  class="p-4 border rounded-lg bg-gray-50"
456
569
  [ngClass]="{
@@ -468,6 +581,10 @@ class MultiFieldInput {
468
581
  *ngIf="!field.fields"
469
582
  [fieldDescription]="field"
470
583
  [value]="data[field.name]"
584
+ [options]="getResolvedOptions(field)"
585
+ [disabled]="getDisabled(field)"
586
+ [readOnly]="getReadOnly(field)"
587
+ [error]="getError(field)"
471
588
  (onValueChangeField)="onFieldChange($event)"
472
589
  ></dfk-field-input>
473
590
 
@@ -477,7 +594,7 @@ class MultiFieldInput {
477
594
  *ngFor="
478
595
  let item of getItems(field);
479
596
  let i = index;
480
- trackBy: trackByIndex
597
+ trackBy: groupTrackBy(field)
481
598
  "
482
599
  style="display: flex; align-items: flex-start; gap: 8px;"
483
600
  >
@@ -485,11 +602,19 @@ class MultiFieldInput {
485
602
  <dfk-multi-field-input
486
603
  [fieldDescriptions]="field.fields"
487
604
  [properties]="item"
605
+ [rootData]="rootData ?? data"
488
606
  (onChange)="onGroupItemChange(field, i, $event)"
489
607
  ></dfk-multi-field-input>
490
608
  </div>
491
609
  <button
492
610
  type="button"
611
+ [attr.aria-label]="
612
+ (field.removeLabel || 'Remove') +
613
+ ' ' +
614
+ (field.label || field.name) +
615
+ ' ' +
616
+ (i + 1)
617
+ "
493
618
  (click)="onGroupItemRemove(field, i)"
494
619
  [disabled]="!canRemoveItem(field)"
495
620
  >
@@ -498,6 +623,9 @@ class MultiFieldInput {
498
623
  </div>
499
624
  <button
500
625
  type="button"
626
+ [attr.aria-label]="
627
+ (field.addLabel || 'Add') + ' ' + (field.label || field.name)
628
+ "
501
629
  (click)="onGroupItemAdd(field)"
502
630
  [disabled]="!canAddItem(field)"
503
631
  >
@@ -506,7 +634,7 @@ class MultiFieldInput {
506
634
  </div>
507
635
  </ng-container>
508
636
  </div>
509
- `, isInline: true, dependencies: [{ kind: "component", type: MultiFieldInput, selector: "dfk-multi-field-input", inputs: ["fieldDescriptions", "properties", "layout"], outputs: ["onChange"] }, { 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"], outputs: ["onValueChangeField"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
637
+ `, isInline: true, dependencies: [{ kind: "component", type: MultiFieldInput, selector: "dfk-multi-field-input", inputs: ["fieldDescriptions", "properties", "layout", "rootData"], outputs: ["onChange", "validityChange"] }, { 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"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
510
638
  }
511
639
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: MultiFieldInput, decorators: [{
512
640
  type: Component,
@@ -539,6 +667,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
539
667
  *ngIf="!field.fields"
540
668
  [fieldDescription]="field"
541
669
  [value]="data[field.name]"
670
+ [options]="getResolvedOptions(field)"
671
+ [disabled]="getDisabled(field)"
672
+ [readOnly]="getReadOnly(field)"
673
+ [error]="getError(field)"
542
674
  (onValueChangeField)="onFieldChange($event)"
543
675
  ></dfk-field-input>
544
676
 
@@ -548,7 +680,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
548
680
  *ngFor="
549
681
  let item of getItems(field);
550
682
  let i = index;
551
- trackBy: trackByIndex
683
+ trackBy: groupTrackBy(field)
552
684
  "
553
685
  style="display: flex; align-items: flex-start; gap: 8px;"
554
686
  >
@@ -556,11 +688,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
556
688
  <dfk-multi-field-input
557
689
  [fieldDescriptions]="field.fields"
558
690
  [properties]="item"
691
+ [rootData]="rootData ?? data"
559
692
  (onChange)="onGroupItemChange(field, i, $event)"
560
693
  ></dfk-multi-field-input>
561
694
  </div>
562
695
  <button
563
696
  type="button"
697
+ [attr.aria-label]="
698
+ (field.removeLabel || 'Remove') +
699
+ ' ' +
700
+ (field.label || field.name) +
701
+ ' ' +
702
+ (i + 1)
703
+ "
564
704
  (click)="onGroupItemRemove(field, i)"
565
705
  [disabled]="!canRemoveItem(field)"
566
706
  >
@@ -569,6 +709,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
569
709
  </div>
570
710
  <button
571
711
  type="button"
712
+ [attr.aria-label]="
713
+ (field.addLabel || 'Add') + ' ' + (field.label || field.name)
714
+ "
572
715
  (click)="onGroupItemAdd(field)"
573
716
  [disabled]="!canAddItem(field)"
574
717
  >
@@ -585,8 +728,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
585
728
  type: Input
586
729
  }], onChange: [{
587
730
  type: Output
731
+ }], validityChange: [{
732
+ type: Output
588
733
  }], layout: [{
589
734
  type: Input
735
+ }], rootData: [{
736
+ type: Input
590
737
  }], onWindowResize: [{
591
738
  type: HostListener,
592
739
  args: ['window:resize']
@@ -767,5 +914,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
767
914
  * Generated bundle index. Do not edit.
768
915
  */
769
916
 
770
- export { BaseInputComponent, ColumnLayout, DynamicFieldKitModule, DynamicInput, FieldInput, GridLayout, LayoutRegistry, MultiFieldInput, RowLayout, layoutRegistry };
917
+ export { BaseInputComponent, ColumnLayout, DynamicFieldKitModule, DynamicInput, FIELD_REGISTRY, FieldInput, GridLayout, LayoutRegistry, MultiFieldInput, RowLayout, layoutRegistry };
771
918
  //# sourceMappingURL=dynamic-field-kit-angular.mjs.map