@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,27 +1,44 @@
1
1
  import { CommonModule } from '@angular/common';
2
2
  import {
3
+ AfterViewInit,
4
+ ChangeDetectionStrategy,
3
5
  Component,
4
6
  ComponentRef,
5
- Input,
6
- Output,
7
7
  EventEmitter,
8
- ViewChild,
9
- ViewContainerRef,
8
+ Input,
10
9
  OnChanges,
11
- AfterViewInit,
12
10
  OnDestroy,
11
+ Output,
13
12
  SimpleChanges,
14
13
  Type,
15
- SimpleChange,
14
+ ViewChild,
15
+ ViewContainerRef,
16
16
  } from '@angular/core';
17
17
  import { fieldRegistry, FieldTypeKey } from '@dynamic-field-kit/core';
18
18
  import { Subscription } from 'rxjs';
19
19
  import { BaseInputComponent } from './BaseInput';
20
20
 
21
+ const KNOWN_PROPS = [
22
+ 'value',
23
+ 'label',
24
+ 'placeholder',
25
+ 'required',
26
+ 'disabled',
27
+ 'options',
28
+ 'className',
29
+ 'description',
30
+ 'errorMessage',
31
+ 'acceptFile',
32
+ 'maxLength',
33
+ 'minNumber',
34
+ 'maxNumber',
35
+ ] as const;
36
+
21
37
  @Component({
22
38
  selector: 'dfk-dynamic-input',
23
39
  standalone: true,
24
40
  imports: [CommonModule],
41
+ changeDetection: ChangeDetectionStrategy.OnPush,
25
42
  template: `<div #host style="display: contents;"></div>`,
26
43
  })
27
44
  export class DynamicInput
@@ -29,160 +46,179 @@ export class DynamicInput
29
46
  implements OnChanges, AfterViewInit, OnDestroy
30
47
  {
31
48
  @Input() type!: FieldTypeKey;
32
- @Output() override valueChange = new EventEmitter<any>();
33
- @Output() onChange = new EventEmitter<any>(); // backward compat
49
+ @Output() override valueChange = new EventEmitter<unknown>();
50
+ @Output() onChange = new EventEmitter<unknown>();
34
51
 
35
52
  @ViewChild('host', { read: ViewContainerRef, static: false })
36
53
  host!: ViewContainerRef;
37
- private compRef?: ComponentRef<any>;
38
- private inputInstance?: any;
54
+ private compRef?: ComponentRef<unknown>;
55
+ private inputInstance?: unknown;
39
56
  private subscriptions: Subscription[] = [];
40
57
 
41
- ngOnChanges(changes: SimpleChanges) {
42
- if (changes['type'] && !changes['type'].firstChange && this.host) {
43
- this.render();
44
- return;
45
- }
58
+ ngOnChanges(changes: SimpleChanges): void {
59
+ super.ngOnChanges(changes);
46
60
 
47
61
  if (this.inputInstance) {
48
- this.applyKnownProps(this.inputInstance);
49
-
50
- const childChanges: SimpleChanges = {};
51
- for (const prop in changes) {
52
- childChanges[prop] = new SimpleChange(
53
- changes[prop].previousValue,
54
- changes[prop].currentValue,
55
- changes[prop].firstChange
56
- );
57
- }
62
+ this.syncPropsToInstance(changes);
63
+ }
58
64
 
59
- this.inputInstance.ngOnChanges?.(childChanges);
60
- this.compRef?.changeDetectorRef.detectChanges();
61
- } else if (this.host) {
65
+ if (changes['type'] && !changes['type'].firstChange && this.host) {
66
+ this.render();
67
+ } else if (!this.inputInstance && this.host) {
62
68
  this.render();
63
69
  }
64
70
  }
65
71
 
66
- ngAfterViewInit() {
72
+ ngAfterViewInit(): void {
67
73
  this.render();
68
74
  }
69
75
 
70
- ngOnDestroy() {
76
+ ngOnDestroy(): void {
77
+ this.cleanup();
78
+ }
79
+
80
+ private syncPropsToInstance(changes: SimpleChanges): void {
81
+ if (!this.inputInstance) {
82
+ return;
83
+ }
84
+
85
+ for (const prop of KNOWN_PROPS) {
86
+ if (changes[prop] && this.inputInstance) {
87
+ (this.inputInstance as Record<string, unknown>)[prop] = (
88
+ this as Record<string, unknown>
89
+ )[prop];
90
+ }
91
+ }
92
+ this.compRef?.changeDetectorRef?.detectChanges();
93
+ }
94
+
95
+ private cleanup(): void {
71
96
  this.cleanupSubscriptions();
72
97
  this.cleanupRenderedComponent();
73
98
  }
74
99
 
75
- private cleanupSubscriptions() {
100
+ private cleanupSubscriptions(): void {
76
101
  this.subscriptions.forEach((s) => s.unsubscribe());
77
102
  this.subscriptions = [];
78
103
  }
79
104
 
80
- private render() {
81
- const Renderer = fieldRegistry.get(this.type as FieldTypeKey);
82
- this.cleanupRenderedComponent();
83
- this.cleanupSubscriptions();
105
+ private cleanupRenderedComponent(): void {
106
+ this.compRef?.destroy();
107
+ this.compRef = undefined;
108
+ this.inputInstance = undefined;
109
+ }
110
+
111
+ private render(): void {
112
+ const Renderer = fieldRegistry.get(this.type);
113
+ this.cleanup();
84
114
  this.host.clear();
115
+
85
116
  if (!Renderer) {
86
- const el = document.createElement('div');
87
- el.textContent = `Unknown field type: ${this.type}`;
88
- this.host.element.nativeElement.appendChild(el);
117
+ this.renderError(`Unknown field type: ${this.type}`);
89
118
  return;
90
119
  }
91
120
 
121
+ if (this.isComponentType(Renderer)) {
122
+ this.renderComponent(Renderer as unknown as Type<unknown>);
123
+ } else if (typeof Renderer === 'function') {
124
+ this.renderFallback(Renderer);
125
+ } else {
126
+ this.renderError(`Invalid renderer for type: ${this.type}`);
127
+ }
128
+ }
129
+
130
+ private isComponentType(renderer: unknown): boolean {
131
+ return (
132
+ typeof renderer === 'object' && renderer !== null && 'cmp' in renderer
133
+ );
134
+ }
135
+
136
+ private renderComponent(compType: Type<unknown>): void {
92
137
  try {
93
- const compType = Renderer as unknown as Type<any>;
94
138
  const compRef = this.host.createComponent(compType);
95
139
  const instance = compRef.instance;
140
+
96
141
  if (!instance) {
97
- throw new Error(`Failed to create instance for ${this.type}`);
142
+ this.renderError(`Failed to create instance for ${this.type}`);
143
+ return;
98
144
  }
99
145
 
100
- this.applyKnownProps(instance);
146
+ this.applyProps(instance);
147
+ this.bindOutputs(instance);
101
148
 
102
- const subA = this.bindOutput(instance, 'valueChange');
103
- if (subA) {
104
- this.subscriptions.push(subA);
105
- }
106
- const subB = this.bindOutput(instance, 'onValueChange');
107
- if (subB) {
108
- this.subscriptions.push(subB);
109
- }
110
-
111
- instance.changeDetectorRef?.detectChanges();
112
149
  this.compRef = compRef;
113
150
  this.inputInstance = instance;
114
151
  compRef.changeDetectorRef.detectChanges();
115
- } catch (err) {
116
- // Fallback to function renderer
117
- try {
118
- const props: any = {
119
- value: this.value,
120
- onValueChange: (v: any) => this.emitValue(v),
121
- label: this.label,
122
- placeholder: this.placeholder,
123
- required: this.required,
124
- options: this.options,
125
- className: this.className,
126
- description: this.description,
127
- disabled: this.disabled,
128
- errorMessage: this.errorMessage,
129
- };
130
- const out = (Renderer as (props: unknown) => unknown)(props);
131
- if (typeof out === 'string') {
132
- const el = document.createElement('div');
133
- el.innerHTML = out;
134
- this.host.element.nativeElement.appendChild(el);
135
- }
136
- } catch (e) {
152
+ } catch {
153
+ this.renderError(`Failed to render field: ${this.type}`);
154
+ }
155
+ }
156
+
157
+ private renderFallback(renderer: unknown): void {
158
+ try {
159
+ const props = this.getFallbackProps();
160
+ const result = (renderer as (props: unknown) => string)(props);
161
+
162
+ if (typeof result === 'string') {
137
163
  const el = document.createElement('div');
138
- el.textContent = `Failed to render field: ${this.type}`;
164
+ el.innerHTML = result;
139
165
  this.host.element.nativeElement.appendChild(el);
140
166
  }
167
+ } catch {
168
+ this.renderError(`Failed to render field: ${this.type}`);
141
169
  }
142
170
  }
143
171
 
144
- private applyKnownProps(instance: any) {
145
- const knownProps = [
146
- 'value',
147
- 'label',
148
- 'placeholder',
149
- 'required',
150
- 'disabled',
151
- 'options',
152
- 'className',
153
- 'description',
154
- 'errorMessage',
155
- ];
156
- for (const prop of knownProps) {
157
- if (prop in this && prop in instance) {
158
- instance[prop] = (this as any)[prop];
172
+ private getFallbackProps(): Record<string, unknown> {
173
+ return {
174
+ value: this.value,
175
+ onValueChange: (v: unknown) => this.emitValue(v),
176
+ label: this.label ?? '',
177
+ placeholder: this.placeholder ?? '',
178
+ required: this.required ?? false,
179
+ options: this.options ?? [],
180
+ className: this.className ?? '',
181
+ description: this.description ?? '',
182
+ disabled: this.disabled ?? false,
183
+ errorMessage: this.errorMessage ?? '',
184
+ };
185
+ }
186
+
187
+ private applyProps(instance: unknown): void {
188
+ const instanceObj = instance as Record<string, unknown>;
189
+ for (const prop of KNOWN_PROPS) {
190
+ if (prop in this && prop in instanceObj) {
191
+ instanceObj[prop] = (this as Record<string, unknown>)[prop];
159
192
  }
160
193
  }
161
194
  }
162
195
 
163
- private bindOutput(
164
- instance: any,
165
- outputName: 'valueChange' | 'onValueChange'
166
- ): Subscription | undefined {
167
- const output = instance?.[outputName];
168
- if (!output || typeof output.subscribe !== 'function') {
169
- return;
170
- }
196
+ private bindOutputs(instance: unknown): void {
197
+ const outputNames: Array<'valueChange' | 'onValueChange'> = [
198
+ 'valueChange',
199
+ 'onValueChange',
200
+ ];
171
201
 
172
- const sub = (output as EventEmitter<any>).subscribe((value) => {
173
- this.emitValue(value);
174
- });
175
- return sub;
202
+ for (const outputName of outputNames) {
203
+ const output = (instance as Record<string, unknown>)[outputName];
204
+ if (output && typeof output === 'object' && 'subscribe' in output) {
205
+ const sub = (
206
+ output as { subscribe: (cb: (v: unknown) => void) => Subscription }
207
+ ).subscribe((value: unknown) => this.emitValue(value));
208
+ this.subscriptions.push(sub);
209
+ }
210
+ }
176
211
  }
177
212
 
178
- private emitValue(value: any) {
213
+ private emitValue(value: unknown): void {
179
214
  this.valueChange.emit(value);
180
215
  this.onChange.emit(value);
181
216
  }
182
217
 
183
- private cleanupRenderedComponent() {
184
- this.compRef?.destroy();
185
- this.compRef = undefined;
186
- this.inputInstance = undefined;
218
+ private renderError(message: string): void {
219
+ const el = document.createElement('div');
220
+ el.textContent = message;
221
+ el.style.color = 'red';
222
+ this.host.element.nativeElement.appendChild(el);
187
223
  }
188
224
  }
@@ -1,21 +1,31 @@
1
1
  import { NgIf } from '@angular/common';
2
- import { Component, Input, Output, EventEmitter } from '@angular/core';
3
- import { FieldDescription, Properties } from '@dynamic-field-kit/core';
2
+ import {
3
+ ChangeDetectionStrategy,
4
+ ChangeDetectorRef,
5
+ Component,
6
+ EventEmitter,
7
+ Input,
8
+ OnChanges,
9
+ Output,
10
+ SimpleChanges,
11
+ } from '@angular/core';
12
+ import { FieldDescription } from '@dynamic-field-kit/core';
4
13
  import { DynamicInput } from './DynamicInput';
5
14
 
6
15
  @Component({
7
16
  selector: 'dfk-field-input',
8
17
  standalone: true,
9
18
  imports: [NgIf, DynamicInput],
19
+ changeDetection: ChangeDetectionStrategy.OnPush,
10
20
  template: `
11
21
  <dfk-dynamic-input
12
- *ngIf="fieldDescription && renderInfos"
22
+ *ngIf="shouldRender"
13
23
  [type]="fieldDescription!.type"
14
24
  [value]="getFieldValue()"
15
25
  [label]="fieldDescription!.label"
16
26
  [placeholder]="fieldDescription!.placeholder"
17
27
  [required]="fieldDescription!.required"
18
- [description]="fieldDescription!.description"
28
+ [description]="$any(fieldDescription!.description)"
19
29
  [options]="fieldDescription!.options"
20
30
  [className]="fieldDescription!.className"
21
31
  (valueChange)="
@@ -26,24 +36,32 @@ import { DynamicInput } from './DynamicInput';
26
36
  ></dfk-dynamic-input>
27
37
  `,
28
38
  })
29
- export class FieldInput {
39
+ export class FieldInput implements OnChanges {
30
40
  @Input() fieldDescription?: FieldDescription;
31
- @Input() renderInfos?: Properties;
41
+ @Input() value?: unknown;
32
42
  @Output() onValueChangeField = new EventEmitter<{
33
- value: any;
43
+ value: unknown;
34
44
  key: string;
35
45
  }>();
36
46
 
37
- getFieldValue() {
38
- if (!this.fieldDescription || !this.renderInfos) {
47
+ shouldRender = false;
48
+
49
+ constructor(private cdr: ChangeDetectorRef) {}
50
+
51
+ ngOnChanges(_changes: SimpleChanges): void {
52
+ this.shouldRender = !!this.fieldDescription;
53
+ this.cdr.markForCheck();
54
+ }
55
+
56
+ getFieldValue(): unknown {
57
+ if (!this.fieldDescription) {
39
58
  return undefined;
40
59
  }
41
60
 
42
- const value = this.renderInfos[this.fieldDescription.name];
43
- if (value === undefined && this.fieldDescription.type === 'text') {
61
+ if (this.value === undefined && this.fieldDescription.type === 'text') {
44
62
  return '';
45
63
  }
46
64
 
47
- return value;
65
+ return this.value;
48
66
  }
49
67
  }
@@ -1,35 +1,95 @@
1
- import { NgClass, NgFor } from '@angular/common';
1
+ import { NgClass, NgFor, NgIf } from '@angular/common';
2
2
  import {
3
+ ChangeDetectionStrategy,
4
+ ChangeDetectorRef,
3
5
  Component,
4
- Input,
5
- Output,
6
6
  EventEmitter,
7
+ HostListener,
8
+ Input,
7
9
  OnChanges,
8
10
  OnInit,
11
+ Output,
9
12
  SimpleChanges,
10
13
  } from '@angular/core';
11
- import { FieldDescription, Properties } from '@dynamic-field-kit/core';
12
- import { LayoutConfig } from '../types/layout';
14
+ import {
15
+ applyComputedValues,
16
+ canAddGroupItem,
17
+ canRemoveGroupItem,
18
+ createGroupItem,
19
+ FieldDescription,
20
+ Properties,
21
+ } from '@dynamic-field-kit/core';
22
+ import { BaseLayoutConfig, LayoutConfig } from '../types/layout';
13
23
  import { FieldInput } from './FieldInput';
14
24
 
25
+ const DEFAULT_BREAKPOINT = 768;
26
+
15
27
  @Component({
16
28
  selector: 'dfk-multi-field-input',
17
29
  standalone: true,
18
- imports: [NgClass, NgFor, FieldInput],
30
+ // Repeatable field groups render a nested <dfk-multi-field-input> per item
31
+ // (see the #groupTpl below), so this component imports itself. That's the
32
+ // supported way to give a standalone component recursive template usage
33
+ // without a cross-file circular import between FieldInput and a separate
34
+ // group component (whose `imports` arrays are evaluated eagerly at module
35
+ // load time and would deadlock on an actual circular module reference).
36
+ imports: [NgClass, NgFor, NgIf, FieldInput, MultiFieldInput],
37
+ changeDetection: ChangeDetectionStrategy.OnPush,
19
38
  template: `
20
39
  <div
21
- class="flex flex-col gap-3 p-4 border rounded-lg bg-gray-50"
40
+ class="p-4 border rounded-lg bg-gray-50"
22
41
  [ngClass]="{
23
- 'flex-row gap-3': layout === 'row',
24
- 'grid grid-cols-2 gap-3': layout === 'grid'
42
+ 'flex flex-col': resolvedLayoutType === 'column',
43
+ 'flex flex-row': resolvedLayoutType === 'row',
44
+ grid: resolvedLayoutType === 'grid'
25
45
  }"
46
+ [style.gap.px]="gap"
47
+ [style.gridTemplateColumns]="
48
+ resolvedLayoutType === 'grid' ? 'repeat(' + columns + ', 1fr)' : null
49
+ "
26
50
  >
27
- <dfk-field-input
28
- *ngFor="let field of visibleFields; trackBy: trackByFn"
29
- [fieldDescription]="field"
30
- [renderInfos]="data"
31
- (onValueChangeField)="onFieldChange($event)"
32
- ></dfk-field-input>
51
+ <ng-container *ngFor="let field of visibleFields; trackBy: trackByFn">
52
+ <dfk-field-input
53
+ *ngIf="!field.fields"
54
+ [fieldDescription]="field"
55
+ [value]="data[field.name]"
56
+ (onValueChangeField)="onFieldChange($event)"
57
+ ></dfk-field-input>
58
+
59
+ <div *ngIf="field.fields" [class]="field.className">
60
+ <div *ngIf="field.label">{{ field.label }}</div>
61
+ <div
62
+ *ngFor="
63
+ let item of getItems(field);
64
+ let i = index;
65
+ trackBy: trackByIndex
66
+ "
67
+ style="display: flex; align-items: flex-start; gap: 8px;"
68
+ >
69
+ <div style="flex: 1">
70
+ <dfk-multi-field-input
71
+ [fieldDescriptions]="field.fields"
72
+ [properties]="item"
73
+ (onChange)="onGroupItemChange(field, i, $event)"
74
+ ></dfk-multi-field-input>
75
+ </div>
76
+ <button
77
+ type="button"
78
+ (click)="onGroupItemRemove(field, i)"
79
+ [disabled]="!canRemoveItem(field)"
80
+ >
81
+ {{ field.removeLabel || 'Remove' }}
82
+ </button>
83
+ </div>
84
+ <button
85
+ type="button"
86
+ (click)="onGroupItemAdd(field)"
87
+ [disabled]="!canAddItem(field)"
88
+ >
89
+ {{ field.addLabel || 'Add' }}
90
+ </button>
91
+ </div>
92
+ </ng-container>
33
93
  </div>
34
94
  `,
35
95
  })
@@ -41,12 +101,57 @@ export class MultiFieldInput implements OnInit, OnChanges {
41
101
 
42
102
  data: Properties = {};
43
103
  visibleFields: FieldDescription[] = [];
104
+ private isMobile = false;
105
+
106
+ constructor(private cdr: ChangeDetectorRef) {}
107
+
108
+ @HostListener('window:resize')
109
+ onWindowResize(): void {
110
+ const wasMobile = this.isMobile;
111
+ this.updateIsMobile();
112
+ if (wasMobile !== this.isMobile) {
113
+ this.cdr.markForCheck();
114
+ }
115
+ }
116
+
117
+ get resolvedLayout(): BaseLayoutConfig {
118
+ if (typeof this.layout === 'object' && this.layout.type === 'responsive') {
119
+ return this.isMobile ? this.layout.mobile : this.layout.desktop;
120
+ }
121
+ return this.layout as BaseLayoutConfig;
122
+ }
123
+
124
+ get resolvedLayoutType(): string {
125
+ const resolved = this.resolvedLayout;
126
+ return typeof resolved === 'string' ? resolved : resolved.type;
127
+ }
128
+
129
+ get gap(): number {
130
+ const resolved = this.resolvedLayout;
131
+ if (typeof resolved === 'object' && resolved.gap !== undefined) {
132
+ return resolved.gap;
133
+ }
134
+ return 12;
135
+ }
136
+
137
+ get columns(): number {
138
+ const resolved = this.resolvedLayout;
139
+ if (typeof resolved === 'object' && resolved.type === 'grid') {
140
+ return resolved.columns ?? 2;
141
+ }
142
+ return 2;
143
+ }
44
144
 
45
145
  trackByFn(index: number, field: FieldDescription): string | number {
46
146
  return field.name || index;
47
147
  }
48
148
 
149
+ trackByIndex(index: number): number {
150
+ return index;
151
+ }
152
+
49
153
  ngOnInit() {
154
+ this.updateIsMobile();
50
155
  this.init();
51
156
  }
52
157
 
@@ -54,9 +159,20 @@ export class MultiFieldInput implements OnInit, OnChanges {
54
159
  this.init();
55
160
  }
56
161
 
162
+ private updateIsMobile(): void {
163
+ const breakpoint =
164
+ typeof this.layout === 'object' && this.layout.type === 'responsive'
165
+ ? this.layout.breakpoint ?? DEFAULT_BREAKPOINT
166
+ : DEFAULT_BREAKPOINT;
167
+ this.isMobile =
168
+ typeof window !== 'undefined' && window.innerWidth < breakpoint;
169
+ }
170
+
57
171
  private init() {
58
172
  if (this.properties) {
59
- this.data = { ...this.properties };
173
+ this.data = applyComputedValues(this.fieldDescriptions, {
174
+ ...this.properties,
175
+ });
60
176
  }
61
177
  this.updateVisibleFields();
62
178
  }
@@ -67,9 +183,56 @@ export class MultiFieldInput implements OnInit, OnChanges {
67
183
  );
68
184
  }
69
185
 
70
- onFieldChange(event: { value: any; key: string }) {
71
- this.data = { ...this.data, [event.key]: event.value };
186
+ onFieldChange(event: { value: unknown; key: string }): void {
187
+ this.commitData({ ...this.data, [event.key]: event.value });
188
+ }
189
+
190
+ getItems(field: FieldDescription): Properties[] {
191
+ const value = this.data[field.name];
192
+ return Array.isArray(value) ? (value as Properties[]) : [];
193
+ }
194
+
195
+ canAddItem(field: FieldDescription): boolean {
196
+ return canAddGroupItem(field, this.getItems(field));
197
+ }
198
+
199
+ canRemoveItem(field: FieldDescription): boolean {
200
+ return canRemoveGroupItem(field, this.getItems(field));
201
+ }
202
+
203
+ onGroupItemAdd(field: FieldDescription): void {
204
+ if (!this.canAddItem(field)) {
205
+ return;
206
+ }
207
+ const items = this.getItems(field);
208
+ this.commitData({
209
+ ...this.data,
210
+ [field.name]: [...items, createGroupItem(field)],
211
+ });
212
+ }
213
+
214
+ onGroupItemRemove(field: FieldDescription, index: number): void {
215
+ if (!this.canRemoveItem(field)) {
216
+ return;
217
+ }
218
+ const items = this.getItems(field).filter((_, i) => i !== index);
219
+ this.commitData({ ...this.data, [field.name]: items });
220
+ }
221
+
222
+ onGroupItemChange(
223
+ field: FieldDescription,
224
+ index: number,
225
+ next: Properties
226
+ ): void {
227
+ const items = this.getItems(field).slice();
228
+ items[index] = next;
229
+ this.commitData({ ...this.data, [field.name]: items });
230
+ }
231
+
232
+ private commitData(nextData: Properties): void {
233
+ this.data = applyComputedValues(this.fieldDescriptions, nextData);
72
234
  this.updateVisibleFields();
73
235
  this.onChange.emit(this.data);
236
+ this.cdr.markForCheck();
74
237
  }
75
238
  }