@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.
- package/CHANGELOG.md +19 -0
- package/README.md +113 -4
- package/dist/README.md +113 -4
- package/dist/components/BaseInput.d.ts +23 -11
- package/dist/components/DynamicInput.d.ts +19 -8
- package/dist/components/FieldInput.d.ts +15 -7
- package/dist/components/MultiFieldInput.d.ts +30 -4
- package/dist/esm2022/components/BaseInput.mjs +17 -18
- package/dist/esm2022/components/DynamicInput.mjs +166 -84
- package/dist/esm2022/components/FieldInput.mjs +54 -26
- package/dist/esm2022/components/MultiFieldInput.mjs +282 -29
- package/dist/esm2022/fieldRegistryToken.mjs +12 -0
- 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 +6 -2
- package/dist/esm2022/types/layout.mjs +1 -1
- package/dist/fesm2022/dynamic-field-kit-angular.mjs +604 -173
- package/dist/fesm2022/dynamic-field-kit-angular.mjs.map +1 -1
- package/dist/fieldRegistryToken.d.ts +3 -0
- 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 +5 -2
- package/dist/types/layout.d.ts +4 -1
- package/package.json +19 -8
- package/src/components/BaseInput.ts +36 -10
- package/src/components/DynamicInput.ts +196 -104
- package/src/components/FieldInput.ts +43 -18
- package/src/components/MultiFieldInput.ts +275 -19
- package/src/fieldRegistryToken.ts +15 -0
- 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 +40 -24
- package/src/types/layout.ts +14 -1
- package/test/DynamicInput.spec.ts +230 -0
- package/test/FieldInput.spec.ts +146 -0
- package/test/MultiFieldInput.spec.ts +256 -0
- package/test/helpers/renderers.ts +89 -0
- package/test/layout.spec.ts +119 -0
- package/test/publicApi.spec.ts +64 -0
- package/test/setup.ts +12 -0
- package/test/smoke.spec.ts +27 -0
- package/tsconfig.json +13 -13
- package/tsconfig.spec.json +9 -0
- package/vitest.config.ts +30 -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,8 +1,9 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { EventEmitter, Output, Input, Component,
|
|
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",
|
|
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();
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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.
|
|
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
|
-
|
|
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 =
|
|
104
|
-
this.
|
|
105
|
-
this.cleanupSubscriptions();
|
|
154
|
+
const Renderer = this.registry.get(this.type);
|
|
155
|
+
this.cleanup();
|
|
106
156
|
this.host.clear();
|
|
107
157
|
if (!Renderer) {
|
|
108
|
-
|
|
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
|
-
|
|
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
|
-
|
|
126
|
-
|
|
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
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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.
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
'
|
|
169
|
-
'
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
if (!output || typeof output.subscribe !== 'function') {
|
|
257
|
+
applyExtraProps(instance) {
|
|
258
|
+
if (!instance || !this.extraProps) {
|
|
184
259
|
return;
|
|
185
260
|
}
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
317
|
+
value;
|
|
318
|
+
options;
|
|
319
|
+
disabled;
|
|
320
|
+
readOnly;
|
|
321
|
+
error;
|
|
225
322
|
onValueChangeField = new EventEmitter();
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
return '';
|
|
331
|
+
if (typeof this.fieldDescription.options === 'function') {
|
|
332
|
+
return undefined;
|
|
233
333
|
}
|
|
234
|
-
return
|
|
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",
|
|
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="
|
|
346
|
+
*ngIf="shouldRender"
|
|
240
347
|
[type]="fieldDescription!.type"
|
|
241
|
-
[value]="
|
|
348
|
+
[value]="value"
|
|
242
349
|
[label]="fieldDescription!.label"
|
|
243
350
|
[placeholder]="fieldDescription!.placeholder"
|
|
244
351
|
[required]="fieldDescription!.required"
|
|
245
|
-
[description]="fieldDescription!.description"
|
|
246
|
-
[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]="
|
|
252
|
-
[
|
|
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="
|
|
374
|
+
*ngIf="shouldRender"
|
|
265
375
|
[type]="fieldDescription!.type"
|
|
266
|
-
[value]="
|
|
376
|
+
[value]="value"
|
|
267
377
|
[label]="fieldDescription!.label"
|
|
268
378
|
[placeholder]="fieldDescription!.placeholder"
|
|
269
379
|
[required]="fieldDescription!.required"
|
|
270
|
-
[description]="fieldDescription!.description"
|
|
271
|
-
[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]="
|
|
277
|
-
[
|
|
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
|
-
}],
|
|
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
|
-
|
|
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.
|
|
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="
|
|
568
|
+
class="p-4 border rounded-lg bg-gray-50"
|
|
323
569
|
[ngClass]="{
|
|
324
|
-
'flex
|
|
325
|
-
'
|
|
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
|
-
<
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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", "
|
|
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
|
-
|
|
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="
|
|
654
|
+
class="p-4 border rounded-lg bg-gray-50"
|
|
346
655
|
[ngClass]="{
|
|
347
|
-
'flex
|
|
348
|
-
'
|
|
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
|
-
<
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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="
|
|
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-
|
|
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="
|
|
778
|
+
[style.display]="'flex'"
|
|
779
|
+
[style.flexDirection]="'column'"
|
|
780
|
+
[style.gap]="gap + 'px'"
|
|
386
781
|
>
|
|
387
|
-
<ng-
|
|
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="
|
|
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-
|
|
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="
|
|
812
|
+
[style.display]="'flex'"
|
|
813
|
+
[style.flexDirection]="'row'"
|
|
814
|
+
[style.gap]="gap + 'px'"
|
|
407
815
|
>
|
|
408
|
-
<ng-
|
|
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="
|
|
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-
|
|
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="
|
|
849
|
+
[style.display]="'grid'"
|
|
850
|
+
[style.gridTemplateColumns]="'repeat(' + columns + ', 1fr)'"
|
|
851
|
+
[style.gap]="gap + 'px'"
|
|
428
852
|
>
|
|
429
|
-
<ng-
|
|
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
|