@dynamic-field-kit/angular 1.2.10 → 1.3.3

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 (47) hide show
  1. package/README.md +63 -1
  2. package/angular.json +27 -0
  3. package/dist/README.md +63 -1
  4. package/dist/components/BaseInput.d.ts +23 -5
  5. package/dist/components/DynamicInput.d.ts +13 -6
  6. package/dist/components/FieldInput.d.ts +10 -6
  7. package/dist/components/MultiFieldInput.d.ts +20 -3
  8. package/dist/esm2022/components/BaseInput.mjs +11 -3
  9. package/dist/esm2022/components/DynamicInput.mjs +112 -83
  10. package/dist/esm2022/components/FieldInput.mjs +25 -16
  11. package/dist/esm2022/components/MultiFieldInput.mjs +207 -28
  12. package/dist/esm2022/layout/defaultLayouts.mjs +70 -23
  13. package/dist/esm2022/layout/index.mjs +2 -1
  14. package/dist/esm2022/layout/layoutRegistry.mjs +14 -0
  15. package/dist/esm2022/public-api.mjs +1 -1
  16. package/dist/esm2022/types/layout.mjs +1 -1
  17. package/dist/fesm2022/dynamic-field-kit-angular.mjs +430 -146
  18. package/dist/fesm2022/dynamic-field-kit-angular.mjs.map +1 -1
  19. package/dist/layout/defaultLayouts.d.ts +21 -3
  20. package/dist/layout/index.d.ts +1 -0
  21. package/dist/layout/layoutRegistry.d.ts +12 -0
  22. package/dist/public-api.d.ts +1 -1
  23. package/dist/types/layout.d.ts +25 -1
  24. package/karma.conf.js +37 -0
  25. package/package.json +18 -5
  26. package/src/components/BaseInput.ts +34 -5
  27. package/src/components/DynamicInput.ts +142 -106
  28. package/src/components/FieldInput.ts +30 -12
  29. package/src/components/MultiFieldInput.ts +181 -18
  30. package/src/layout/defaultLayouts.ts +42 -10
  31. package/src/layout/index.ts +1 -0
  32. package/src/layout/layoutRegistry.ts +25 -0
  33. package/src/public-api.ts +1 -1
  34. package/src/types/layout.ts +36 -1
  35. package/test/DynamicInput.spec.ts +30 -0
  36. package/test/FieldInput.spec.ts +44 -0
  37. package/test/angular.spec.ts +102 -0
  38. package/test/integration.spec.ts +57 -0
  39. package/test/layouts.spec.ts +26 -0
  40. package/test.ts +12 -0
  41. package/tsconfig.spec.json +21 -0
  42. package/src/adapters/tailwind.ts +0 -13
  43. package/src/components/imports.ts +0 -3
  44. package/src/examples/index.ts +0 -4
  45. package/src/examples/registerExample.ts +0 -7
  46. package/src/examples/text-field.component.ts +0 -18
  47. package/src/types.ts +0 -1
@@ -1,7 +1,8 @@
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, 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
+ import { fieldRegistry, applyComputedValues, canAddGroupItem, canRemoveGroupItem, createGroupItem } from '@dynamic-field-kit/core';
5
6
  export { fieldRegistry } from '@dynamic-field-kit/core';
6
7
 
7
8
  class BaseInputComponent {
@@ -19,13 +20,21 @@ class BaseInputComponent {
19
20
  maxLength;
20
21
  minNumber;
21
22
  maxNumber;
22
- // Add more from mplis as needed
23
23
  valueChange = new EventEmitter();
24
24
  constructor(cdr) {
25
25
  this.cdr = cdr;
26
26
  }
27
+ ngOnChanges(_changes) {
28
+ this.detectChanges();
29
+ }
30
+ detectChanges() {
31
+ this.cdr.markForCheck();
32
+ }
33
+ hasChanges(changes, prop) {
34
+ return changes[prop] !== undefined;
35
+ }
27
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 });
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 });
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 });
29
38
  }
30
39
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: BaseInputComponent, decorators: [{
31
40
  type: Component,
@@ -62,29 +71,38 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
62
71
  type: Output
63
72
  }] } });
64
73
 
74
+ const KNOWN_PROPS = [
75
+ 'value',
76
+ 'label',
77
+ 'placeholder',
78
+ 'required',
79
+ 'disabled',
80
+ 'options',
81
+ 'className',
82
+ 'description',
83
+ 'errorMessage',
84
+ 'acceptFile',
85
+ 'maxLength',
86
+ 'minNumber',
87
+ 'maxNumber',
88
+ ];
65
89
  class DynamicInput extends BaseInputComponent {
66
90
  type;
67
91
  valueChange = new EventEmitter();
68
- onChange = new EventEmitter(); // backward compat
92
+ onChange = new EventEmitter();
69
93
  host;
70
94
  compRef;
71
95
  inputInstance;
72
96
  subscriptions = [];
73
97
  ngOnChanges(changes) {
98
+ super.ngOnChanges(changes);
99
+ if (this.inputInstance) {
100
+ this.syncPropsToInstance(changes);
101
+ }
74
102
  if (changes['type'] && !changes['type'].firstChange && this.host) {
75
103
  this.render();
76
- return;
77
- }
78
- 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();
86
104
  }
87
- else if (this.host) {
105
+ else if (!this.inputInstance && this.host) {
88
106
  this.render();
89
107
  }
90
108
  }
@@ -92,6 +110,20 @@ class DynamicInput extends BaseInputComponent {
92
110
  this.render();
93
111
  }
94
112
  ngOnDestroy() {
113
+ this.cleanup();
114
+ }
115
+ syncPropsToInstance(changes) {
116
+ if (!this.inputInstance) {
117
+ return;
118
+ }
119
+ for (const prop of KNOWN_PROPS) {
120
+ if (changes[prop] && this.inputInstance) {
121
+ this.inputInstance[prop] = this[prop];
122
+ }
123
+ }
124
+ this.compRef?.changeDetectorRef?.detectChanges();
125
+ }
126
+ cleanup() {
95
127
  this.cleanupSubscriptions();
96
128
  this.cleanupRenderedComponent();
97
129
  }
@@ -99,106 +131,111 @@ class DynamicInput extends BaseInputComponent {
99
131
  this.subscriptions.forEach((s) => s.unsubscribe());
100
132
  this.subscriptions = [];
101
133
  }
134
+ cleanupRenderedComponent() {
135
+ this.compRef?.destroy();
136
+ this.compRef = undefined;
137
+ this.inputInstance = undefined;
138
+ }
102
139
  render() {
103
140
  const Renderer = fieldRegistry.get(this.type);
104
- this.cleanupRenderedComponent();
105
- this.cleanupSubscriptions();
141
+ this.cleanup();
106
142
  this.host.clear();
107
143
  if (!Renderer) {
108
- const el = document.createElement('div');
109
- el.textContent = `Unknown field type: ${this.type}`;
110
- this.host.element.nativeElement.appendChild(el);
144
+ this.renderError(`Unknown field type: ${this.type}`);
111
145
  return;
112
146
  }
147
+ if (this.isComponentType(Renderer)) {
148
+ this.renderComponent(Renderer);
149
+ }
150
+ else if (typeof Renderer === 'function') {
151
+ this.renderFallback(Renderer);
152
+ }
153
+ else {
154
+ this.renderError(`Invalid renderer for type: ${this.type}`);
155
+ }
156
+ }
157
+ isComponentType(renderer) {
158
+ return (typeof renderer === 'object' && renderer !== null && 'cmp' in renderer);
159
+ }
160
+ renderComponent(compType) {
113
161
  try {
114
- const compType = Renderer;
115
162
  const compRef = this.host.createComponent(compType);
116
163
  const instance = compRef.instance;
117
164
  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);
124
- }
125
- const subB = this.bindOutput(instance, 'onValueChange');
126
- if (subB) {
127
- this.subscriptions.push(subB);
165
+ this.renderError(`Failed to create instance for ${this.type}`);
166
+ return;
128
167
  }
129
- instance.changeDetectorRef?.detectChanges();
168
+ this.applyProps(instance);
169
+ this.bindOutputs(instance);
130
170
  this.compRef = compRef;
131
171
  this.inputInstance = instance;
132
172
  compRef.changeDetectorRef.detectChanges();
133
173
  }
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) {
174
+ catch {
175
+ this.renderError(`Failed to render field: ${this.type}`);
176
+ }
177
+ }
178
+ renderFallback(renderer) {
179
+ try {
180
+ const props = this.getFallbackProps();
181
+ const result = renderer(props);
182
+ if (typeof result === 'string') {
157
183
  const el = document.createElement('div');
158
- el.textContent = `Failed to render field: ${this.type}`;
184
+ el.innerHTML = result;
159
185
  this.host.element.nativeElement.appendChild(el);
160
186
  }
161
187
  }
188
+ catch {
189
+ this.renderError(`Failed to render field: ${this.type}`);
190
+ }
162
191
  }
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];
192
+ getFallbackProps() {
193
+ return {
194
+ value: this.value,
195
+ onValueChange: (v) => this.emitValue(v),
196
+ label: this.label ?? '',
197
+ placeholder: this.placeholder ?? '',
198
+ required: this.required ?? false,
199
+ options: this.options ?? [],
200
+ className: this.className ?? '',
201
+ description: this.description ?? '',
202
+ disabled: this.disabled ?? false,
203
+ errorMessage: this.errorMessage ?? '',
204
+ };
205
+ }
206
+ applyProps(instance) {
207
+ const instanceObj = instance;
208
+ for (const prop of KNOWN_PROPS) {
209
+ if (prop in this && prop in instanceObj) {
210
+ instanceObj[prop] = this[prop];
178
211
  }
179
212
  }
180
213
  }
181
- bindOutput(instance, outputName) {
182
- const output = instance?.[outputName];
183
- if (!output || typeof output.subscribe !== 'function') {
184
- return;
214
+ bindOutputs(instance) {
215
+ const outputNames = [
216
+ 'valueChange',
217
+ 'onValueChange',
218
+ ];
219
+ for (const outputName of outputNames) {
220
+ const output = instance[outputName];
221
+ if (output && typeof output === 'object' && 'subscribe' in output) {
222
+ const sub = output.subscribe((value) => this.emitValue(value));
223
+ this.subscriptions.push(sub);
224
+ }
185
225
  }
186
- const sub = output.subscribe((value) => {
187
- this.emitValue(value);
188
- });
189
- return sub;
190
226
  }
191
227
  emitValue(value) {
192
228
  this.valueChange.emit(value);
193
229
  this.onChange.emit(value);
194
230
  }
195
- cleanupRenderedComponent() {
196
- this.compRef?.destroy();
197
- this.compRef = undefined;
198
- this.inputInstance = undefined;
231
+ renderError(message) {
232
+ const el = document.createElement('div');
233
+ el.textContent = message;
234
+ el.style.color = 'red';
235
+ this.host.element.nativeElement.appendChild(el);
199
236
  }
200
237
  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 }] });
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 });
202
239
  }
203
240
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: DynamicInput, decorators: [{
204
241
  type: Component,
@@ -206,6 +243,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
206
243
  selector: 'dfk-dynamic-input',
207
244
  standalone: true,
208
245
  imports: [CommonModule],
246
+ changeDetection: ChangeDetectionStrategy.OnPush,
209
247
  template: `<div #host style="display: contents;"></div>`,
210
248
  }]
211
249
  }], propDecorators: { type: [{
@@ -220,29 +258,37 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
220
258
  }] } });
221
259
 
222
260
  class FieldInput {
261
+ cdr;
223
262
  fieldDescription;
224
- renderInfos;
263
+ value;
225
264
  onValueChangeField = new EventEmitter();
265
+ shouldRender = false;
266
+ constructor(cdr) {
267
+ this.cdr = cdr;
268
+ }
269
+ ngOnChanges(_changes) {
270
+ this.shouldRender = !!this.fieldDescription;
271
+ this.cdr.markForCheck();
272
+ }
226
273
  getFieldValue() {
227
- if (!this.fieldDescription || !this.renderInfos) {
274
+ if (!this.fieldDescription) {
228
275
  return undefined;
229
276
  }
230
- const value = this.renderInfos[this.fieldDescription.name];
231
- if (value === undefined && this.fieldDescription.type === 'text') {
277
+ if (this.value === undefined && this.fieldDescription.type === 'text') {
232
278
  return '';
233
279
  }
234
- return value;
280
+ return this.value;
235
281
  }
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: `
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: `
238
284
  <dfk-dynamic-input
239
- *ngIf="fieldDescription && renderInfos"
285
+ *ngIf="shouldRender"
240
286
  [type]="fieldDescription!.type"
241
287
  [value]="getFieldValue()"
242
288
  [label]="fieldDescription!.label"
243
289
  [placeholder]="fieldDescription!.placeholder"
244
290
  [required]="fieldDescription!.required"
245
- [description]="fieldDescription!.description"
291
+ [description]="$any(fieldDescription!.description)"
246
292
  [options]="fieldDescription!.options"
247
293
  [className]="fieldDescription!.className"
248
294
  (valueChange)="
@@ -251,7 +297,7 @@ class FieldInput {
251
297
  [disabled]="false"
252
298
  [errorMessage]="''"
253
299
  ></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"] }] });
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 });
255
301
  }
256
302
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: FieldInput, decorators: [{
257
303
  type: Component,
@@ -259,15 +305,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
259
305
  selector: 'dfk-field-input',
260
306
  standalone: true,
261
307
  imports: [NgIf, DynamicInput],
308
+ changeDetection: ChangeDetectionStrategy.OnPush,
262
309
  template: `
263
310
  <dfk-dynamic-input
264
- *ngIf="fieldDescription && renderInfos"
311
+ *ngIf="shouldRender"
265
312
  [type]="fieldDescription!.type"
266
313
  [value]="getFieldValue()"
267
314
  [label]="fieldDescription!.label"
268
315
  [placeholder]="fieldDescription!.placeholder"
269
316
  [required]="fieldDescription!.required"
270
- [description]="fieldDescription!.description"
317
+ [description]="$any(fieldDescription!.description)"
271
318
  [options]="fieldDescription!.options"
272
319
  [className]="fieldDescription!.className"
273
320
  (valueChange)="
@@ -278,33 +325,83 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
278
325
  ></dfk-dynamic-input>
279
326
  `,
280
327
  }]
281
- }], propDecorators: { fieldDescription: [{
328
+ }], ctorParameters: () => [{ type: i0.ChangeDetectorRef }], propDecorators: { fieldDescription: [{
282
329
  type: Input
283
- }], renderInfos: [{
330
+ }], value: [{
284
331
  type: Input
285
332
  }], onValueChangeField: [{
286
333
  type: Output
287
334
  }] } });
288
335
 
336
+ const DEFAULT_BREAKPOINT = 768;
289
337
  class MultiFieldInput {
338
+ cdr;
290
339
  fieldDescriptions = [];
291
340
  properties;
292
341
  onChange = new EventEmitter();
293
342
  layout = 'column';
294
343
  data = {};
295
344
  visibleFields = [];
345
+ isMobile = false;
346
+ constructor(cdr) {
347
+ this.cdr = cdr;
348
+ }
349
+ onWindowResize() {
350
+ const wasMobile = this.isMobile;
351
+ this.updateIsMobile();
352
+ if (wasMobile !== this.isMobile) {
353
+ this.cdr.markForCheck();
354
+ }
355
+ }
356
+ get resolvedLayout() {
357
+ if (typeof this.layout === 'object' && this.layout.type === 'responsive') {
358
+ return this.isMobile ? this.layout.mobile : this.layout.desktop;
359
+ }
360
+ return this.layout;
361
+ }
362
+ get resolvedLayoutType() {
363
+ const resolved = this.resolvedLayout;
364
+ return typeof resolved === 'string' ? resolved : resolved.type;
365
+ }
366
+ get gap() {
367
+ const resolved = this.resolvedLayout;
368
+ if (typeof resolved === 'object' && resolved.gap !== undefined) {
369
+ return resolved.gap;
370
+ }
371
+ return 12;
372
+ }
373
+ get columns() {
374
+ const resolved = this.resolvedLayout;
375
+ if (typeof resolved === 'object' && resolved.type === 'grid') {
376
+ return resolved.columns ?? 2;
377
+ }
378
+ return 2;
379
+ }
296
380
  trackByFn(index, field) {
297
381
  return field.name || index;
298
382
  }
383
+ trackByIndex(index) {
384
+ return index;
385
+ }
299
386
  ngOnInit() {
387
+ this.updateIsMobile();
300
388
  this.init();
301
389
  }
302
390
  ngOnChanges(_changes) {
303
391
  this.init();
304
392
  }
393
+ updateIsMobile() {
394
+ const breakpoint = typeof this.layout === 'object' && this.layout.type === 'responsive'
395
+ ? this.layout.breakpoint ?? DEFAULT_BREAKPOINT
396
+ : DEFAULT_BREAKPOINT;
397
+ this.isMobile =
398
+ typeof window !== 'undefined' && window.innerWidth < breakpoint;
399
+ }
305
400
  init() {
306
401
  if (this.properties) {
307
- this.data = { ...this.properties };
402
+ this.data = applyComputedValues(this.fieldDescriptions, {
403
+ ...this.properties,
404
+ });
308
405
  }
309
406
  this.updateVisibleFields();
310
407
  }
@@ -312,52 +409,177 @@ class MultiFieldInput {
312
409
  this.visibleFields = this.fieldDescriptions.filter((f) => !f.appearCondition || f.appearCondition(this.data));
313
410
  }
314
411
  onFieldChange(event) {
315
- this.data = { ...this.data, [event.key]: event.value };
412
+ this.commitData({ ...this.data, [event.key]: event.value });
413
+ }
414
+ getItems(field) {
415
+ const value = this.data[field.name];
416
+ return Array.isArray(value) ? value : [];
417
+ }
418
+ canAddItem(field) {
419
+ return canAddGroupItem(field, this.getItems(field));
420
+ }
421
+ canRemoveItem(field) {
422
+ return canRemoveGroupItem(field, this.getItems(field));
423
+ }
424
+ onGroupItemAdd(field) {
425
+ if (!this.canAddItem(field)) {
426
+ return;
427
+ }
428
+ const items = this.getItems(field);
429
+ this.commitData({
430
+ ...this.data,
431
+ [field.name]: [...items, createGroupItem(field)],
432
+ });
433
+ }
434
+ onGroupItemRemove(field, index) {
435
+ if (!this.canRemoveItem(field)) {
436
+ return;
437
+ }
438
+ const items = this.getItems(field).filter((_, i) => i !== index);
439
+ this.commitData({ ...this.data, [field.name]: items });
440
+ }
441
+ onGroupItemChange(field, index, next) {
442
+ const items = this.getItems(field).slice();
443
+ items[index] = next;
444
+ this.commitData({ ...this.data, [field.name]: items });
445
+ }
446
+ commitData(nextData) {
447
+ this.data = applyComputedValues(this.fieldDescriptions, nextData);
316
448
  this.updateVisibleFields();
317
449
  this.onChange.emit(this.data);
450
+ this.cdr.markForCheck();
318
451
  }
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: `
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: `
321
454
  <div
322
- class="flex flex-col gap-3 p-4 border rounded-lg bg-gray-50"
455
+ class="p-4 border rounded-lg bg-gray-50"
323
456
  [ngClass]="{
324
- 'flex-row gap-3': layout === 'row',
325
- 'grid grid-cols-2 gap-3': layout === 'grid'
457
+ 'flex flex-col': resolvedLayoutType === 'column',
458
+ 'flex flex-row': resolvedLayoutType === 'row',
459
+ grid: resolvedLayoutType === 'grid'
326
460
  }"
461
+ [style.gap.px]="gap"
462
+ [style.gridTemplateColumns]="
463
+ resolvedLayoutType === 'grid' ? 'repeat(' + columns + ', 1fr)' : null
464
+ "
327
465
  >
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>
466
+ <ng-container *ngFor="let field of visibleFields; trackBy: trackByFn">
467
+ <dfk-field-input
468
+ *ngIf="!field.fields"
469
+ [fieldDescription]="field"
470
+ [value]="data[field.name]"
471
+ (onValueChangeField)="onFieldChange($event)"
472
+ ></dfk-field-input>
473
+
474
+ <div *ngIf="field.fields" [class]="field.className">
475
+ <div *ngIf="field.label">{{ field.label }}</div>
476
+ <div
477
+ *ngFor="
478
+ let item of getItems(field);
479
+ let i = index;
480
+ trackBy: trackByIndex
481
+ "
482
+ style="display: flex; align-items: flex-start; gap: 8px;"
483
+ >
484
+ <div style="flex: 1">
485
+ <dfk-multi-field-input
486
+ [fieldDescriptions]="field.fields"
487
+ [properties]="item"
488
+ (onChange)="onGroupItemChange(field, i, $event)"
489
+ ></dfk-multi-field-input>
490
+ </div>
491
+ <button
492
+ type="button"
493
+ (click)="onGroupItemRemove(field, i)"
494
+ [disabled]="!canRemoveItem(field)"
495
+ >
496
+ {{ field.removeLabel || 'Remove' }}
497
+ </button>
498
+ </div>
499
+ <button
500
+ type="button"
501
+ (click)="onGroupItemAdd(field)"
502
+ [disabled]="!canAddItem(field)"
503
+ >
504
+ {{ field.addLabel || 'Add' }}
505
+ </button>
506
+ </div>
507
+ </ng-container>
334
508
  </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"] }] });
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 });
336
510
  }
337
511
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: MultiFieldInput, decorators: [{
338
512
  type: Component,
339
513
  args: [{
340
514
  selector: 'dfk-multi-field-input',
341
515
  standalone: true,
342
- imports: [NgClass, NgFor, FieldInput],
516
+ // Repeatable field groups render a nested <dfk-multi-field-input> per item
517
+ // (see the #groupTpl below), so this component imports itself. That's the
518
+ // supported way to give a standalone component recursive template usage
519
+ // without a cross-file circular import between FieldInput and a separate
520
+ // group component (whose `imports` arrays are evaluated eagerly at module
521
+ // load time and would deadlock on an actual circular module reference).
522
+ imports: [NgClass, NgFor, NgIf, FieldInput, MultiFieldInput],
523
+ changeDetection: ChangeDetectionStrategy.OnPush,
343
524
  template: `
344
525
  <div
345
- class="flex flex-col gap-3 p-4 border rounded-lg bg-gray-50"
526
+ class="p-4 border rounded-lg bg-gray-50"
346
527
  [ngClass]="{
347
- 'flex-row gap-3': layout === 'row',
348
- 'grid grid-cols-2 gap-3': layout === 'grid'
528
+ 'flex flex-col': resolvedLayoutType === 'column',
529
+ 'flex flex-row': resolvedLayoutType === 'row',
530
+ grid: resolvedLayoutType === 'grid'
349
531
  }"
532
+ [style.gap.px]="gap"
533
+ [style.gridTemplateColumns]="
534
+ resolvedLayoutType === 'grid' ? 'repeat(' + columns + ', 1fr)' : null
535
+ "
350
536
  >
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>
537
+ <ng-container *ngFor="let field of visibleFields; trackBy: trackByFn">
538
+ <dfk-field-input
539
+ *ngIf="!field.fields"
540
+ [fieldDescription]="field"
541
+ [value]="data[field.name]"
542
+ (onValueChangeField)="onFieldChange($event)"
543
+ ></dfk-field-input>
544
+
545
+ <div *ngIf="field.fields" [class]="field.className">
546
+ <div *ngIf="field.label">{{ field.label }}</div>
547
+ <div
548
+ *ngFor="
549
+ let item of getItems(field);
550
+ let i = index;
551
+ trackBy: trackByIndex
552
+ "
553
+ style="display: flex; align-items: flex-start; gap: 8px;"
554
+ >
555
+ <div style="flex: 1">
556
+ <dfk-multi-field-input
557
+ [fieldDescriptions]="field.fields"
558
+ [properties]="item"
559
+ (onChange)="onGroupItemChange(field, i, $event)"
560
+ ></dfk-multi-field-input>
561
+ </div>
562
+ <button
563
+ type="button"
564
+ (click)="onGroupItemRemove(field, i)"
565
+ [disabled]="!canRemoveItem(field)"
566
+ >
567
+ {{ field.removeLabel || 'Remove' }}
568
+ </button>
569
+ </div>
570
+ <button
571
+ type="button"
572
+ (click)="onGroupItemAdd(field)"
573
+ [disabled]="!canAddItem(field)"
574
+ >
575
+ {{ field.addLabel || 'Add' }}
576
+ </button>
577
+ </div>
578
+ </ng-container>
357
579
  </div>
358
580
  `,
359
581
  }]
360
- }], propDecorators: { fieldDescriptions: [{
582
+ }], ctorParameters: () => [{ type: i0.ChangeDetectorRef }], propDecorators: { fieldDescriptions: [{
361
583
  type: Input
362
584
  }], properties: [{
363
585
  type: Input
@@ -365,15 +587,39 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
365
587
  type: Output
366
588
  }], layout: [{
367
589
  type: Input
590
+ }], onWindowResize: [{
591
+ type: HostListener,
592
+ args: ['window:resize']
368
593
  }] } });
369
594
 
595
+ class LayoutRegistry {
596
+ layouts = new Map();
597
+ register(type, component) {
598
+ if (this.layouts.has(type)) {
599
+ console.warn(`[dynamic-field-kit] Layout "${type}" already exists`);
600
+ }
601
+ this.layouts.set(type, component);
602
+ }
603
+ get(type) {
604
+ return this.layouts.get(type);
605
+ }
606
+ }
607
+ const layoutRegistry = new LayoutRegistry();
608
+
370
609
  class ColumnLayout {
610
+ config;
611
+ template;
612
+ get gap() {
613
+ return this.config?.gap ?? 12;
614
+ }
371
615
  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)"
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
617
+ [style.display]="'flex'"
618
+ [style.flexDirection]="'column'"
619
+ [style.gap]="gap + 'px'"
374
620
  >
375
- <ng-content></ng-content>
376
- </div>`, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }] });
621
+ <ng-container *ngTemplateOutlet="template"></ng-container>
622
+ </div>`, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] });
377
623
  }
378
624
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: ColumnLayout, decorators: [{
379
625
  type: Component,
@@ -382,19 +628,32 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
382
628
  standalone: true,
383
629
  imports: [CommonModule],
384
630
  template: `<div
385
- style="display:flex;flex-direction:column;gap:var(--dfk-gap,12px)"
631
+ [style.display]="'flex'"
632
+ [style.flexDirection]="'column'"
633
+ [style.gap]="gap + 'px'"
386
634
  >
387
- <ng-content></ng-content>
635
+ <ng-container *ngTemplateOutlet="template"></ng-container>
388
636
  </div>`,
389
637
  }]
390
- }] });
638
+ }], propDecorators: { config: [{
639
+ type: Input
640
+ }], template: [{
641
+ type: Input
642
+ }] } });
391
643
  class RowLayout {
644
+ config;
645
+ template;
646
+ get gap() {
647
+ return this.config?.gap ?? 12;
648
+ }
392
649
  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)"
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
651
+ [style.display]="'flex'"
652
+ [style.flexDirection]="'row'"
653
+ [style.gap]="gap + 'px'"
395
654
  >
396
- <ng-content></ng-content>
397
- </div>`, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }] });
655
+ <ng-container *ngTemplateOutlet="template"></ng-container>
656
+ </div>`, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] });
398
657
  }
399
658
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: RowLayout, decorators: [{
400
659
  type: Component,
@@ -403,19 +662,35 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
403
662
  standalone: true,
404
663
  imports: [CommonModule],
405
664
  template: `<div
406
- style="display:flex;flex-direction:row;gap:var(--dfk-gap,12px)"
665
+ [style.display]="'flex'"
666
+ [style.flexDirection]="'row'"
667
+ [style.gap]="gap + 'px'"
407
668
  >
408
- <ng-content></ng-content>
669
+ <ng-container *ngTemplateOutlet="template"></ng-container>
409
670
  </div>`,
410
671
  }]
411
- }] });
672
+ }], propDecorators: { config: [{
673
+ type: Input
674
+ }], template: [{
675
+ type: Input
676
+ }] } });
412
677
  class GridLayout {
678
+ config;
679
+ template;
680
+ get columns() {
681
+ return this.config?.columns ?? 2;
682
+ }
683
+ get gap() {
684
+ return this.config?.gap ?? 12;
685
+ }
413
686
  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)"
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
688
+ [style.display]="'grid'"
689
+ [style.gridTemplateColumns]="'repeat(' + columns + ', 1fr)'"
690
+ [style.gap]="gap + 'px'"
416
691
  >
417
- <ng-content></ng-content>
418
- </div>`, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }] });
692
+ <ng-container *ngTemplateOutlet="template"></ng-container>
693
+ </div>`, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] });
419
694
  }
420
695
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: GridLayout, decorators: [{
421
696
  type: Component,
@@ -424,12 +699,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
424
699
  standalone: true,
425
700
  imports: [CommonModule],
426
701
  template: `<div
427
- style="display:grid;grid-template-columns:repeat(var(--dfk-columns,2),1fr);gap:var(--dfk-gap,12px)"
702
+ [style.display]="'grid'"
703
+ [style.gridTemplateColumns]="'repeat(' + columns + ', 1fr)'"
704
+ [style.gap]="gap + 'px'"
428
705
  >
429
- <ng-content></ng-content>
706
+ <ng-container *ngTemplateOutlet="template"></ng-container>
430
707
  </div>`,
431
708
  }]
432
- }] });
709
+ }], propDecorators: { config: [{
710
+ type: Input
711
+ }], template: [{
712
+ type: Input
713
+ }] } });
714
+ layoutRegistry.register('column', ColumnLayout);
715
+ layoutRegistry.register('row', RowLayout);
716
+ layoutRegistry.register('grid', GridLayout);
433
717
 
434
718
  class DynamicFieldKitModule {
435
719
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: DynamicFieldKitModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
@@ -483,5 +767,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
483
767
  * Generated bundle index. Do not edit.
484
768
  */
485
769
 
486
- export { BaseInputComponent, ColumnLayout, DynamicFieldKitModule, DynamicInput, FieldInput, GridLayout, MultiFieldInput, RowLayout };
770
+ export { BaseInputComponent, ColumnLayout, DynamicFieldKitModule, DynamicInput, FieldInput, GridLayout, LayoutRegistry, MultiFieldInput, RowLayout, layoutRegistry };
487
771
  //# sourceMappingURL=dynamic-field-kit-angular.mjs.map