@dynamic-field-kit/angular 1.3.3 → 1.5.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 (51) hide show
  1. package/CHANGELOG.md +185 -0
  2. package/LICENSE +21 -0
  3. package/README.md +193 -7
  4. package/dist/components/BaseInput.d.ts +5 -11
  5. package/dist/components/DynamicFormDevTools.d.ts +16 -0
  6. package/dist/components/DynamicInput.d.ts +8 -3
  7. package/dist/components/FieldInput.d.ts +12 -2
  8. package/dist/components/MultiFieldInput.d.ts +22 -2
  9. package/dist/fesm2022/dynamic-field-kit-angular.mjs +778 -87
  10. package/dist/fieldRegistryToken.d.ts +3 -0
  11. package/dist/index.d.ts +1 -1
  12. package/dist/layout/index.d.ts +2 -2
  13. package/dist/lib/dynamic-field-kit.module.d.ts +4 -4
  14. package/dist/lib/dynamic-form.store.d.ts +23 -0
  15. package/dist/public-api.d.ts +13 -8
  16. package/dist/types/layout.d.ts +4 -25
  17. package/package.json +46 -29
  18. package/angular.json +0 -27
  19. package/dist/README.md +0 -188
  20. package/dist/esm2022/components/BaseInput.mjs +0 -68
  21. package/dist/esm2022/components/DynamicInput.mjs +0 -191
  22. package/dist/esm2022/components/FieldInput.mjs +0 -80
  23. package/dist/esm2022/components/MultiFieldInput.mjs +0 -264
  24. package/dist/esm2022/dynamic-field-kit-angular.mjs +0 -5
  25. package/dist/esm2022/layout/defaultLayouts.mjs +0 -114
  26. package/dist/esm2022/layout/index.mjs +0 -3
  27. package/dist/esm2022/layout/layoutRegistry.mjs +0 -14
  28. package/dist/esm2022/lib/dynamic-field-kit.module.mjs +0 -52
  29. package/dist/esm2022/public-api.mjs +0 -14
  30. package/dist/esm2022/types/layout.mjs +0 -2
  31. package/dist/fesm2022/dynamic-field-kit-angular.mjs.map +0 -1
  32. package/karma.conf.js +0 -37
  33. package/ng-package.json +0 -7
  34. package/src/components/BaseInput.ts +0 -60
  35. package/src/components/DynamicInput.ts +0 -224
  36. package/src/components/FieldInput.ts +0 -67
  37. package/src/components/MultiFieldInput.ts +0 -238
  38. package/src/layout/defaultLayouts.ts +0 -70
  39. package/src/layout/index.ts +0 -2
  40. package/src/layout/layoutRegistry.ts +0 -25
  41. package/src/lib/dynamic-field-kit.module.ts +0 -29
  42. package/src/public-api.ts +0 -24
  43. package/src/types/layout.ts +0 -36
  44. package/test/DynamicInput.spec.ts +0 -30
  45. package/test/FieldInput.spec.ts +0 -44
  46. package/test/angular.spec.ts +0 -102
  47. package/test/integration.spec.ts +0 -57
  48. package/test/layouts.spec.ts +0 -26
  49. package/test.ts +0 -12
  50. package/tsconfig.json +0 -13
  51. package/tsconfig.spec.json +0 -21
@@ -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, signal, computed, NgModule } from '@angular/core';
3
3
  import * as i1 from '@angular/common';
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';
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';
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;
@@ -33,10 +30,10 @@ class BaseInputComponent {
33
30
  hasChanges(changes, prop) {
34
31
  return changes[prop] !== undefined;
35
32
  }
36
- 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 });
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 });
38
35
  }
39
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: BaseInputComponent, decorators: [{
36
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: BaseInputComponent, decorators: [{
40
37
  type: Component,
41
38
  args: [{
42
39
  template: '',
@@ -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,10 +151,13 @@ 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) {
158
+ if (this.renderDefaultFallbackHTML5(this.type)) {
159
+ return;
160
+ }
144
161
  this.renderError(`Unknown field type: ${this.type}`);
145
162
  return;
146
163
  }
@@ -154,8 +171,16 @@ class DynamicInput extends BaseInputComponent {
154
171
  this.renderError(`Invalid renderer for type: ${this.type}`);
155
172
  }
156
173
  }
174
+ // Angular component classes carry a static ɵcmp. Checked instead of
175
+ // reflectComponentType() because the peer range starts at Angular 13 and
176
+ // reflectComponentType is v14+. Plain function renderers have no ɵcmp and
177
+ // fall through to renderFallback. Uses hasOwnProperty (not `in`) so a
178
+ // class that merely extends a component without its own @Component
179
+ // decorator - and would otherwise inherit the parent's static ɵcmp via the
180
+ // prototype chain - is not mistaken for a component in its own right.
157
181
  isComponentType(renderer) {
158
- return (typeof renderer === 'object' && renderer !== null && 'cmp' in renderer);
182
+ return (typeof renderer === 'function' &&
183
+ Object.prototype.hasOwnProperty.call(renderer, 'ɵcmp'));
159
184
  }
160
185
  renderComponent(compType) {
161
186
  try {
@@ -191,25 +216,55 @@ class DynamicInput extends BaseInputComponent {
191
216
  }
192
217
  getFallbackProps() {
193
218
  return {
219
+ ...this.extraProps,
194
220
  value: this.value,
195
221
  onValueChange: (v) => this.emitValue(v),
196
222
  label: this.label ?? '',
197
223
  placeholder: this.placeholder ?? '',
198
224
  required: this.required ?? false,
225
+ disabled: this.disabled ?? false,
226
+ readOnly: this.readOnly ?? false,
227
+ error: this.error,
199
228
  options: this.options ?? [],
200
229
  className: this.className ?? '',
201
230
  description: this.description ?? '',
202
- disabled: this.disabled ?? false,
203
- errorMessage: this.errorMessage ?? '',
204
231
  };
205
232
  }
233
+ // Gates on `this.supplied` (populated from SimpleChanges in ngOnChanges),
234
+ // not on `prop in instanceObj` or `prop in this`. Both `in` checks test a
235
+ // TypeScript class-field emit artifact rather than intent: whether a
236
+ // property slot exists at runtime depends on the compile target
237
+ // (useDefineForClassFields is false below ES2022, true at ES2022+), so the
238
+ // same source can behave oppositely between this package's test build and
239
+ // its shipped ES2022 bundle, where every declared field is always an own
240
+ // property. `this.supplied` instead reflects only whether Angular ever
241
+ // fired a SimpleChange for that input, i.e. whether the prop was *bound*
242
+ // at all - it skips only props DynamicInput was never given a binding
243
+ // for. It is NOT a general "renderer defaults survive" guarantee: Angular
244
+ // fires a firstChange SimpleChange for every bound input even when the
245
+ // bound value is undefined, and FieldInput's template binds all
246
+ // KNOWN_PROPS unconditionally, so on the real
247
+ // MultiFieldInput -> FieldInput -> DynamicInput path every prop counts as
248
+ // supplied and renderer-side defaults (e.g. `@Input() label = 'None'`)
249
+ // are still overwritten with undefined. This only helps when DynamicInput
250
+ // is mounted directly with some inputs left unbound.
206
251
  applyProps(instance) {
207
252
  const instanceObj = instance;
208
253
  for (const prop of KNOWN_PROPS) {
209
- if (prop in this && prop in instanceObj) {
254
+ if (this.supplied.has(prop)) {
210
255
  instanceObj[prop] = this[prop];
211
256
  }
212
257
  }
258
+ this.applyExtraProps(instance);
259
+ }
260
+ applyExtraProps(instance) {
261
+ if (!instance || !this.extraProps) {
262
+ return;
263
+ }
264
+ const instanceObj = instance;
265
+ for (const [key, value] of Object.entries(this.extraProps)) {
266
+ instanceObj[key] = value;
267
+ }
213
268
  }
214
269
  bindOutputs(instance) {
215
270
  const outputNames = [
@@ -228,16 +283,128 @@ class DynamicInput extends BaseInputComponent {
228
283
  this.valueChange.emit(value);
229
284
  this.onChange.emit(value);
230
285
  }
286
+ renderDefaultFallbackHTML5(type) {
287
+ const nativeEl = this.host.element.nativeElement;
288
+ if (type === 'text' ||
289
+ type === 'password' ||
290
+ type === 'email' ||
291
+ type === 'number') {
292
+ const input = document.createElement('input');
293
+ input.type = type;
294
+ if (this.className) {
295
+ input.className = this.className;
296
+ }
297
+ if (this.placeholder) {
298
+ input.placeholder = this.placeholder;
299
+ }
300
+ if (this.disabled) {
301
+ input.disabled = true;
302
+ }
303
+ if (this.readOnly) {
304
+ input.readOnly = true;
305
+ }
306
+ if (this.required) {
307
+ input.required = true;
308
+ }
309
+ input.value = (this.value ?? '').toString();
310
+ input.addEventListener('input', (e) => {
311
+ const raw = e.target.value;
312
+ const val = type === 'number' ? (raw === '' ? undefined : Number(raw)) : raw;
313
+ this.emitValue(val);
314
+ });
315
+ nativeEl.appendChild(input);
316
+ return true;
317
+ }
318
+ if (type === 'textarea') {
319
+ const textarea = document.createElement('textarea');
320
+ if (this.className) {
321
+ textarea.className = this.className;
322
+ }
323
+ if (this.placeholder) {
324
+ textarea.placeholder = this.placeholder;
325
+ }
326
+ if (this.disabled) {
327
+ textarea.disabled = true;
328
+ }
329
+ if (this.readOnly) {
330
+ textarea.readOnly = true;
331
+ }
332
+ if (this.required) {
333
+ textarea.required = true;
334
+ }
335
+ textarea.value = this.value ?? '';
336
+ textarea.addEventListener('input', (e) => {
337
+ this.emitValue(e.target.value);
338
+ });
339
+ nativeEl.appendChild(textarea);
340
+ return true;
341
+ }
342
+ if (type === 'checkbox') {
343
+ const checkbox = document.createElement('input');
344
+ checkbox.type = 'checkbox';
345
+ if (this.className) {
346
+ checkbox.className = this.className;
347
+ }
348
+ if (this.disabled || this.readOnly) {
349
+ checkbox.disabled = true;
350
+ }
351
+ if (this.required) {
352
+ checkbox.required = true;
353
+ }
354
+ checkbox.checked = Boolean(this.value);
355
+ checkbox.addEventListener('change', (e) => {
356
+ this.emitValue(e.target.checked);
357
+ });
358
+ nativeEl.appendChild(checkbox);
359
+ return true;
360
+ }
361
+ if (type === 'select') {
362
+ const select = document.createElement('select');
363
+ if (this.className) {
364
+ select.className = this.className;
365
+ }
366
+ if (this.disabled || this.readOnly) {
367
+ select.disabled = true;
368
+ }
369
+ if (this.required) {
370
+ select.required = true;
371
+ }
372
+ const defaultOpt = document.createElement('option');
373
+ defaultOpt.value = '';
374
+ defaultOpt.disabled = true;
375
+ defaultOpt.textContent = '-- Select --';
376
+ select.appendChild(defaultOpt);
377
+ const options = this.options || [];
378
+ options.forEach((opt) => {
379
+ const optObj = opt;
380
+ const optVal = optObj['value'] ?? optObj['id'] ?? opt;
381
+ const optLabel = optObj['label'] ?? optObj['name'] ?? String(optVal);
382
+ const optionEl = document.createElement('option');
383
+ optionEl.value = String(optVal);
384
+ optionEl.textContent = String(optLabel);
385
+ if (String(optVal) === String(this.value)) {
386
+ optionEl.selected = true;
387
+ }
388
+ select.appendChild(optionEl);
389
+ });
390
+ select.addEventListener('change', (e) => {
391
+ this.emitValue(e.target.value);
392
+ });
393
+ nativeEl.appendChild(select);
394
+ return true;
395
+ }
396
+ return false;
397
+ }
231
398
  renderError(message) {
232
399
  const el = document.createElement('div');
233
400
  el.textContent = message;
234
401
  el.style.color = 'red';
235
402
  this.host.element.nativeElement.appendChild(el);
236
403
  }
237
- 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 });
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 });
239
406
  }
240
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: DynamicInput, decorators: [{
407
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: DynamicInput, decorators: [{
241
408
  type: Component,
242
409
  args: [{
243
410
  selector: 'dfk-dynamic-input',
@@ -248,6 +415,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
248
415
  }]
249
416
  }], propDecorators: { type: [{
250
417
  type: Input
418
+ }], extraProps: [{
419
+ type: Input
251
420
  }], valueChange: [{
252
421
  type: Output
253
422
  }], onChange: [{
@@ -261,8 +430,30 @@ class FieldInput {
261
430
  cdr;
262
431
  fieldDescription;
263
432
  value;
433
+ options;
434
+ disabled;
435
+ readOnly;
436
+ error;
264
437
  onValueChangeField = new EventEmitter();
438
+ /**
439
+ * Emits this field's name when focus leaves it. Driven by `focusout`, which
440
+ * bubbles, so it works for any renderer without the renderer having to
441
+ * declare a blur output of its own.
442
+ */
443
+ onBlurField = new EventEmitter();
265
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
+ }
266
457
  constructor(cdr) {
267
458
  this.cdr = cdr;
268
459
  }
@@ -270,36 +461,30 @@ class FieldInput {
270
461
  this.shouldRender = !!this.fieldDescription;
271
462
  this.cdr.markForCheck();
272
463
  }
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
- 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: `
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: `
284
466
  <dfk-dynamic-input
285
467
  *ngIf="shouldRender"
286
468
  [type]="fieldDescription!.type"
287
- [value]="getFieldValue()"
469
+ [value]="value"
288
470
  [label]="fieldDescription!.label"
289
471
  [placeholder]="fieldDescription!.placeholder"
290
472
  [required]="fieldDescription!.required"
291
473
  [description]="$any(fieldDescription!.description)"
292
- [options]="fieldDescription!.options"
474
+ [options]="resolvedOptions"
293
475
  [className]="fieldDescription!.className"
294
476
  (valueChange)="
295
477
  onValueChangeField.emit({ value: $event, key: fieldDescription!.name })
296
478
  "
297
- [disabled]="false"
298
- [errorMessage]="''"
479
+ (focusout)="onBlurField.emit(fieldDescription!.name)"
480
+ [disabled]="disabled"
481
+ [readOnly]="readOnly"
482
+ [error]="$any(error)"
483
+ [extraProps]="fieldDescription!.props"
299
484
  ></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 });
485
+ `, 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
486
  }
302
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: FieldInput, decorators: [{
487
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: FieldInput, decorators: [{
303
488
  type: Component,
304
489
  args: [{
305
490
  selector: 'dfk-field-input',
@@ -310,18 +495,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
310
495
  <dfk-dynamic-input
311
496
  *ngIf="shouldRender"
312
497
  [type]="fieldDescription!.type"
313
- [value]="getFieldValue()"
498
+ [value]="value"
314
499
  [label]="fieldDescription!.label"
315
500
  [placeholder]="fieldDescription!.placeholder"
316
501
  [required]="fieldDescription!.required"
317
502
  [description]="$any(fieldDescription!.description)"
318
- [options]="fieldDescription!.options"
503
+ [options]="resolvedOptions"
319
504
  [className]="fieldDescription!.className"
320
505
  (valueChange)="
321
506
  onValueChangeField.emit({ value: $event, key: fieldDescription!.name })
322
507
  "
323
- [disabled]="false"
324
- [errorMessage]="''"
508
+ (focusout)="onBlurField.emit(fieldDescription!.name)"
509
+ [disabled]="disabled"
510
+ [readOnly]="readOnly"
511
+ [error]="$any(error)"
512
+ [extraProps]="fieldDescription!.props"
325
513
  ></dfk-dynamic-input>
326
514
  `,
327
515
  }]
@@ -329,8 +517,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
329
517
  type: Input
330
518
  }], value: [{
331
519
  type: Input
520
+ }], options: [{
521
+ type: Input
522
+ }], disabled: [{
523
+ type: Input
524
+ }], readOnly: [{
525
+ type: Input
526
+ }], error: [{
527
+ type: Input
332
528
  }], onValueChangeField: [{
333
529
  type: Output
530
+ }], onBlurField: [{
531
+ type: Output
334
532
  }] } });
335
533
 
336
534
  const DEFAULT_BREAKPOINT = 768;
@@ -339,7 +537,28 @@ class MultiFieldInput {
339
537
  fieldDescriptions = [];
340
538
  properties;
341
539
  onChange = new EventEmitter();
540
+ validityChange = new EventEmitter();
541
+ /**
542
+ * Emits a field's name when it loses focus. Touched state is tracked
543
+ * internally either way; this is the hook for driving an external form store
544
+ * - pass `createDynamicFormStore`'s `handleBlur` to get its `touched` map and
545
+ * `validateOnBlur` behaviour.
546
+ */
547
+ onBlurField = new EventEmitter();
548
+ touchedFields = {};
549
+ handleBlurField(fieldName) {
550
+ this.touchedFields = { ...this.touchedFields, [fieldName]: true };
551
+ this.onBlurField.emit(fieldName);
552
+ }
553
+ /** Whether this field has been blurred at least once. */
554
+ isTouched(fieldName) {
555
+ return Boolean(this.touchedFields[fieldName]);
556
+ }
342
557
  layout = 'column';
558
+ // Top-level form data, threaded down through repeatable groups so a nested
559
+ // field's appearCondition/computeValue can read the root form. Omitted at
560
+ // the top level, where the form's own data is the root.
561
+ rootData;
343
562
  data = {};
344
563
  visibleFields = [];
345
564
  isMobile = false;
@@ -383,6 +602,18 @@ class MultiFieldInput {
383
602
  trackByIndex(index) {
384
603
  return index;
385
604
  }
605
+ groupTrackByCache = new WeakMap();
606
+ // Returns a stable trackBy per group field: item[keyField] when configured,
607
+ // else the index. Cached by field reference so the template hands Angular the
608
+ // same function each change-detection pass.
609
+ groupTrackBy(field) {
610
+ let fn = this.groupTrackByCache.get(field);
611
+ if (!fn) {
612
+ fn = (index, item) => field.keyField ? (item[field.keyField] ?? index) : index;
613
+ this.groupTrackByCache.set(field, fn);
614
+ }
615
+ return fn;
616
+ }
386
617
  ngOnInit() {
387
618
  this.updateIsMobile();
388
619
  this.init();
@@ -392,21 +623,27 @@ class MultiFieldInput {
392
623
  }
393
624
  updateIsMobile() {
394
625
  const breakpoint = typeof this.layout === 'object' && this.layout.type === 'responsive'
395
- ? this.layout.breakpoint ?? DEFAULT_BREAKPOINT
626
+ ? (this.layout.breakpoint ?? DEFAULT_BREAKPOINT)
396
627
  : DEFAULT_BREAKPOINT;
397
- this.isMobile =
398
- typeof window !== 'undefined' && window.innerWidth < breakpoint;
628
+ if (typeof window !== 'undefined' &&
629
+ typeof window.matchMedia === 'function') {
630
+ this.isMobile = window.matchMedia(`(max-width: ${breakpoint - 1}px)`).matches;
631
+ }
632
+ else {
633
+ this.isMobile =
634
+ typeof window !== 'undefined' && window.innerWidth < breakpoint;
635
+ }
399
636
  }
400
637
  init() {
401
638
  if (this.properties) {
402
- this.data = applyComputedValues(this.fieldDescriptions, {
403
- ...this.properties,
404
- });
639
+ this.data = applyComputedValues(this.fieldDescriptions, { ...this.properties }, this.rootData);
405
640
  }
406
641
  this.updateVisibleFields();
642
+ this.validityChange.emit(validateFields(this.fieldDescriptions, this.data, this.rootData));
407
643
  }
408
644
  updateVisibleFields() {
409
- this.visibleFields = this.fieldDescriptions.filter((f) => !f.appearCondition || f.appearCondition(this.data));
645
+ const root = this.rootData ?? this.data;
646
+ this.visibleFields = this.fieldDescriptions.filter((f) => !f.appearCondition || f.appearCondition(this.data, root));
410
647
  }
411
648
  onFieldChange(event) {
412
649
  this.commitData({ ...this.data, [event.key]: event.value });
@@ -415,6 +652,22 @@ class MultiFieldInput {
415
652
  const value = this.data[field.name];
416
653
  return Array.isArray(value) ? value : [];
417
654
  }
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
+ }
418
671
  canAddItem(field) {
419
672
  return canAddGroupItem(field, this.getItems(field));
420
673
  }
@@ -444,19 +697,20 @@ class MultiFieldInput {
444
697
  this.commitData({ ...this.data, [field.name]: items });
445
698
  }
446
699
  commitData(nextData) {
447
- this.data = applyComputedValues(this.fieldDescriptions, nextData);
700
+ this.data = applyComputedValues(this.fieldDescriptions, nextData, this.rootData);
448
701
  this.updateVisibleFields();
449
702
  this.onChange.emit(this.data);
703
+ this.validityChange.emit(validateFields(this.fieldDescriptions, this.data, this.rootData));
450
704
  this.cdr.markForCheck();
451
705
  }
452
- 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: `
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: `
454
708
  <div
455
709
  class="p-4 border rounded-lg bg-gray-50"
456
710
  [ngClass]="{
457
711
  'flex flex-col': resolvedLayoutType === 'column',
458
712
  'flex flex-row': resolvedLayoutType === 'row',
459
- grid: resolvedLayoutType === 'grid'
713
+ grid: resolvedLayoutType === 'grid',
460
714
  }"
461
715
  [style.gap.px]="gap"
462
716
  [style.gridTemplateColumns]="
@@ -468,7 +722,12 @@ class MultiFieldInput {
468
722
  *ngIf="!field.fields"
469
723
  [fieldDescription]="field"
470
724
  [value]="data[field.name]"
725
+ [options]="getResolvedOptions(field)"
726
+ [disabled]="getDisabled(field)"
727
+ [readOnly]="getReadOnly(field)"
728
+ [error]="getError(field)"
471
729
  (onValueChangeField)="onFieldChange($event)"
730
+ (onBlurField)="handleBlurField($event)"
472
731
  ></dfk-field-input>
473
732
 
474
733
  <div *ngIf="field.fields" [class]="field.className">
@@ -477,7 +736,7 @@ class MultiFieldInput {
477
736
  *ngFor="
478
737
  let item of getItems(field);
479
738
  let i = index;
480
- trackBy: trackByIndex
739
+ trackBy: groupTrackBy(field)
481
740
  "
482
741
  style="display: flex; align-items: flex-start; gap: 8px;"
483
742
  >
@@ -485,11 +744,19 @@ class MultiFieldInput {
485
744
  <dfk-multi-field-input
486
745
  [fieldDescriptions]="field.fields"
487
746
  [properties]="item"
747
+ [rootData]="rootData ?? data"
488
748
  (onChange)="onGroupItemChange(field, i, $event)"
489
749
  ></dfk-multi-field-input>
490
750
  </div>
491
751
  <button
492
752
  type="button"
753
+ [attr.aria-label]="
754
+ (field.removeLabel || 'Remove') +
755
+ ' ' +
756
+ (field.label || field.name) +
757
+ ' ' +
758
+ (i + 1)
759
+ "
493
760
  (click)="onGroupItemRemove(field, i)"
494
761
  [disabled]="!canRemoveItem(field)"
495
762
  >
@@ -498,6 +765,9 @@ class MultiFieldInput {
498
765
  </div>
499
766
  <button
500
767
  type="button"
768
+ [attr.aria-label]="
769
+ (field.addLabel || 'Add') + ' ' + (field.label || field.name)
770
+ "
501
771
  (click)="onGroupItemAdd(field)"
502
772
  [disabled]="!canAddItem(field)"
503
773
  >
@@ -506,9 +776,9 @@ class MultiFieldInput {
506
776
  </div>
507
777
  </ng-container>
508
778
  </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 });
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 });
510
780
  }
511
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: MultiFieldInput, decorators: [{
781
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: MultiFieldInput, decorators: [{
512
782
  type: Component,
513
783
  args: [{
514
784
  selector: 'dfk-multi-field-input',
@@ -527,7 +797,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
527
797
  [ngClass]="{
528
798
  'flex flex-col': resolvedLayoutType === 'column',
529
799
  'flex flex-row': resolvedLayoutType === 'row',
530
- grid: resolvedLayoutType === 'grid'
800
+ grid: resolvedLayoutType === 'grid',
531
801
  }"
532
802
  [style.gap.px]="gap"
533
803
  [style.gridTemplateColumns]="
@@ -539,7 +809,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
539
809
  *ngIf="!field.fields"
540
810
  [fieldDescription]="field"
541
811
  [value]="data[field.name]"
812
+ [options]="getResolvedOptions(field)"
813
+ [disabled]="getDisabled(field)"
814
+ [readOnly]="getReadOnly(field)"
815
+ [error]="getError(field)"
542
816
  (onValueChangeField)="onFieldChange($event)"
817
+ (onBlurField)="handleBlurField($event)"
543
818
  ></dfk-field-input>
544
819
 
545
820
  <div *ngIf="field.fields" [class]="field.className">
@@ -548,7 +823,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
548
823
  *ngFor="
549
824
  let item of getItems(field);
550
825
  let i = index;
551
- trackBy: trackByIndex
826
+ trackBy: groupTrackBy(field)
552
827
  "
553
828
  style="display: flex; align-items: flex-start; gap: 8px;"
554
829
  >
@@ -556,11 +831,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
556
831
  <dfk-multi-field-input
557
832
  [fieldDescriptions]="field.fields"
558
833
  [properties]="item"
834
+ [rootData]="rootData ?? data"
559
835
  (onChange)="onGroupItemChange(field, i, $event)"
560
836
  ></dfk-multi-field-input>
561
837
  </div>
562
838
  <button
563
839
  type="button"
840
+ [attr.aria-label]="
841
+ (field.removeLabel || 'Remove') +
842
+ ' ' +
843
+ (field.label || field.name) +
844
+ ' ' +
845
+ (i + 1)
846
+ "
564
847
  (click)="onGroupItemRemove(field, i)"
565
848
  [disabled]="!canRemoveItem(field)"
566
849
  >
@@ -569,6 +852,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
569
852
  </div>
570
853
  <button
571
854
  type="button"
855
+ [attr.aria-label]="
856
+ (field.addLabel || 'Add') + ' ' + (field.label || field.name)
857
+ "
572
858
  (click)="onGroupItemAdd(field)"
573
859
  [disabled]="!canAddItem(field)"
574
860
  >
@@ -585,13 +871,419 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
585
871
  type: Input
586
872
  }], onChange: [{
587
873
  type: Output
874
+ }], validityChange: [{
875
+ type: Output
876
+ }], onBlurField: [{
877
+ type: Output
588
878
  }], layout: [{
589
879
  type: Input
880
+ }], rootData: [{
881
+ type: Input
590
882
  }], onWindowResize: [{
591
883
  type: HostListener,
592
884
  args: ['window:resize']
593
885
  }] } });
594
886
 
887
+ class DynamicFormDevToolsComponent {
888
+ data = {};
889
+ errors = {};
890
+ touched = {};
891
+ isDirty = false;
892
+ fields = [];
893
+ /** Number of fields carrying errors, mirroring the react and vue overlays. */
894
+ errorCount() {
895
+ return Object.keys(this.errors || {}).length;
896
+ }
897
+ isOpen = signal(false);
898
+ activeTab = signal('data');
899
+ tabs = [
900
+ 'data',
901
+ 'errors',
902
+ 'meta',
903
+ 'fields',
904
+ ];
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: `
907
+ <button
908
+ *ngIf="!isOpen()"
909
+ type="button"
910
+ (click)="isOpen.set(true)"
911
+ style="
912
+ position: fixed;
913
+ right: 16px;
914
+ bottom: 16px;
915
+ z-index: 99999;
916
+ background: #1e293b;
917
+ color: #f8fafc;
918
+ border: 1px solid #334155;
919
+ border-radius: 20px;
920
+ padding: 8px 14px;
921
+ font-size: 12px;
922
+ font-weight: 600;
923
+ cursor: pointer;
924
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
925
+ display: flex;
926
+ align-items: center;
927
+ gap: 6px;
928
+ "
929
+ >
930
+ <span>🔍 DevTools</span>
931
+ <span
932
+ *ngIf="errorCount() > 0"
933
+ class="dfk-devtools-badge"
934
+ style="
935
+ background: #ef4444;
936
+ color: #fff;
937
+ border-radius: 10px;
938
+ padding: 2px 6px;
939
+ font-size: 10px;
940
+ "
941
+ >{{ errorCount() }}</span
942
+ >
943
+ </button>
944
+
945
+ <div
946
+ *ngIf="isOpen()"
947
+ style="
948
+ position: fixed;
949
+ right: 16px;
950
+ bottom: 16px;
951
+ z-index: 99999;
952
+ width: 360px;
953
+ max-height: 420px;
954
+ background: #0f172a;
955
+ color: #f8fafc;
956
+ border: 1px solid #334155;
957
+ border-radius: 12px;
958
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
959
+ display: flex;
960
+ flex-direction: column;
961
+ font-family: monospace, sans-serif;
962
+ font-size: 12px;
963
+ overflow: hidden;
964
+ "
965
+ >
966
+ <div
967
+ style="
968
+ padding: 10px 14px;
969
+ background: #1e293b;
970
+ display: flex;
971
+ justify-content: space-between;
972
+ align-items: center;
973
+ border-bottom: 1px solid #334155;
974
+ "
975
+ >
976
+ <span style="font-weight: bold; color: #38bdf8;">🛠️ Form DevTools</span>
977
+ <button
978
+ type="button"
979
+ (click)="isOpen.set(false)"
980
+ style="background: transparent; border: none; color: #94a3b8; font-size: 14px; cursor: pointer;"
981
+ >
982
+
983
+ </button>
984
+ </div>
985
+
986
+ <div
987
+ style="display: flex; background: #1e293b; border-bottom: 1px solid #334155;"
988
+ >
989
+ <button
990
+ type="button"
991
+ *ngFor="let tab of tabs"
992
+ (click)="activeTab.set(tab)"
993
+ [style.background]="activeTab() === tab ? '#0f172a' : 'transparent'"
994
+ [style.color]="activeTab() === tab ? '#38bdf8' : '#94a3b8'"
995
+ style="flex: 1; padding: 6px 0; border: none; cursor: pointer; text-transform: capitalize; font-size: 11px;"
996
+ >
997
+ {{ tab
998
+ }}{{
999
+ tab === 'errors' && errorCount() > 0
1000
+ ? ' (' + errorCount() + ')'
1001
+ : ''
1002
+ }}
1003
+ </button>
1004
+ </div>
1005
+
1006
+ <div style="padding: 12px; overflow-y: auto; flex: 1;">
1007
+ <pre
1008
+ *ngIf="activeTab() === 'data'"
1009
+ style="margin: 0; white-space: pre-wrap; word-break: break-all; color: #a7f3d0;"
1010
+ >
1011
+ {{ data | json }}
1012
+ </pre>
1013
+ <pre
1014
+ *ngIf="activeTab() === 'errors'"
1015
+ style="margin: 0; white-space: pre-wrap; word-break: break-all; color: #f87171;"
1016
+ >
1017
+ {{ errors | json }}
1018
+ </pre>
1019
+ <div *ngIf="activeTab() === 'meta'">
1020
+ <div>isDirty: {{ isDirty }}</div>
1021
+ <pre style="margin: 4px 0 0 0; color: #cbd5e1;">{{
1022
+ touched | json
1023
+ }}</pre>
1024
+ </div>
1025
+ <div *ngIf="activeTab() === 'fields'">
1026
+ <div
1027
+ *ngFor="let f of fields"
1028
+ style="padding: 6px; margin-bottom: 6px; background: #1e293b; border-radius: 4px;"
1029
+ >
1030
+ <div style="color: #38bdf8; font-weight: bold;">{{ f.name }}</div>
1031
+ <div style="color: #94a3b8; font-size: 10px;">
1032
+ type: {{ f.type }}
1033
+ </div>
1034
+ </div>
1035
+ </div>
1036
+ </div>
1037
+ </div>
1038
+ `, 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
+ }
1040
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: DynamicFormDevToolsComponent, decorators: [{
1041
+ type: Component,
1042
+ args: [{
1043
+ selector: 'dfk-dev-tools',
1044
+ standalone: true,
1045
+ imports: [NgIf, NgFor, JsonPipe],
1046
+ changeDetection: ChangeDetectionStrategy.OnPush,
1047
+ template: `
1048
+ <button
1049
+ *ngIf="!isOpen()"
1050
+ type="button"
1051
+ (click)="isOpen.set(true)"
1052
+ style="
1053
+ position: fixed;
1054
+ right: 16px;
1055
+ bottom: 16px;
1056
+ z-index: 99999;
1057
+ background: #1e293b;
1058
+ color: #f8fafc;
1059
+ border: 1px solid #334155;
1060
+ border-radius: 20px;
1061
+ padding: 8px 14px;
1062
+ font-size: 12px;
1063
+ font-weight: 600;
1064
+ cursor: pointer;
1065
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
1066
+ display: flex;
1067
+ align-items: center;
1068
+ gap: 6px;
1069
+ "
1070
+ >
1071
+ <span>🔍 DevTools</span>
1072
+ <span
1073
+ *ngIf="errorCount() > 0"
1074
+ class="dfk-devtools-badge"
1075
+ style="
1076
+ background: #ef4444;
1077
+ color: #fff;
1078
+ border-radius: 10px;
1079
+ padding: 2px 6px;
1080
+ font-size: 10px;
1081
+ "
1082
+ >{{ errorCount() }}</span
1083
+ >
1084
+ </button>
1085
+
1086
+ <div
1087
+ *ngIf="isOpen()"
1088
+ style="
1089
+ position: fixed;
1090
+ right: 16px;
1091
+ bottom: 16px;
1092
+ z-index: 99999;
1093
+ width: 360px;
1094
+ max-height: 420px;
1095
+ background: #0f172a;
1096
+ color: #f8fafc;
1097
+ border: 1px solid #334155;
1098
+ border-radius: 12px;
1099
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
1100
+ display: flex;
1101
+ flex-direction: column;
1102
+ font-family: monospace, sans-serif;
1103
+ font-size: 12px;
1104
+ overflow: hidden;
1105
+ "
1106
+ >
1107
+ <div
1108
+ style="
1109
+ padding: 10px 14px;
1110
+ background: #1e293b;
1111
+ display: flex;
1112
+ justify-content: space-between;
1113
+ align-items: center;
1114
+ border-bottom: 1px solid #334155;
1115
+ "
1116
+ >
1117
+ <span style="font-weight: bold; color: #38bdf8;">🛠️ Form DevTools</span>
1118
+ <button
1119
+ type="button"
1120
+ (click)="isOpen.set(false)"
1121
+ style="background: transparent; border: none; color: #94a3b8; font-size: 14px; cursor: pointer;"
1122
+ >
1123
+
1124
+ </button>
1125
+ </div>
1126
+
1127
+ <div
1128
+ style="display: flex; background: #1e293b; border-bottom: 1px solid #334155;"
1129
+ >
1130
+ <button
1131
+ type="button"
1132
+ *ngFor="let tab of tabs"
1133
+ (click)="activeTab.set(tab)"
1134
+ [style.background]="activeTab() === tab ? '#0f172a' : 'transparent'"
1135
+ [style.color]="activeTab() === tab ? '#38bdf8' : '#94a3b8'"
1136
+ style="flex: 1; padding: 6px 0; border: none; cursor: pointer; text-transform: capitalize; font-size: 11px;"
1137
+ >
1138
+ {{ tab
1139
+ }}{{
1140
+ tab === 'errors' && errorCount() > 0
1141
+ ? ' (' + errorCount() + ')'
1142
+ : ''
1143
+ }}
1144
+ </button>
1145
+ </div>
1146
+
1147
+ <div style="padding: 12px; overflow-y: auto; flex: 1;">
1148
+ <pre
1149
+ *ngIf="activeTab() === 'data'"
1150
+ style="margin: 0; white-space: pre-wrap; word-break: break-all; color: #a7f3d0;"
1151
+ >
1152
+ {{ data | json }}
1153
+ </pre>
1154
+ <pre
1155
+ *ngIf="activeTab() === 'errors'"
1156
+ style="margin: 0; white-space: pre-wrap; word-break: break-all; color: #f87171;"
1157
+ >
1158
+ {{ errors | json }}
1159
+ </pre>
1160
+ <div *ngIf="activeTab() === 'meta'">
1161
+ <div>isDirty: {{ isDirty }}</div>
1162
+ <pre style="margin: 4px 0 0 0; color: #cbd5e1;">{{
1163
+ touched | json
1164
+ }}</pre>
1165
+ </div>
1166
+ <div *ngIf="activeTab() === 'fields'">
1167
+ <div
1168
+ *ngFor="let f of fields"
1169
+ style="padding: 6px; margin-bottom: 6px; background: #1e293b; border-radius: 4px;"
1170
+ >
1171
+ <div style="color: #38bdf8; font-weight: bold;">{{ f.name }}</div>
1172
+ <div style="color: #94a3b8; font-size: 10px;">
1173
+ type: {{ f.type }}
1174
+ </div>
1175
+ </div>
1176
+ </div>
1177
+ </div>
1178
+ </div>
1179
+ `,
1180
+ }]
1181
+ }], propDecorators: { data: [{
1182
+ type: Input
1183
+ }], errors: [{
1184
+ type: Input
1185
+ }], touched: [{
1186
+ type: Input
1187
+ }], isDirty: [{
1188
+ type: Input
1189
+ }], fields: [{
1190
+ type: Input
1191
+ }] } });
1192
+
1193
+ function createDynamicFormStore(options) {
1194
+ const fields = options.fields;
1195
+ const initialValues = options.initialValues || {};
1196
+ const validateOnBlur = options.validateOnBlur ?? true;
1197
+ 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);
1205
+ function validate() {
1206
+ const res = validateFields(fields, data());
1207
+ errors.set(res.errors);
1208
+ return res.valid;
1209
+ }
1210
+ function handleChange(newData) {
1211
+ const next = applyComputedValues(fields, newData);
1212
+ data.set(next);
1213
+ isDirty.set(true);
1214
+ if (validateOnChange) {
1215
+ const res = validateFields(fields, next);
1216
+ errors.set(res.errors);
1217
+ }
1218
+ }
1219
+ function setFieldValue(name, value) {
1220
+ handleChange({ ...data(), [name]: value });
1221
+ }
1222
+ function setFieldTouched(name, isTouched = true) {
1223
+ touched.set({ ...touched(), [name]: isTouched });
1224
+ }
1225
+ function handleBlur(fieldName) {
1226
+ setFieldTouched(fieldName, true);
1227
+ if (validateOnBlur) {
1228
+ const res = validateFields(fields, data());
1229
+ errors.set(res.errors);
1230
+ }
1231
+ }
1232
+ function reset(newValues) {
1233
+ const seed = newValues ?? initialValues;
1234
+ const next = applyComputedValues(fields, seed);
1235
+ data.set(next);
1236
+ errors.set({});
1237
+ isDirty.set(false);
1238
+ touched.set({});
1239
+ isSubmitting.set(false);
1240
+ isSubmitted.set(false);
1241
+ }
1242
+ /**
1243
+ * Returns a submit handler, mirroring the React and Vue `useDynamicForm`
1244
+ * hooks. Bind it once and use it as the `(ngSubmit)` handler:
1245
+ * `onSubmit = this.store.handleSubmit(data => ...)`.
1246
+ */
1247
+ function handleSubmit(onValid, onInvalid) {
1248
+ return async (e) => {
1249
+ if (e && typeof e.preventDefault === 'function') {
1250
+ e.preventDefault();
1251
+ }
1252
+ isSubmitting.set(true);
1253
+ try {
1254
+ const res = validateFields(fields, data());
1255
+ errors.set(res.errors);
1256
+ isSubmitted.set(true);
1257
+ if (res.valid) {
1258
+ await onValid(data());
1259
+ }
1260
+ else if (onInvalid) {
1261
+ onInvalid(res.errors);
1262
+ }
1263
+ }
1264
+ finally {
1265
+ isSubmitting.set(false);
1266
+ }
1267
+ };
1268
+ }
1269
+ return {
1270
+ data,
1271
+ errors,
1272
+ isValid,
1273
+ isDirty,
1274
+ touched,
1275
+ isSubmitting,
1276
+ isSubmitted,
1277
+ setFieldValue,
1278
+ setFieldTouched,
1279
+ handleChange,
1280
+ handleBlur,
1281
+ reset,
1282
+ validate,
1283
+ handleSubmit,
1284
+ };
1285
+ }
1286
+
595
1287
  class LayoutRegistry {
596
1288
  layouts = new Map();
597
1289
  register(type, component) {
@@ -612,8 +1304,8 @@ class ColumnLayout {
612
1304
  get gap() {
613
1305
  return this.config?.gap ?? 12;
614
1306
  }
615
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: ColumnLayout, deps: [], target: i0.ɵɵFactoryTarget.Component });
616
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: ColumnLayout, isStandalone: true, selector: "dfk-column-layout", inputs: { config: "config", template: "template" }, ngImport: i0, template: `<div
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
617
1309
  [style.display]="'flex'"
618
1310
  [style.flexDirection]="'column'"
619
1311
  [style.gap]="gap + 'px'"
@@ -621,7 +1313,7 @@ class ColumnLayout {
621
1313
  <ng-container *ngTemplateOutlet="template"></ng-container>
622
1314
  </div>`, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] });
623
1315
  }
624
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: ColumnLayout, decorators: [{
1316
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: ColumnLayout, decorators: [{
625
1317
  type: Component,
626
1318
  args: [{
627
1319
  selector: 'dfk-column-layout',
@@ -646,8 +1338,8 @@ class RowLayout {
646
1338
  get gap() {
647
1339
  return this.config?.gap ?? 12;
648
1340
  }
649
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: RowLayout, deps: [], target: i0.ɵɵFactoryTarget.Component });
650
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: RowLayout, isStandalone: true, selector: "dfk-row-layout", inputs: { config: "config", template: "template" }, ngImport: i0, template: `<div
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
651
1343
  [style.display]="'flex'"
652
1344
  [style.flexDirection]="'row'"
653
1345
  [style.gap]="gap + 'px'"
@@ -655,7 +1347,7 @@ class RowLayout {
655
1347
  <ng-container *ngTemplateOutlet="template"></ng-container>
656
1348
  </div>`, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] });
657
1349
  }
658
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: RowLayout, decorators: [{
1350
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: RowLayout, decorators: [{
659
1351
  type: Component,
660
1352
  args: [{
661
1353
  selector: 'dfk-row-layout',
@@ -683,8 +1375,8 @@ class GridLayout {
683
1375
  get gap() {
684
1376
  return this.config?.gap ?? 12;
685
1377
  }
686
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: GridLayout, deps: [], target: i0.ɵɵFactoryTarget.Component });
687
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: GridLayout, isStandalone: true, selector: "dfk-grid-layout", inputs: { config: "config", template: "template" }, ngImport: i0, template: `<div
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
688
1380
  [style.display]="'grid'"
689
1381
  [style.gridTemplateColumns]="'repeat(' + columns + ', 1fr)'"
690
1382
  [style.gap]="gap + 'px'"
@@ -692,7 +1384,7 @@ class GridLayout {
692
1384
  <ng-container *ngTemplateOutlet="template"></ng-container>
693
1385
  </div>`, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] });
694
1386
  }
695
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: GridLayout, decorators: [{
1387
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: GridLayout, decorators: [{
696
1388
  type: Component,
697
1389
  args: [{
698
1390
  selector: 'dfk-grid-layout',
@@ -716,8 +1408,8 @@ layoutRegistry.register('row', RowLayout);
716
1408
  layoutRegistry.register('grid', GridLayout);
717
1409
 
718
1410
  class DynamicFieldKitModule {
719
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: DynamicFieldKitModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
720
- static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.20", ngImport: i0, type: DynamicFieldKitModule, imports: [CommonModule,
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,
721
1413
  DynamicInput,
722
1414
  FieldInput,
723
1415
  MultiFieldInput,
@@ -729,7 +1421,7 @@ class DynamicFieldKitModule {
729
1421
  ColumnLayout,
730
1422
  RowLayout,
731
1423
  GridLayout] });
732
- static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: DynamicFieldKitModule, imports: [CommonModule,
1424
+ static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: DynamicFieldKitModule, imports: [CommonModule,
733
1425
  DynamicInput,
734
1426
  FieldInput,
735
1427
  MultiFieldInput,
@@ -737,7 +1429,7 @@ class DynamicFieldKitModule {
737
1429
  RowLayout,
738
1430
  GridLayout] });
739
1431
  }
740
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: DynamicFieldKitModule, decorators: [{
1432
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: DynamicFieldKitModule, decorators: [{
741
1433
  type: NgModule,
742
1434
  args: [{
743
1435
  imports: [
@@ -767,5 +1459,4 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
767
1459
  * Generated bundle index. Do not edit.
768
1460
  */
769
1461
 
770
- export { BaseInputComponent, ColumnLayout, DynamicFieldKitModule, DynamicInput, FieldInput, GridLayout, LayoutRegistry, MultiFieldInput, RowLayout, layoutRegistry };
771
- //# sourceMappingURL=dynamic-field-kit-angular.mjs.map
1462
+ export { BaseInputComponent, ColumnLayout, DynamicFieldKitModule, DynamicFormDevToolsComponent, DynamicInput, FIELD_REGISTRY, FieldInput, GridLayout, LayoutRegistry, MultiFieldInput, RowLayout, createDynamicFormStore, layoutRegistry };