@dynamic-field-kit/angular 1.2.10 → 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 (53) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +113 -4
  3. package/dist/README.md +113 -4
  4. package/dist/components/BaseInput.d.ts +23 -11
  5. package/dist/components/DynamicInput.d.ts +19 -8
  6. package/dist/components/FieldInput.d.ts +15 -7
  7. package/dist/components/MultiFieldInput.d.ts +30 -4
  8. package/dist/esm2022/components/BaseInput.mjs +17 -18
  9. package/dist/esm2022/components/DynamicInput.mjs +166 -84
  10. package/dist/esm2022/components/FieldInput.mjs +54 -26
  11. package/dist/esm2022/components/MultiFieldInput.mjs +282 -29
  12. package/dist/esm2022/fieldRegistryToken.mjs +12 -0
  13. package/dist/esm2022/layout/defaultLayouts.mjs +70 -23
  14. package/dist/esm2022/layout/index.mjs +2 -1
  15. package/dist/esm2022/layout/layoutRegistry.mjs +14 -0
  16. package/dist/esm2022/public-api.mjs +6 -2
  17. package/dist/esm2022/types/layout.mjs +1 -1
  18. package/dist/fesm2022/dynamic-field-kit-angular.mjs +604 -173
  19. package/dist/fesm2022/dynamic-field-kit-angular.mjs.map +1 -1
  20. package/dist/fieldRegistryToken.d.ts +3 -0
  21. package/dist/layout/defaultLayouts.d.ts +21 -3
  22. package/dist/layout/index.d.ts +1 -0
  23. package/dist/layout/layoutRegistry.d.ts +12 -0
  24. package/dist/public-api.d.ts +5 -2
  25. package/dist/types/layout.d.ts +4 -1
  26. package/package.json +19 -8
  27. package/src/components/BaseInput.ts +36 -10
  28. package/src/components/DynamicInput.ts +196 -104
  29. package/src/components/FieldInput.ts +43 -18
  30. package/src/components/MultiFieldInput.ts +275 -19
  31. package/src/fieldRegistryToken.ts +15 -0
  32. package/src/layout/defaultLayouts.ts +42 -10
  33. package/src/layout/index.ts +1 -0
  34. package/src/layout/layoutRegistry.ts +25 -0
  35. package/src/public-api.ts +40 -24
  36. package/src/types/layout.ts +14 -1
  37. package/test/DynamicInput.spec.ts +230 -0
  38. package/test/FieldInput.spec.ts +146 -0
  39. package/test/MultiFieldInput.spec.ts +256 -0
  40. package/test/helpers/renderers.ts +89 -0
  41. package/test/layout.spec.ts +119 -0
  42. package/test/publicApi.spec.ts +64 -0
  43. package/test/setup.ts +12 -0
  44. package/test/smoke.spec.ts +27 -0
  45. package/tsconfig.json +13 -13
  46. package/tsconfig.spec.json +9 -0
  47. package/vitest.config.ts +30 -0
  48. package/src/adapters/tailwind.ts +0 -13
  49. package/src/components/imports.ts +0 -3
  50. package/src/examples/index.ts +0 -4
  51. package/src/examples/registerExample.ts +0 -7
  52. package/src/examples/text-field.component.ts +0 -18
  53. package/src/types.ts +0 -1
@@ -1,8 +1,9 @@
1
1
  import * as i0 from '@angular/core';
2
- import { EventEmitter, Output, Input, Component, SimpleChange, ViewContainerRef, ViewChild, NgModule } from '@angular/core';
2
+ import { EventEmitter, Output, Input, Component, InjectionToken, inject, ViewContainerRef, ViewChild, ChangeDetectionStrategy, HostListener, NgModule } from '@angular/core';
3
+ import * as i1 from '@angular/common';
3
4
  import { CommonModule, NgIf, NgClass, NgFor } from '@angular/common';
4
- import { fieldRegistry } from '@dynamic-field-kit/core';
5
- 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';
6
7
 
7
8
  class BaseInputComponent {
8
9
  cdr;
@@ -11,21 +12,26 @@ class BaseInputComponent {
11
12
  placeholder;
12
13
  required;
13
14
  disabled;
15
+ readOnly;
16
+ error;
14
17
  options;
15
18
  className;
16
19
  description;
17
- errorMessage;
18
- acceptFile;
19
- maxLength;
20
- minNumber;
21
- maxNumber;
22
- // Add more from mplis as needed
23
20
  valueChange = new EventEmitter();
24
21
  constructor(cdr) {
25
22
  this.cdr = cdr;
26
23
  }
24
+ ngOnChanges(_changes) {
25
+ this.detectChanges();
26
+ }
27
+ detectChanges() {
28
+ this.cdr.markForCheck();
29
+ }
30
+ hasChanges(changes, prop) {
31
+ return changes[prop] !== undefined;
32
+ }
27
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 });
28
- 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" }, 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 });
29
35
  }
30
36
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: BaseInputComponent, decorators: [{
31
37
  type: Component,
@@ -42,49 +48,72 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
42
48
  type: Input
43
49
  }], disabled: [{
44
50
  type: Input
51
+ }], readOnly: [{
52
+ type: Input
53
+ }], error: [{
54
+ type: Input
45
55
  }], options: [{
46
56
  type: Input
47
57
  }], className: [{
48
58
  type: Input
49
59
  }], description: [{
50
60
  type: Input
51
- }], errorMessage: [{
52
- type: Input
53
- }], acceptFile: [{
54
- type: Input
55
- }], maxLength: [{
56
- type: Input
57
- }], minNumber: [{
58
- type: Input
59
- }], maxNumber: [{
60
- type: Input
61
61
  }], valueChange: [{
62
62
  type: Output
63
63
  }] } });
64
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.
77
+ const KNOWN_PROPS = [
78
+ 'value',
79
+ 'label',
80
+ 'placeholder',
81
+ 'required',
82
+ 'disabled',
83
+ 'readOnly',
84
+ 'error',
85
+ 'options',
86
+ 'className',
87
+ 'description',
88
+ ];
65
89
  class DynamicInput extends BaseInputComponent {
66
90
  type;
91
+ // Extra, framework-agnostic props forwarded verbatim to the renderer.
92
+ extraProps;
67
93
  valueChange = new EventEmitter();
68
- onChange = new EventEmitter(); // backward compat
94
+ onChange = new EventEmitter();
95
+ registry = inject(FIELD_REGISTRY);
69
96
  host;
70
97
  compRef;
71
98
  inputInstance;
72
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();
73
103
  ngOnChanges(changes) {
74
- if (changes['type'] && !changes['type'].firstChange && this.host) {
75
- this.render();
76
- return;
104
+ super.ngOnChanges(changes);
105
+ for (const prop of KNOWN_PROPS) {
106
+ if (changes[prop]) {
107
+ this.supplied.add(prop);
108
+ }
77
109
  }
78
110
  if (this.inputInstance) {
79
- this.applyKnownProps(this.inputInstance);
80
- const childChanges = {};
81
- for (const prop in changes) {
82
- childChanges[prop] = new SimpleChange(changes[prop].previousValue, changes[prop].currentValue, changes[prop].firstChange);
83
- }
84
- this.inputInstance.ngOnChanges?.(childChanges);
85
- this.compRef?.changeDetectorRef.detectChanges();
111
+ this.syncPropsToInstance(changes);
86
112
  }
87
- else if (this.host) {
113
+ if (changes['type'] && !changes['type'].firstChange && this.host) {
114
+ this.render();
115
+ }
116
+ else if (!this.inputInstance && this.host) {
88
117
  this.render();
89
118
  }
90
119
  }
@@ -92,6 +121,23 @@ class DynamicInput extends BaseInputComponent {
92
121
  this.render();
93
122
  }
94
123
  ngOnDestroy() {
124
+ this.cleanup();
125
+ }
126
+ syncPropsToInstance(changes) {
127
+ if (!this.inputInstance) {
128
+ return;
129
+ }
130
+ for (const prop of KNOWN_PROPS) {
131
+ if (changes[prop] && this.inputInstance) {
132
+ this.inputInstance[prop] = this[prop];
133
+ }
134
+ }
135
+ if (changes['extraProps']) {
136
+ this.applyExtraProps(this.inputInstance);
137
+ }
138
+ this.compRef?.changeDetectorRef?.detectChanges();
139
+ }
140
+ cleanup() {
95
141
  this.cleanupSubscriptions();
96
142
  this.cleanupRenderedComponent();
97
143
  }
@@ -99,106 +145,149 @@ class DynamicInput extends BaseInputComponent {
99
145
  this.subscriptions.forEach((s) => s.unsubscribe());
100
146
  this.subscriptions = [];
101
147
  }
148
+ cleanupRenderedComponent() {
149
+ this.compRef?.destroy();
150
+ this.compRef = undefined;
151
+ this.inputInstance = undefined;
152
+ }
102
153
  render() {
103
- const Renderer = fieldRegistry.get(this.type);
104
- this.cleanupRenderedComponent();
105
- this.cleanupSubscriptions();
154
+ const Renderer = this.registry.get(this.type);
155
+ this.cleanup();
106
156
  this.host.clear();
107
157
  if (!Renderer) {
108
- const el = document.createElement('div');
109
- el.textContent = `Unknown field type: ${this.type}`;
110
- this.host.element.nativeElement.appendChild(el);
158
+ this.renderError(`Unknown field type: ${this.type}`);
111
159
  return;
112
160
  }
161
+ if (this.isComponentType(Renderer)) {
162
+ this.renderComponent(Renderer);
163
+ }
164
+ else if (typeof Renderer === 'function') {
165
+ this.renderFallback(Renderer);
166
+ }
167
+ else {
168
+ this.renderError(`Invalid renderer for type: ${this.type}`);
169
+ }
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.
178
+ isComponentType(renderer) {
179
+ return (typeof renderer === 'function' &&
180
+ Object.prototype.hasOwnProperty.call(renderer, 'ɵcmp'));
181
+ }
182
+ renderComponent(compType) {
113
183
  try {
114
- const compType = Renderer;
115
184
  const compRef = this.host.createComponent(compType);
116
185
  const instance = compRef.instance;
117
186
  if (!instance) {
118
- throw new Error(`Failed to create instance for ${this.type}`);
119
- }
120
- this.applyKnownProps(instance);
121
- const subA = this.bindOutput(instance, 'valueChange');
122
- if (subA) {
123
- this.subscriptions.push(subA);
187
+ this.renderError(`Failed to create instance for ${this.type}`);
188
+ return;
124
189
  }
125
- const subB = this.bindOutput(instance, 'onValueChange');
126
- if (subB) {
127
- this.subscriptions.push(subB);
128
- }
129
- instance.changeDetectorRef?.detectChanges();
190
+ this.applyProps(instance);
191
+ this.bindOutputs(instance);
130
192
  this.compRef = compRef;
131
193
  this.inputInstance = instance;
132
194
  compRef.changeDetectorRef.detectChanges();
133
195
  }
134
- catch (err) {
135
- // Fallback to function renderer
136
- try {
137
- const props = {
138
- value: this.value,
139
- onValueChange: (v) => this.emitValue(v),
140
- label: this.label,
141
- placeholder: this.placeholder,
142
- required: this.required,
143
- options: this.options,
144
- className: this.className,
145
- description: this.description,
146
- disabled: this.disabled,
147
- errorMessage: this.errorMessage,
148
- };
149
- const out = Renderer(props);
150
- if (typeof out === 'string') {
151
- const el = document.createElement('div');
152
- el.innerHTML = out;
153
- this.host.element.nativeElement.appendChild(el);
154
- }
155
- }
156
- catch (e) {
196
+ catch {
197
+ this.renderError(`Failed to render field: ${this.type}`);
198
+ }
199
+ }
200
+ renderFallback(renderer) {
201
+ try {
202
+ const props = this.getFallbackProps();
203
+ const result = renderer(props);
204
+ if (typeof result === 'string') {
157
205
  const el = document.createElement('div');
158
- el.textContent = `Failed to render field: ${this.type}`;
206
+ el.innerHTML = result;
159
207
  this.host.element.nativeElement.appendChild(el);
160
208
  }
161
209
  }
210
+ catch {
211
+ this.renderError(`Failed to render field: ${this.type}`);
212
+ }
162
213
  }
163
- applyKnownProps(instance) {
164
- const knownProps = [
165
- 'value',
166
- 'label',
167
- 'placeholder',
168
- 'required',
169
- 'disabled',
170
- 'options',
171
- 'className',
172
- 'description',
173
- 'errorMessage',
174
- ];
175
- for (const prop of knownProps) {
176
- if (prop in this && prop in instance) {
177
- instance[prop] = this[prop];
214
+ getFallbackProps() {
215
+ return {
216
+ ...this.extraProps,
217
+ value: this.value,
218
+ onValueChange: (v) => this.emitValue(v),
219
+ label: this.label ?? '',
220
+ placeholder: this.placeholder ?? '',
221
+ required: this.required ?? false,
222
+ disabled: this.disabled ?? false,
223
+ readOnly: this.readOnly ?? false,
224
+ error: this.error,
225
+ options: this.options ?? [],
226
+ className: this.className ?? '',
227
+ description: this.description ?? '',
228
+ };
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.
248
+ applyProps(instance) {
249
+ const instanceObj = instance;
250
+ for (const prop of KNOWN_PROPS) {
251
+ if (this.supplied.has(prop)) {
252
+ instanceObj[prop] = this[prop];
178
253
  }
179
254
  }
255
+ this.applyExtraProps(instance);
180
256
  }
181
- bindOutput(instance, outputName) {
182
- const output = instance?.[outputName];
183
- if (!output || typeof output.subscribe !== 'function') {
257
+ applyExtraProps(instance) {
258
+ if (!instance || !this.extraProps) {
184
259
  return;
185
260
  }
186
- const sub = output.subscribe((value) => {
187
- this.emitValue(value);
188
- });
189
- return sub;
261
+ const instanceObj = instance;
262
+ for (const [key, value] of Object.entries(this.extraProps)) {
263
+ instanceObj[key] = value;
264
+ }
265
+ }
266
+ bindOutputs(instance) {
267
+ const outputNames = [
268
+ 'valueChange',
269
+ 'onValueChange',
270
+ ];
271
+ for (const outputName of outputNames) {
272
+ const output = instance[outputName];
273
+ if (output && typeof output === 'object' && 'subscribe' in output) {
274
+ const sub = output.subscribe((value) => this.emitValue(value));
275
+ this.subscriptions.push(sub);
276
+ }
277
+ }
190
278
  }
191
279
  emitValue(value) {
192
280
  this.valueChange.emit(value);
193
281
  this.onChange.emit(value);
194
282
  }
195
- cleanupRenderedComponent() {
196
- this.compRef?.destroy();
197
- this.compRef = undefined;
198
- this.inputInstance = undefined;
283
+ renderError(message) {
284
+ const el = document.createElement('div');
285
+ el.textContent = message;
286
+ el.style.color = 'red';
287
+ this.host.element.nativeElement.appendChild(el);
199
288
  }
200
289
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: DynamicInput, deps: null, target: i0.ɵɵFactoryTarget.Component });
201
- 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 }] });
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 });
202
291
  }
203
292
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: DynamicInput, decorators: [{
204
293
  type: Component,
@@ -206,10 +295,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
206
295
  selector: 'dfk-dynamic-input',
207
296
  standalone: true,
208
297
  imports: [CommonModule],
298
+ changeDetection: ChangeDetectionStrategy.OnPush,
209
299
  template: `<div #host style="display: contents;"></div>`,
210
300
  }]
211
301
  }], propDecorators: { type: [{
212
302
  type: Input
303
+ }], extraProps: [{
304
+ type: Input
213
305
  }], valueChange: [{
214
306
  type: Output
215
307
  }], onChange: [{
@@ -220,38 +312,55 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
220
312
  }] } });
221
313
 
222
314
  class FieldInput {
315
+ cdr;
223
316
  fieldDescription;
224
- renderInfos;
317
+ value;
318
+ options;
319
+ disabled;
320
+ readOnly;
321
+ error;
225
322
  onValueChangeField = new EventEmitter();
226
- getFieldValue() {
227
- if (!this.fieldDescription || !this.renderInfos) {
323
+ shouldRender = false;
324
+ get resolvedOptions() {
325
+ if (this.options) {
326
+ return this.options;
327
+ }
328
+ if (!this.fieldDescription?.options) {
228
329
  return undefined;
229
330
  }
230
- const value = this.renderInfos[this.fieldDescription.name];
231
- if (value === undefined && this.fieldDescription.type === 'text') {
232
- return '';
331
+ if (typeof this.fieldDescription.options === 'function') {
332
+ return undefined;
233
333
  }
234
- return value;
334
+ return this.fieldDescription.options;
335
+ }
336
+ constructor(cdr) {
337
+ this.cdr = cdr;
338
+ }
339
+ ngOnChanges(_changes) {
340
+ this.shouldRender = !!this.fieldDescription;
341
+ this.cdr.markForCheck();
235
342
  }
236
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: FieldInput, deps: [], target: i0.ɵɵFactoryTarget.Component });
237
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: FieldInput, isStandalone: true, selector: "dfk-field-input", inputs: { fieldDescription: "fieldDescription", renderInfos: "renderInfos" }, outputs: { onValueChangeField: "onValueChangeField" }, ngImport: i0, template: `
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 });
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: `
238
345
  <dfk-dynamic-input
239
- *ngIf="fieldDescription && renderInfos"
346
+ *ngIf="shouldRender"
240
347
  [type]="fieldDescription!.type"
241
- [value]="getFieldValue()"
348
+ [value]="value"
242
349
  [label]="fieldDescription!.label"
243
350
  [placeholder]="fieldDescription!.placeholder"
244
351
  [required]="fieldDescription!.required"
245
- [description]="fieldDescription!.description"
246
- [options]="fieldDescription!.options"
352
+ [description]="$any(fieldDescription!.description)"
353
+ [options]="resolvedOptions"
247
354
  [className]="fieldDescription!.className"
248
355
  (valueChange)="
249
356
  onValueChangeField.emit({ value: $event, key: fieldDescription!.name })
250
357
  "
251
- [disabled]="false"
252
- [errorMessage]="''"
358
+ [disabled]="disabled"
359
+ [readOnly]="readOnly"
360
+ [error]="$any(error)"
361
+ [extraProps]="fieldDescription!.props"
253
362
  ></dfk-dynamic-input>
254
- `, 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"] }] });
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 });
255
364
  }
256
365
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: FieldInput, decorators: [{
257
366
  type: Component,
@@ -259,121 +368,405 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
259
368
  selector: 'dfk-field-input',
260
369
  standalone: true,
261
370
  imports: [NgIf, DynamicInput],
371
+ changeDetection: ChangeDetectionStrategy.OnPush,
262
372
  template: `
263
373
  <dfk-dynamic-input
264
- *ngIf="fieldDescription && renderInfos"
374
+ *ngIf="shouldRender"
265
375
  [type]="fieldDescription!.type"
266
- [value]="getFieldValue()"
376
+ [value]="value"
267
377
  [label]="fieldDescription!.label"
268
378
  [placeholder]="fieldDescription!.placeholder"
269
379
  [required]="fieldDescription!.required"
270
- [description]="fieldDescription!.description"
271
- [options]="fieldDescription!.options"
380
+ [description]="$any(fieldDescription!.description)"
381
+ [options]="resolvedOptions"
272
382
  [className]="fieldDescription!.className"
273
383
  (valueChange)="
274
384
  onValueChangeField.emit({ value: $event, key: fieldDescription!.name })
275
385
  "
276
- [disabled]="false"
277
- [errorMessage]="''"
386
+ [disabled]="disabled"
387
+ [readOnly]="readOnly"
388
+ [error]="$any(error)"
389
+ [extraProps]="fieldDescription!.props"
278
390
  ></dfk-dynamic-input>
279
391
  `,
280
392
  }]
281
- }], propDecorators: { fieldDescription: [{
393
+ }], ctorParameters: () => [{ type: i0.ChangeDetectorRef }], propDecorators: { fieldDescription: [{
282
394
  type: Input
283
- }], renderInfos: [{
395
+ }], value: [{
396
+ type: Input
397
+ }], options: [{
398
+ type: Input
399
+ }], disabled: [{
400
+ type: Input
401
+ }], readOnly: [{
402
+ type: Input
403
+ }], error: [{
284
404
  type: Input
285
405
  }], onValueChangeField: [{
286
406
  type: Output
287
407
  }] } });
288
408
 
409
+ const DEFAULT_BREAKPOINT = 768;
289
410
  class MultiFieldInput {
411
+ cdr;
290
412
  fieldDescriptions = [];
291
413
  properties;
292
414
  onChange = new EventEmitter();
415
+ validityChange = new EventEmitter();
293
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;
294
421
  data = {};
295
422
  visibleFields = [];
423
+ isMobile = false;
424
+ constructor(cdr) {
425
+ this.cdr = cdr;
426
+ }
427
+ onWindowResize() {
428
+ const wasMobile = this.isMobile;
429
+ this.updateIsMobile();
430
+ if (wasMobile !== this.isMobile) {
431
+ this.cdr.markForCheck();
432
+ }
433
+ }
434
+ get resolvedLayout() {
435
+ if (typeof this.layout === 'object' && this.layout.type === 'responsive') {
436
+ return this.isMobile ? this.layout.mobile : this.layout.desktop;
437
+ }
438
+ return this.layout;
439
+ }
440
+ get resolvedLayoutType() {
441
+ const resolved = this.resolvedLayout;
442
+ return typeof resolved === 'string' ? resolved : resolved.type;
443
+ }
444
+ get gap() {
445
+ const resolved = this.resolvedLayout;
446
+ if (typeof resolved === 'object' && resolved.gap !== undefined) {
447
+ return resolved.gap;
448
+ }
449
+ return 12;
450
+ }
451
+ get columns() {
452
+ const resolved = this.resolvedLayout;
453
+ if (typeof resolved === 'object' && resolved.type === 'grid') {
454
+ return resolved.columns ?? 2;
455
+ }
456
+ return 2;
457
+ }
296
458
  trackByFn(index, field) {
297
459
  return field.name || index;
298
460
  }
461
+ trackByIndex(index) {
462
+ return index;
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
+ }
299
476
  ngOnInit() {
477
+ this.updateIsMobile();
300
478
  this.init();
301
479
  }
302
480
  ngOnChanges(_changes) {
303
481
  this.init();
304
482
  }
483
+ updateIsMobile() {
484
+ const breakpoint = typeof this.layout === 'object' && this.layout.type === 'responsive'
485
+ ? this.layout.breakpoint ?? DEFAULT_BREAKPOINT
486
+ : DEFAULT_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
+ }
495
+ }
305
496
  init() {
306
497
  if (this.properties) {
307
- this.data = { ...this.properties };
498
+ this.data = applyComputedValues(this.fieldDescriptions, { ...this.properties }, this.rootData);
308
499
  }
309
500
  this.updateVisibleFields();
501
+ this.validityChange.emit(validateFields(this.fieldDescriptions, this.data, this.rootData));
310
502
  }
311
503
  updateVisibleFields() {
312
- 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));
313
506
  }
314
507
  onFieldChange(event) {
315
- this.data = { ...this.data, [event.key]: event.value };
508
+ this.commitData({ ...this.data, [event.key]: event.value });
509
+ }
510
+ getItems(field) {
511
+ const value = this.data[field.name];
512
+ return Array.isArray(value) ? value : [];
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
+ }
530
+ canAddItem(field) {
531
+ return canAddGroupItem(field, this.getItems(field));
532
+ }
533
+ canRemoveItem(field) {
534
+ return canRemoveGroupItem(field, this.getItems(field));
535
+ }
536
+ onGroupItemAdd(field) {
537
+ if (!this.canAddItem(field)) {
538
+ return;
539
+ }
540
+ const items = this.getItems(field);
541
+ this.commitData({
542
+ ...this.data,
543
+ [field.name]: [...items, createGroupItem(field)],
544
+ });
545
+ }
546
+ onGroupItemRemove(field, index) {
547
+ if (!this.canRemoveItem(field)) {
548
+ return;
549
+ }
550
+ const items = this.getItems(field).filter((_, i) => i !== index);
551
+ this.commitData({ ...this.data, [field.name]: items });
552
+ }
553
+ onGroupItemChange(field, index, next) {
554
+ const items = this.getItems(field).slice();
555
+ items[index] = next;
556
+ this.commitData({ ...this.data, [field.name]: items });
557
+ }
558
+ commitData(nextData) {
559
+ this.data = applyComputedValues(this.fieldDescriptions, nextData, this.rootData);
316
560
  this.updateVisibleFields();
317
561
  this.onChange.emit(this.data);
562
+ this.validityChange.emit(validateFields(this.fieldDescriptions, this.data, this.rootData));
563
+ this.cdr.markForCheck();
318
564
  }
319
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: MultiFieldInput, deps: [], target: i0.ɵɵFactoryTarget.Component });
320
- 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" }, usesOnChanges: true, ngImport: i0, template: `
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 });
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: `
321
567
  <div
322
- class="flex flex-col gap-3 p-4 border rounded-lg bg-gray-50"
568
+ class="p-4 border rounded-lg bg-gray-50"
323
569
  [ngClass]="{
324
- 'flex-row gap-3': layout === 'row',
325
- 'grid grid-cols-2 gap-3': layout === 'grid'
570
+ 'flex flex-col': resolvedLayoutType === 'column',
571
+ 'flex flex-row': resolvedLayoutType === 'row',
572
+ grid: resolvedLayoutType === 'grid'
326
573
  }"
574
+ [style.gap.px]="gap"
575
+ [style.gridTemplateColumns]="
576
+ resolvedLayoutType === 'grid' ? 'repeat(' + columns + ', 1fr)' : null
577
+ "
327
578
  >
328
- <dfk-field-input
329
- *ngFor="let field of visibleFields; trackBy: trackByFn"
330
- [fieldDescription]="field"
331
- [renderInfos]="data"
332
- (onValueChangeField)="onFieldChange($event)"
333
- ></dfk-field-input>
579
+ <ng-container *ngFor="let field of visibleFields; trackBy: trackByFn">
580
+ <dfk-field-input
581
+ *ngIf="!field.fields"
582
+ [fieldDescription]="field"
583
+ [value]="data[field.name]"
584
+ [options]="getResolvedOptions(field)"
585
+ [disabled]="getDisabled(field)"
586
+ [readOnly]="getReadOnly(field)"
587
+ [error]="getError(field)"
588
+ (onValueChangeField)="onFieldChange($event)"
589
+ ></dfk-field-input>
590
+
591
+ <div *ngIf="field.fields" [class]="field.className">
592
+ <div *ngIf="field.label">{{ field.label }}</div>
593
+ <div
594
+ *ngFor="
595
+ let item of getItems(field);
596
+ let i = index;
597
+ trackBy: groupTrackBy(field)
598
+ "
599
+ style="display: flex; align-items: flex-start; gap: 8px;"
600
+ >
601
+ <div style="flex: 1">
602
+ <dfk-multi-field-input
603
+ [fieldDescriptions]="field.fields"
604
+ [properties]="item"
605
+ [rootData]="rootData ?? data"
606
+ (onChange)="onGroupItemChange(field, i, $event)"
607
+ ></dfk-multi-field-input>
608
+ </div>
609
+ <button
610
+ type="button"
611
+ [attr.aria-label]="
612
+ (field.removeLabel || 'Remove') +
613
+ ' ' +
614
+ (field.label || field.name) +
615
+ ' ' +
616
+ (i + 1)
617
+ "
618
+ (click)="onGroupItemRemove(field, i)"
619
+ [disabled]="!canRemoveItem(field)"
620
+ >
621
+ {{ field.removeLabel || 'Remove' }}
622
+ </button>
623
+ </div>
624
+ <button
625
+ type="button"
626
+ [attr.aria-label]="
627
+ (field.addLabel || 'Add') + ' ' + (field.label || field.name)
628
+ "
629
+ (click)="onGroupItemAdd(field)"
630
+ [disabled]="!canAddItem(field)"
631
+ >
632
+ {{ field.addLabel || 'Add' }}
633
+ </button>
634
+ </div>
635
+ </ng-container>
334
636
  </div>
335
- `, isInline: true, dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgFor, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "component", type: FieldInput, selector: "dfk-field-input", inputs: ["fieldDescription", "renderInfos"], outputs: ["onValueChangeField"] }] });
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 });
336
638
  }
337
639
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: MultiFieldInput, decorators: [{
338
640
  type: Component,
339
641
  args: [{
340
642
  selector: 'dfk-multi-field-input',
341
643
  standalone: true,
342
- imports: [NgClass, NgFor, FieldInput],
644
+ // Repeatable field groups render a nested <dfk-multi-field-input> per item
645
+ // (see the #groupTpl below), so this component imports itself. That's the
646
+ // supported way to give a standalone component recursive template usage
647
+ // without a cross-file circular import between FieldInput and a separate
648
+ // group component (whose `imports` arrays are evaluated eagerly at module
649
+ // load time and would deadlock on an actual circular module reference).
650
+ imports: [NgClass, NgFor, NgIf, FieldInput, MultiFieldInput],
651
+ changeDetection: ChangeDetectionStrategy.OnPush,
343
652
  template: `
344
653
  <div
345
- class="flex flex-col gap-3 p-4 border rounded-lg bg-gray-50"
654
+ class="p-4 border rounded-lg bg-gray-50"
346
655
  [ngClass]="{
347
- 'flex-row gap-3': layout === 'row',
348
- 'grid grid-cols-2 gap-3': layout === 'grid'
656
+ 'flex flex-col': resolvedLayoutType === 'column',
657
+ 'flex flex-row': resolvedLayoutType === 'row',
658
+ grid: resolvedLayoutType === 'grid'
349
659
  }"
660
+ [style.gap.px]="gap"
661
+ [style.gridTemplateColumns]="
662
+ resolvedLayoutType === 'grid' ? 'repeat(' + columns + ', 1fr)' : null
663
+ "
350
664
  >
351
- <dfk-field-input
352
- *ngFor="let field of visibleFields; trackBy: trackByFn"
353
- [fieldDescription]="field"
354
- [renderInfos]="data"
355
- (onValueChangeField)="onFieldChange($event)"
356
- ></dfk-field-input>
665
+ <ng-container *ngFor="let field of visibleFields; trackBy: trackByFn">
666
+ <dfk-field-input
667
+ *ngIf="!field.fields"
668
+ [fieldDescription]="field"
669
+ [value]="data[field.name]"
670
+ [options]="getResolvedOptions(field)"
671
+ [disabled]="getDisabled(field)"
672
+ [readOnly]="getReadOnly(field)"
673
+ [error]="getError(field)"
674
+ (onValueChangeField)="onFieldChange($event)"
675
+ ></dfk-field-input>
676
+
677
+ <div *ngIf="field.fields" [class]="field.className">
678
+ <div *ngIf="field.label">{{ field.label }}</div>
679
+ <div
680
+ *ngFor="
681
+ let item of getItems(field);
682
+ let i = index;
683
+ trackBy: groupTrackBy(field)
684
+ "
685
+ style="display: flex; align-items: flex-start; gap: 8px;"
686
+ >
687
+ <div style="flex: 1">
688
+ <dfk-multi-field-input
689
+ [fieldDescriptions]="field.fields"
690
+ [properties]="item"
691
+ [rootData]="rootData ?? data"
692
+ (onChange)="onGroupItemChange(field, i, $event)"
693
+ ></dfk-multi-field-input>
694
+ </div>
695
+ <button
696
+ type="button"
697
+ [attr.aria-label]="
698
+ (field.removeLabel || 'Remove') +
699
+ ' ' +
700
+ (field.label || field.name) +
701
+ ' ' +
702
+ (i + 1)
703
+ "
704
+ (click)="onGroupItemRemove(field, i)"
705
+ [disabled]="!canRemoveItem(field)"
706
+ >
707
+ {{ field.removeLabel || 'Remove' }}
708
+ </button>
709
+ </div>
710
+ <button
711
+ type="button"
712
+ [attr.aria-label]="
713
+ (field.addLabel || 'Add') + ' ' + (field.label || field.name)
714
+ "
715
+ (click)="onGroupItemAdd(field)"
716
+ [disabled]="!canAddItem(field)"
717
+ >
718
+ {{ field.addLabel || 'Add' }}
719
+ </button>
720
+ </div>
721
+ </ng-container>
357
722
  </div>
358
723
  `,
359
724
  }]
360
- }], propDecorators: { fieldDescriptions: [{
725
+ }], ctorParameters: () => [{ type: i0.ChangeDetectorRef }], propDecorators: { fieldDescriptions: [{
361
726
  type: Input
362
727
  }], properties: [{
363
728
  type: Input
364
729
  }], onChange: [{
365
730
  type: Output
731
+ }], validityChange: [{
732
+ type: Output
366
733
  }], layout: [{
367
734
  type: Input
735
+ }], rootData: [{
736
+ type: Input
737
+ }], onWindowResize: [{
738
+ type: HostListener,
739
+ args: ['window:resize']
368
740
  }] } });
369
741
 
742
+ class LayoutRegistry {
743
+ layouts = new Map();
744
+ register(type, component) {
745
+ if (this.layouts.has(type)) {
746
+ console.warn(`[dynamic-field-kit] Layout "${type}" already exists`);
747
+ }
748
+ this.layouts.set(type, component);
749
+ }
750
+ get(type) {
751
+ return this.layouts.get(type);
752
+ }
753
+ }
754
+ const layoutRegistry = new LayoutRegistry();
755
+
370
756
  class ColumnLayout {
757
+ config;
758
+ template;
759
+ get gap() {
760
+ return this.config?.gap ?? 12;
761
+ }
371
762
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: ColumnLayout, deps: [], target: i0.ɵɵFactoryTarget.Component });
372
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: ColumnLayout, isStandalone: true, selector: "dfk-column-layout", ngImport: i0, template: `<div
373
- style="display:flex;flex-direction:column;gap:var(--dfk-gap,12px)"
763
+ 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
764
+ [style.display]="'flex'"
765
+ [style.flexDirection]="'column'"
766
+ [style.gap]="gap + 'px'"
374
767
  >
375
- <ng-content></ng-content>
376
- </div>`, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }] });
768
+ <ng-container *ngTemplateOutlet="template"></ng-container>
769
+ </div>`, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] });
377
770
  }
378
771
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: ColumnLayout, decorators: [{
379
772
  type: Component,
@@ -382,19 +775,32 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
382
775
  standalone: true,
383
776
  imports: [CommonModule],
384
777
  template: `<div
385
- style="display:flex;flex-direction:column;gap:var(--dfk-gap,12px)"
778
+ [style.display]="'flex'"
779
+ [style.flexDirection]="'column'"
780
+ [style.gap]="gap + 'px'"
386
781
  >
387
- <ng-content></ng-content>
782
+ <ng-container *ngTemplateOutlet="template"></ng-container>
388
783
  </div>`,
389
784
  }]
390
- }] });
785
+ }], propDecorators: { config: [{
786
+ type: Input
787
+ }], template: [{
788
+ type: Input
789
+ }] } });
391
790
  class RowLayout {
791
+ config;
792
+ template;
793
+ get gap() {
794
+ return this.config?.gap ?? 12;
795
+ }
392
796
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: RowLayout, deps: [], target: i0.ɵɵFactoryTarget.Component });
393
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: RowLayout, isStandalone: true, selector: "dfk-row-layout", ngImport: i0, template: `<div
394
- style="display:flex;flex-direction:row;gap:var(--dfk-gap,12px)"
797
+ 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
798
+ [style.display]="'flex'"
799
+ [style.flexDirection]="'row'"
800
+ [style.gap]="gap + 'px'"
395
801
  >
396
- <ng-content></ng-content>
397
- </div>`, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }] });
802
+ <ng-container *ngTemplateOutlet="template"></ng-container>
803
+ </div>`, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] });
398
804
  }
399
805
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: RowLayout, decorators: [{
400
806
  type: Component,
@@ -403,19 +809,35 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
403
809
  standalone: true,
404
810
  imports: [CommonModule],
405
811
  template: `<div
406
- style="display:flex;flex-direction:row;gap:var(--dfk-gap,12px)"
812
+ [style.display]="'flex'"
813
+ [style.flexDirection]="'row'"
814
+ [style.gap]="gap + 'px'"
407
815
  >
408
- <ng-content></ng-content>
816
+ <ng-container *ngTemplateOutlet="template"></ng-container>
409
817
  </div>`,
410
818
  }]
411
- }] });
819
+ }], propDecorators: { config: [{
820
+ type: Input
821
+ }], template: [{
822
+ type: Input
823
+ }] } });
412
824
  class GridLayout {
825
+ config;
826
+ template;
827
+ get columns() {
828
+ return this.config?.columns ?? 2;
829
+ }
830
+ get gap() {
831
+ return this.config?.gap ?? 12;
832
+ }
413
833
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: GridLayout, deps: [], target: i0.ɵɵFactoryTarget.Component });
414
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: GridLayout, isStandalone: true, selector: "dfk-grid-layout", ngImport: i0, template: `<div
415
- style="display:grid;grid-template-columns:repeat(var(--dfk-columns,2),1fr);gap:var(--dfk-gap,12px)"
834
+ 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
835
+ [style.display]="'grid'"
836
+ [style.gridTemplateColumns]="'repeat(' + columns + ', 1fr)'"
837
+ [style.gap]="gap + 'px'"
416
838
  >
417
- <ng-content></ng-content>
418
- </div>`, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }] });
839
+ <ng-container *ngTemplateOutlet="template"></ng-container>
840
+ </div>`, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] });
419
841
  }
420
842
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: GridLayout, decorators: [{
421
843
  type: Component,
@@ -424,12 +846,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
424
846
  standalone: true,
425
847
  imports: [CommonModule],
426
848
  template: `<div
427
- style="display:grid;grid-template-columns:repeat(var(--dfk-columns,2),1fr);gap:var(--dfk-gap,12px)"
849
+ [style.display]="'grid'"
850
+ [style.gridTemplateColumns]="'repeat(' + columns + ', 1fr)'"
851
+ [style.gap]="gap + 'px'"
428
852
  >
429
- <ng-content></ng-content>
853
+ <ng-container *ngTemplateOutlet="template"></ng-container>
430
854
  </div>`,
431
855
  }]
432
- }] });
856
+ }], propDecorators: { config: [{
857
+ type: Input
858
+ }], template: [{
859
+ type: Input
860
+ }] } });
861
+ layoutRegistry.register('column', ColumnLayout);
862
+ layoutRegistry.register('row', RowLayout);
863
+ layoutRegistry.register('grid', GridLayout);
433
864
 
434
865
  class DynamicFieldKitModule {
435
866
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: DynamicFieldKitModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
@@ -483,5 +914,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
483
914
  * Generated bundle index. Do not edit.
484
915
  */
485
916
 
486
- export { BaseInputComponent, ColumnLayout, DynamicFieldKitModule, DynamicInput, FieldInput, GridLayout, MultiFieldInput, RowLayout };
917
+ export { BaseInputComponent, ColumnLayout, DynamicFieldKitModule, DynamicInput, FIELD_REGISTRY, FieldInput, GridLayout, LayoutRegistry, MultiFieldInput, RowLayout, layoutRegistry };
487
918
  //# sourceMappingURL=dynamic-field-kit-angular.mjs.map