@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.
- package/README.md +63 -1
- package/angular.json +27 -0
- package/dist/README.md +63 -1
- package/dist/components/BaseInput.d.ts +23 -5
- package/dist/components/DynamicInput.d.ts +13 -6
- package/dist/components/FieldInput.d.ts +10 -6
- package/dist/components/MultiFieldInput.d.ts +20 -3
- package/dist/esm2022/components/BaseInput.mjs +11 -3
- package/dist/esm2022/components/DynamicInput.mjs +112 -83
- package/dist/esm2022/components/FieldInput.mjs +25 -16
- package/dist/esm2022/components/MultiFieldInput.mjs +207 -28
- package/dist/esm2022/layout/defaultLayouts.mjs +70 -23
- package/dist/esm2022/layout/index.mjs +2 -1
- package/dist/esm2022/layout/layoutRegistry.mjs +14 -0
- package/dist/esm2022/public-api.mjs +1 -1
- package/dist/esm2022/types/layout.mjs +1 -1
- package/dist/fesm2022/dynamic-field-kit-angular.mjs +430 -146
- package/dist/fesm2022/dynamic-field-kit-angular.mjs.map +1 -1
- package/dist/layout/defaultLayouts.d.ts +21 -3
- package/dist/layout/index.d.ts +1 -0
- package/dist/layout/layoutRegistry.d.ts +12 -0
- package/dist/public-api.d.ts +1 -1
- package/dist/types/layout.d.ts +25 -1
- package/karma.conf.js +37 -0
- package/package.json +18 -5
- package/src/components/BaseInput.ts +34 -5
- package/src/components/DynamicInput.ts +142 -106
- package/src/components/FieldInput.ts +30 -12
- package/src/components/MultiFieldInput.ts +181 -18
- package/src/layout/defaultLayouts.ts +42 -10
- package/src/layout/index.ts +1 -0
- package/src/layout/layoutRegistry.ts +25 -0
- package/src/public-api.ts +1 -1
- package/src/types/layout.ts +36 -1
- package/test/DynamicInput.spec.ts +30 -0
- package/test/FieldInput.spec.ts +44 -0
- package/test/angular.spec.ts +102 -0
- package/test/integration.spec.ts +57 -0
- package/test/layouts.spec.ts +26 -0
- package/test.ts +12 -0
- package/tsconfig.spec.json +21 -0
- package/src/adapters/tailwind.ts +0 -13
- package/src/components/imports.ts +0 -3
- package/src/examples/index.ts +0 -4
- package/src/examples/registerExample.ts +0 -7
- package/src/examples/text-field.component.ts +0 -18
- 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
|
-
|
|
9
|
-
ViewContainerRef,
|
|
8
|
+
Input,
|
|
10
9
|
OnChanges,
|
|
11
|
-
AfterViewInit,
|
|
12
10
|
OnDestroy,
|
|
11
|
+
Output,
|
|
13
12
|
SimpleChanges,
|
|
14
13
|
Type,
|
|
15
|
-
|
|
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<
|
|
33
|
-
@Output() onChange = new EventEmitter<
|
|
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<
|
|
38
|
-
private inputInstance?:
|
|
54
|
+
private compRef?: ComponentRef<unknown>;
|
|
55
|
+
private inputInstance?: unknown;
|
|
39
56
|
private subscriptions: Subscription[] = [];
|
|
40
57
|
|
|
41
|
-
ngOnChanges(changes: SimpleChanges) {
|
|
42
|
-
|
|
43
|
-
this.render();
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
58
|
+
ngOnChanges(changes: SimpleChanges): void {
|
|
59
|
+
super.ngOnChanges(changes);
|
|
46
60
|
|
|
47
61
|
if (this.inputInstance) {
|
|
48
|
-
this.
|
|
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
|
-
|
|
60
|
-
this.
|
|
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
|
|
81
|
-
|
|
82
|
-
this.
|
|
83
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
142
|
+
this.renderError(`Failed to create instance for ${this.type}`);
|
|
143
|
+
return;
|
|
98
144
|
}
|
|
99
145
|
|
|
100
|
-
this.
|
|
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
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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.
|
|
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
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
'
|
|
149
|
-
'
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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:
|
|
213
|
+
private emitValue(value: unknown): void {
|
|
179
214
|
this.valueChange.emit(value);
|
|
180
215
|
this.onChange.emit(value);
|
|
181
216
|
}
|
|
182
217
|
|
|
183
|
-
private
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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 {
|
|
3
|
-
|
|
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="
|
|
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()
|
|
41
|
+
@Input() value?: unknown;
|
|
32
42
|
@Output() onValueChangeField = new EventEmitter<{
|
|
33
|
-
value:
|
|
43
|
+
value: unknown;
|
|
34
44
|
key: string;
|
|
35
45
|
}>();
|
|
36
46
|
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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 {
|
|
12
|
-
|
|
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
|
-
|
|
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="
|
|
40
|
+
class="p-4 border rounded-lg bg-gray-50"
|
|
22
41
|
[ngClass]="{
|
|
23
|
-
'flex
|
|
24
|
-
'
|
|
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
|
-
<
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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 =
|
|
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:
|
|
71
|
-
this.
|
|
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
|
}
|