ngx-dynamic-entity 1.1.0 → 1.2.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/esm2022/lib/field-types/array-field.component.mjs +13 -5
- package/esm2022/lib/field-types/boolean-field.component.mjs +4 -3
- package/esm2022/lib/field-types/checkbox-field.component.mjs +4 -3
- package/esm2022/lib/field-types/currency-field.component.mjs +4 -3
- package/esm2022/lib/field-types/date-field.component.mjs +4 -3
- package/esm2022/lib/field-types/dropdown-field.component.mjs +9 -7
- package/esm2022/lib/field-types/email-field.component.mjs +4 -3
- package/esm2022/lib/field-types/entity-ref-field.component.mjs +21 -7
- package/esm2022/lib/field-types/field-component.contract.mjs +2 -0
- package/esm2022/lib/field-types/file-field.component.mjs +4 -3
- package/esm2022/lib/field-types/group-field.component.mjs +5 -5
- package/esm2022/lib/field-types/image-field.component.mjs +4 -3
- package/esm2022/lib/field-types/month-year-field.component.mjs +4 -3
- package/esm2022/lib/field-types/multi-select-field.component.mjs +4 -3
- package/esm2022/lib/field-types/number-field.component.mjs +7 -11
- package/esm2022/lib/field-types/password-field.component.mjs +4 -3
- package/esm2022/lib/field-types/radio-field.component.mjs +4 -3
- package/esm2022/lib/field-types/text-field.component.mjs +25 -18
- package/esm2022/lib/field-types/textarea-field.component.mjs +4 -3
- package/esm2022/lib/form/dynamic-field/dynamic-field.component.mjs +53 -6
- package/esm2022/lib/form/dynamic-form.component.mjs +131 -15
- package/esm2022/lib/form/dynamic-record-form.component.mjs +2 -2
- package/esm2022/lib/providers/provide-ngx-dynamic-entity.mjs +7 -2
- package/esm2022/lib/services/cascade-data.service.mjs +2 -2
- package/esm2022/lib/services/entity-ref-registry.service.mjs +2 -2
- package/esm2022/lib/services/lookup-registry.service.mjs +2 -2
- package/esm2022/lib/services/validation-messages.service.mjs +56 -0
- package/esm2022/lib/services/validator-registry.service.mjs +24 -2
- package/esm2022/lib/tokens/injection-tokens.mjs +19 -6
- package/esm2022/public-api.mjs +6 -1
- package/fesm2022/ngx-dynamic-entity.mjs +373 -81
- package/fesm2022/ngx-dynamic-entity.mjs.map +1 -1
- package/lib/field-types/array-field.component.d.ts +7 -1
- package/lib/field-types/dropdown-field.component.d.ts +1 -0
- package/lib/field-types/entity-ref-field.component.d.ts +11 -1
- package/lib/field-types/field-component.contract.d.ts +49 -0
- package/lib/field-types/group-field.component.d.ts +1 -1
- package/lib/field-types/number-field.component.d.ts +1 -0
- package/lib/field-types/text-field.component.d.ts +10 -2
- package/lib/form/dynamic-field/dynamic-field.component.d.ts +39 -4
- package/lib/form/dynamic-form.component.d.ts +62 -2
- package/lib/providers/provide-ngx-dynamic-entity.d.ts +17 -1
- package/lib/services/cascade-data.service.d.ts +1 -1
- package/lib/services/entity-ref-registry.service.d.ts +1 -1
- package/lib/services/lookup-registry.service.d.ts +1 -1
- package/lib/services/validation-messages.service.d.ts +43 -0
- package/lib/services/validator-registry.service.d.ts +13 -1
- package/lib/tokens/injection-tokens.d.ts +18 -5
- package/package.json +2 -2
- package/public-api.d.ts +2 -0
- package/styles.css +579 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken, makeEnvironmentProviders,
|
|
2
|
+
import { InjectionToken, makeEnvironmentProviders, inject, Injectable, signal, Input, ChangeDetectionStrategy, Component, DestroyRef, Injector, isDevMode, ViewContainerRef, ViewChild, forwardRef, ChangeDetectorRef, Optional, Inject, EventEmitter, computed, HostListener, ViewChildren, Output } from '@angular/core';
|
|
3
3
|
import * as i1 from '@angular/forms';
|
|
4
4
|
import { ReactiveFormsModule, Validators, FormBuilder, FormArray, FormGroup } from '@angular/forms';
|
|
5
5
|
import { resolveLabel, normalizeLookupValues, lookupValuesToOptions, valuesMatch, getOptionStoredValue, resolveOptionLabel, resolveOptionValue, buildReferenceCacheKey, normalizeReferenceOptions, entityKeyFromCacheKey, applyCascadeFilter, resolveEffectiveMask, shouldMaskField, findTab, fileRefName, evaluateFormRules, filterRulesForTab, evaluateFieldVisibility, normalizeConfigOptions, migrateRecord, getTabData, getValueByPath, setTabData, setValueByPath, normalizeArrayStructures, getTabPath, applyAutoPatch, applyPatchOnTrue, formatDisplayValue } from '@dynamic-entity/core';
|
|
@@ -29,7 +29,7 @@ class InMemoryEntityRefCacheStore {
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
/** Roles that see XXXXXXXXX for masked fields
|
|
32
|
+
/** Roles that see XXXXXXXXX for masked fields. Presentational only — enforce authz server-side. */
|
|
33
33
|
const MASKED_ROLES = new InjectionToken('MASKED_ROLES');
|
|
34
34
|
/**
|
|
35
35
|
* Consumer **overrides**: fieldType string → Angular component class.
|
|
@@ -86,8 +86,25 @@ const LOOKUP_REGISTRY = new InjectionToken('LOOKUP_REGISTRY');
|
|
|
86
86
|
* })
|
|
87
87
|
*/
|
|
88
88
|
const RECORD_MIGRATIONS = new InjectionToken('RECORD_MIGRATIONS');
|
|
89
|
+
/**
|
|
90
|
+
* Overrides for the messages shown under an invalid field, keyed by Angular error key
|
|
91
|
+
* (`required`, `minlength`, `pattern`, …) plus a few field-specific keys.
|
|
92
|
+
*
|
|
93
|
+
* The built-in messages are English literals. Registering here is what makes a non-English
|
|
94
|
+
* form possible without re-implementing every field component; unlisted keys keep their
|
|
95
|
+
* default, so overriding one does not mean supplying them all.
|
|
96
|
+
*/
|
|
97
|
+
const VALIDATION_MESSAGES = new InjectionToken('VALIDATION_MESSAGES');
|
|
89
98
|
/** Registry: validator key → ValidatorFn */
|
|
90
99
|
const VALIDATOR_REGISTRY = new InjectionToken('VALIDATOR_REGISTRY');
|
|
100
|
+
/**
|
|
101
|
+
* Registry: validator key → AsyncValidatorFn.
|
|
102
|
+
*
|
|
103
|
+
* Kept separate from VALIDATOR_REGISTRY because Angular applies the two differently: an async
|
|
104
|
+
* validator runs only once the synchronous ones pass, and puts the control into `pending`
|
|
105
|
+
* while it does. Name them from a schema with `validators: { customAsync: ['uniqueEmail'] }`.
|
|
106
|
+
*/
|
|
107
|
+
const ASYNC_VALIDATOR_REGISTRY = new InjectionToken('ASYNC_VALIDATOR_REGISTRY');
|
|
91
108
|
/** Registry: hook key → async (data, context) => data */
|
|
92
109
|
const HOOK_REGISTRY = new InjectionToken('HOOK_REGISTRY');
|
|
93
110
|
/**
|
|
@@ -125,10 +142,6 @@ const UPLOAD_HANDLER = new InjectionToken('UPLOAD_HANDLER');
|
|
|
125
142
|
* { provide: ENTITY_REF_CACHE_STORE, useClass: SessionStorageRefCache }
|
|
126
143
|
*/
|
|
127
144
|
const ENTITY_REF_CACHE_STORE = new InjectionToken('ENTITY_REF_CACHE_STORE', { providedIn: 'root', factory: () => new InMemoryEntityRefCacheStore() });
|
|
128
|
-
/**
|
|
129
|
-
* Consumer-provided predicate function to check whether user roles can edit system default tabs or fields.
|
|
130
|
-
* Defaults to allowing edits if not provided.
|
|
131
|
-
*/
|
|
132
145
|
/**
|
|
133
146
|
* Consumer-provided predicate function to check whether user roles can edit system default tabs or fields.
|
|
134
147
|
* Defaults to allowing edits if not provided.
|
|
@@ -156,16 +169,90 @@ const provideNgxDynamicEntity = (config = {}) => makeEnvironmentProviders([
|
|
|
156
169
|
{ provide: ENTITY_REF_REGISTRY, useValue: new Map(Object.entries(config.entityRefs ?? {})) },
|
|
157
170
|
{ provide: LOOKUP_REGISTRY, useValue: new Map(Object.entries(config.lookups ?? {})) },
|
|
158
171
|
{ provide: VALIDATOR_REGISTRY, useValue: new Map(Object.entries(config.validators ?? {})) },
|
|
172
|
+
{
|
|
173
|
+
provide: ASYNC_VALIDATOR_REGISTRY,
|
|
174
|
+
useValue: new Map(Object.entries(config.asyncValidators ?? {})),
|
|
175
|
+
},
|
|
159
176
|
{ provide: HOOK_REGISTRY, useValue: new Map(Object.entries(config.hooks ?? {})) },
|
|
160
177
|
{ provide: RECORD_MIGRATIONS, useValue: config.migrations ?? [] },
|
|
178
|
+
{ provide: VALIDATION_MESSAGES, useValue: config.validationMessages ?? {} },
|
|
161
179
|
]);
|
|
162
180
|
|
|
181
|
+
/** The built-in English messages. Every key here can be overridden individually. */
|
|
182
|
+
const DEFAULT_VALIDATION_MESSAGES = {
|
|
183
|
+
required: 'This field is required.',
|
|
184
|
+
requiredSelection: 'Please select an option.',
|
|
185
|
+
email: 'Please enter a valid email address.',
|
|
186
|
+
pattern: 'Invalid format.',
|
|
187
|
+
minlength: (_lang, err) => `Minimum ${err?.requiredLength} characters required.`,
|
|
188
|
+
maxlength: (_lang, err) => `Maximum ${err?.requiredLength} characters allowed.`,
|
|
189
|
+
min: (_lang, err) => `Value must be at least ${err?.min}.`,
|
|
190
|
+
max: (_lang, err) => `Value must not exceed ${err?.max}.`,
|
|
191
|
+
invalid: 'Invalid value.',
|
|
192
|
+
invalidNumber: 'Invalid number.',
|
|
193
|
+
invalidSelection: 'Invalid selection.',
|
|
194
|
+
};
|
|
195
|
+
class ValidationMessagesService {
|
|
196
|
+
overrides = inject(VALIDATION_MESSAGES, { optional: true }) ?? {};
|
|
197
|
+
/**
|
|
198
|
+
* The message for the first error present, in the order the caller lists.
|
|
199
|
+
*
|
|
200
|
+
* Order is the caller's because it is field-specific: a dropdown wants
|
|
201
|
+
* `requiredSelection` where a text input wants `required`, and the first match wins so the
|
|
202
|
+
* most specific message is the one shown.
|
|
203
|
+
*/
|
|
204
|
+
resolve(errors, language, order, fallbackKey = 'invalid') {
|
|
205
|
+
if (!errors)
|
|
206
|
+
return '';
|
|
207
|
+
for (const entry of order) {
|
|
208
|
+
// A tuple maps an Angular error key to a different message key: a dropdown raises the
|
|
209
|
+
// standard `required` error but reads better as "Please select an option", and both
|
|
210
|
+
// messages stay independently overridable.
|
|
211
|
+
const [errorKey, messageKey] = typeof entry === 'string' ? [entry, entry] : entry;
|
|
212
|
+
// `errors[errorKey]` is usually an object of detail, but `required` is simply `true`.
|
|
213
|
+
if (errors[errorKey] === undefined)
|
|
214
|
+
continue;
|
|
215
|
+
return this.messageFor(messageKey, language, errors[errorKey]);
|
|
216
|
+
}
|
|
217
|
+
return this.messageFor(fallbackKey, language, null);
|
|
218
|
+
}
|
|
219
|
+
/** One message by key, honouring an override before the default. */
|
|
220
|
+
messageFor(key, language, error) {
|
|
221
|
+
const message = this.overrides[key] ?? DEFAULT_VALIDATION_MESSAGES[key];
|
|
222
|
+
if (message === undefined)
|
|
223
|
+
return '';
|
|
224
|
+
return typeof message === 'function' ? message(language, error) : message;
|
|
225
|
+
}
|
|
226
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ValidationMessagesService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
227
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ValidationMessagesService, providedIn: 'root' });
|
|
228
|
+
}
|
|
229
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ValidationMessagesService, decorators: [{
|
|
230
|
+
type: Injectable,
|
|
231
|
+
args: [{ providedIn: 'root' }]
|
|
232
|
+
}] });
|
|
233
|
+
|
|
163
234
|
class TextFieldComponent {
|
|
235
|
+
messages = inject(ValidationMessagesService);
|
|
236
|
+
displaySub;
|
|
164
237
|
field;
|
|
165
|
-
control
|
|
238
|
+
set control(value) {
|
|
239
|
+
this._control = value;
|
|
240
|
+
this.displaySub?.unsubscribe();
|
|
241
|
+
this.displayValue.set(value?.value ?? null);
|
|
242
|
+
this.displaySub = value?.valueChanges.subscribe(v => this.displayValue.set(v));
|
|
243
|
+
}
|
|
244
|
+
get control() {
|
|
245
|
+
return this._control;
|
|
246
|
+
}
|
|
247
|
+
_control;
|
|
166
248
|
language = 'en';
|
|
167
249
|
readonly = false;
|
|
168
250
|
masked = false;
|
|
251
|
+
/** Readonly markup reads this signal so an external patch (autoPatch) is visible under OnPush. */
|
|
252
|
+
displayValue = signal(null);
|
|
253
|
+
ngOnDestroy() {
|
|
254
|
+
this.displaySub?.unsubscribe();
|
|
255
|
+
}
|
|
169
256
|
get label() {
|
|
170
257
|
return resolveLabel(this.field?.label, this.language);
|
|
171
258
|
}
|
|
@@ -175,18 +262,7 @@ class TextFieldComponent {
|
|
|
175
262
|
get errorMessage() {
|
|
176
263
|
if (!this.control || !this.control.errors || !this.control.touched)
|
|
177
264
|
return '';
|
|
178
|
-
|
|
179
|
-
if (errs['required'])
|
|
180
|
-
return 'This field is required.';
|
|
181
|
-
if (errs['email'])
|
|
182
|
-
return 'Please enter a valid email address.';
|
|
183
|
-
if (errs['minlength'])
|
|
184
|
-
return `Minimum ${errs['minlength'].requiredLength} characters required.`;
|
|
185
|
-
if (errs['maxlength'])
|
|
186
|
-
return `Maximum ${errs['maxlength'].requiredLength} characters allowed.`;
|
|
187
|
-
if (errs['pattern'])
|
|
188
|
-
return 'Invalid format.';
|
|
189
|
-
return 'Invalid value.';
|
|
265
|
+
return this.messages.resolve(this.control.errors, this.language, ['required', 'email', 'minlength', 'maxlength', 'pattern']);
|
|
190
266
|
}
|
|
191
267
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: TextFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
192
268
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: TextFieldComponent, isStandalone: true, selector: "ngx-text-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
@@ -201,7 +277,7 @@ class TextFieldComponent {
|
|
|
201
277
|
@if (masked) {
|
|
202
278
|
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
203
279
|
} @else if (readonly) {
|
|
204
|
-
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{
|
|
280
|
+
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ displayValue() ?? '—' }}</span>
|
|
205
281
|
} @else {
|
|
206
282
|
<input
|
|
207
283
|
[id]="field.id"
|
|
@@ -218,11 +294,12 @@ class TextFieldComponent {
|
|
|
218
294
|
}
|
|
219
295
|
}
|
|
220
296
|
</div>
|
|
221
|
-
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }] });
|
|
297
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
222
298
|
}
|
|
223
299
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: TextFieldComponent, decorators: [{
|
|
224
300
|
type: Component,
|
|
225
301
|
args: [{
|
|
302
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
226
303
|
selector: 'ngx-text-field',
|
|
227
304
|
standalone: true,
|
|
228
305
|
imports: [ReactiveFormsModule],
|
|
@@ -238,7 +315,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
238
315
|
@if (masked) {
|
|
239
316
|
<span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
|
|
240
317
|
} @else if (readonly) {
|
|
241
|
-
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{
|
|
318
|
+
<span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ displayValue() ?? '—' }}</span>
|
|
242
319
|
} @else {
|
|
243
320
|
<input
|
|
244
321
|
[id]="field.id"
|
|
@@ -304,11 +381,12 @@ class TextareaFieldComponent {
|
|
|
304
381
|
}
|
|
305
382
|
}
|
|
306
383
|
</div>
|
|
307
|
-
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }] });
|
|
384
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
308
385
|
}
|
|
309
386
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: TextareaFieldComponent, decorators: [{
|
|
310
387
|
type: Component,
|
|
311
388
|
args: [{
|
|
389
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
312
390
|
selector: 'ngx-textarea-field',
|
|
313
391
|
standalone: true,
|
|
314
392
|
imports: [ReactiveFormsModule],
|
|
@@ -349,6 +427,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
349
427
|
}] } });
|
|
350
428
|
|
|
351
429
|
class NumberFieldComponent {
|
|
430
|
+
messages = inject(ValidationMessagesService);
|
|
352
431
|
field;
|
|
353
432
|
control;
|
|
354
433
|
language = 'en';
|
|
@@ -363,14 +442,7 @@ class NumberFieldComponent {
|
|
|
363
442
|
get errorMessage() {
|
|
364
443
|
if (!this.control || !this.control.errors || !this.control.touched)
|
|
365
444
|
return '';
|
|
366
|
-
|
|
367
|
-
if (errs['required'])
|
|
368
|
-
return 'This field is required.';
|
|
369
|
-
if (errs['min'])
|
|
370
|
-
return `Value must be at least ${errs['min'].min}.`;
|
|
371
|
-
if (errs['max'])
|
|
372
|
-
return `Value must not exceed ${errs['max'].max}.`;
|
|
373
|
-
return 'Invalid number.';
|
|
445
|
+
return this.messages.resolve(this.control.errors, this.language, ['required', 'min', 'max'], 'invalidNumber');
|
|
374
446
|
}
|
|
375
447
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: NumberFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
376
448
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: NumberFieldComponent, isStandalone: true, selector: "ngx-number-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
@@ -405,11 +477,12 @@ class NumberFieldComponent {
|
|
|
405
477
|
}
|
|
406
478
|
}
|
|
407
479
|
</div>
|
|
408
|
-
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }] });
|
|
480
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
409
481
|
}
|
|
410
482
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: NumberFieldComponent, decorators: [{
|
|
411
483
|
type: Component,
|
|
412
484
|
args: [{
|
|
485
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
413
486
|
selector: 'ngx-number-field',
|
|
414
487
|
standalone: true,
|
|
415
488
|
imports: [ReactiveFormsModule],
|
|
@@ -508,11 +581,12 @@ class CurrencyFieldComponent {
|
|
|
508
581
|
}
|
|
509
582
|
}
|
|
510
583
|
</div>
|
|
511
|
-
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }] });
|
|
584
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
512
585
|
}
|
|
513
586
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: CurrencyFieldComponent, decorators: [{
|
|
514
587
|
type: Component,
|
|
515
588
|
args: [{
|
|
589
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
516
590
|
selector: 'ngx-currency-field',
|
|
517
591
|
standalone: true,
|
|
518
592
|
imports: [ReactiveFormsModule],
|
|
@@ -599,11 +673,12 @@ class EmailFieldComponent {
|
|
|
599
673
|
}
|
|
600
674
|
}
|
|
601
675
|
</div>
|
|
602
|
-
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }] });
|
|
676
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
603
677
|
}
|
|
604
678
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: EmailFieldComponent, decorators: [{
|
|
605
679
|
type: Component,
|
|
606
680
|
args: [{
|
|
681
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
607
682
|
selector: 'ngx-email-field',
|
|
608
683
|
standalone: true,
|
|
609
684
|
imports: [ReactiveFormsModule],
|
|
@@ -701,11 +776,12 @@ class PasswordFieldComponent {
|
|
|
701
776
|
}
|
|
702
777
|
}
|
|
703
778
|
</div>
|
|
704
|
-
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }] });
|
|
779
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
705
780
|
}
|
|
706
781
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: PasswordFieldComponent, decorators: [{
|
|
707
782
|
type: Component,
|
|
708
783
|
args: [{
|
|
784
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
709
785
|
selector: 'ngx-password-field',
|
|
710
786
|
standalone: true,
|
|
711
787
|
imports: [ReactiveFormsModule],
|
|
@@ -787,11 +863,12 @@ class CheckboxFieldComponent {
|
|
|
787
863
|
</label>
|
|
788
864
|
}
|
|
789
865
|
</div>
|
|
790
|
-
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }] });
|
|
866
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
791
867
|
}
|
|
792
868
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: CheckboxFieldComponent, decorators: [{
|
|
793
869
|
type: Component,
|
|
794
870
|
args: [{
|
|
871
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
795
872
|
selector: 'ngx-checkbox-field',
|
|
796
873
|
standalone: true,
|
|
797
874
|
imports: [ReactiveFormsModule],
|
|
@@ -875,11 +952,12 @@ class BooleanFieldComponent {
|
|
|
875
952
|
</div>
|
|
876
953
|
}
|
|
877
954
|
</div>
|
|
878
|
-
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }] });
|
|
955
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
879
956
|
}
|
|
880
957
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: BooleanFieldComponent, decorators: [{
|
|
881
958
|
type: Component,
|
|
882
959
|
args: [{
|
|
960
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
883
961
|
selector: 'ngx-boolean-field',
|
|
884
962
|
standalone: true,
|
|
885
963
|
imports: [ReactiveFormsModule],
|
|
@@ -968,11 +1046,12 @@ class DateFieldComponent {
|
|
|
968
1046
|
}
|
|
969
1047
|
}
|
|
970
1048
|
</div>
|
|
971
|
-
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }] });
|
|
1049
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
972
1050
|
}
|
|
973
1051
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: DateFieldComponent, decorators: [{
|
|
974
1052
|
type: Component,
|
|
975
1053
|
args: [{
|
|
1054
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
976
1055
|
selector: 'ngx-date-field',
|
|
977
1056
|
standalone: true,
|
|
978
1057
|
imports: [ReactiveFormsModule],
|
|
@@ -1097,11 +1176,12 @@ class MonthYearFieldComponent {
|
|
|
1097
1176
|
}
|
|
1098
1177
|
}
|
|
1099
1178
|
</div>
|
|
1100
|
-
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }] });
|
|
1179
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1101
1180
|
}
|
|
1102
1181
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: MonthYearFieldComponent, decorators: [{
|
|
1103
1182
|
type: Component,
|
|
1104
1183
|
args: [{
|
|
1184
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1105
1185
|
selector: 'ngx-month-year-field',
|
|
1106
1186
|
standalone: true,
|
|
1107
1187
|
imports: [ReactiveFormsModule],
|
|
@@ -1372,7 +1452,7 @@ const hostLastConfig = new WeakMap();
|
|
|
1372
1452
|
* Point a choice component's `options` signal at the right source (parity plan §6.3).
|
|
1373
1453
|
*
|
|
1374
1454
|
* Called from the `field` / `language` input setters of `dropdown`, `radio` and `multiSelect` —
|
|
1375
|
-
* the wiring is theirs, the decision is the service's, which is what keeps the
|
|
1455
|
+
* the wiring is theirs, the decision is the service's, which is what keeps the field
|
|
1376
1456
|
* five-input contract intact: options are never pushed in as a sixth input.
|
|
1377
1457
|
*
|
|
1378
1458
|
* Setters rather than `ngOnChanges` on purpose. `ngOnChanges` only fires for inputs Angular
|
|
@@ -1401,6 +1481,7 @@ function refreshChoiceOptions(host, lookups) {
|
|
|
1401
1481
|
|
|
1402
1482
|
class DropdownFieldComponent {
|
|
1403
1483
|
lookups = inject(LookupRegistryService);
|
|
1484
|
+
messages = inject(ValidationMessagesService);
|
|
1404
1485
|
_field;
|
|
1405
1486
|
_language = 'en';
|
|
1406
1487
|
/** Setter-based so options resolve however the input is set — see `refreshChoiceOptions`. */
|
|
@@ -1459,10 +1540,9 @@ class DropdownFieldComponent {
|
|
|
1459
1540
|
get errorMessage() {
|
|
1460
1541
|
if (!this.control || !this.control.errors || !this.control.touched)
|
|
1461
1542
|
return '';
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
return 'Invalid selection.';
|
|
1543
|
+
// `requiredSelection` rather than `required`: "Please select an option" reads better on a
|
|
1544
|
+
// dropdown than "This field is required", and both stay independently overridable.
|
|
1545
|
+
return this.messages.resolve(this.control.errors, this.language, [['required', 'requiredSelection']], 'invalidSelection');
|
|
1466
1546
|
}
|
|
1467
1547
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: DropdownFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1468
1548
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: DropdownFieldComponent, isStandalone: true, selector: "ngx-dropdown-field", inputs: { field: "field", language: "language", control: "control", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
|
|
@@ -1505,11 +1585,12 @@ class DropdownFieldComponent {
|
|
|
1505
1585
|
}
|
|
1506
1586
|
}
|
|
1507
1587
|
</div>
|
|
1508
|
-
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }] });
|
|
1588
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1509
1589
|
}
|
|
1510
1590
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: DropdownFieldComponent, decorators: [{
|
|
1511
1591
|
type: Component,
|
|
1512
1592
|
args: [{
|
|
1593
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1513
1594
|
selector: 'ngx-dropdown-field',
|
|
1514
1595
|
standalone: true,
|
|
1515
1596
|
imports: [ReactiveFormsModule],
|
|
@@ -1660,11 +1741,12 @@ class RadioFieldComponent {
|
|
|
1660
1741
|
}
|
|
1661
1742
|
</fieldset>
|
|
1662
1743
|
</div>
|
|
1663
|
-
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.RadioControlValueAccessor, selector: "input[type=radio][formControlName],input[type=radio][formControl],input[type=radio][ngModel]", inputs: ["name", "formControlName", "value"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }] });
|
|
1744
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.RadioControlValueAccessor, selector: "input[type=radio][formControlName],input[type=radio][formControl],input[type=radio][ngModel]", inputs: ["name", "formControlName", "value"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1664
1745
|
}
|
|
1665
1746
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: RadioFieldComponent, decorators: [{
|
|
1666
1747
|
type: Component,
|
|
1667
1748
|
args: [{
|
|
1749
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1668
1750
|
selector: 'ngx-radio-field',
|
|
1669
1751
|
standalone: true,
|
|
1670
1752
|
imports: [ReactiveFormsModule],
|
|
@@ -1799,11 +1881,12 @@ class MultiSelectFieldComponent {
|
|
|
1799
1881
|
}
|
|
1800
1882
|
}
|
|
1801
1883
|
</div>
|
|
1802
|
-
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.SelectMultipleControlValueAccessor, selector: "select[multiple][formControlName],select[multiple][formControl],select[multiple][ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }] });
|
|
1884
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.SelectMultipleControlValueAccessor, selector: "select[multiple][formControlName],select[multiple][formControl],select[multiple][ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1803
1885
|
}
|
|
1804
1886
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: MultiSelectFieldComponent, decorators: [{
|
|
1805
1887
|
type: Component,
|
|
1806
1888
|
args: [{
|
|
1889
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1807
1890
|
selector: 'ngx-multi-select-field',
|
|
1808
1891
|
standalone: true,
|
|
1809
1892
|
imports: [ReactiveFormsModule],
|
|
@@ -1854,7 +1937,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
1854
1937
|
|
|
1855
1938
|
/**
|
|
1856
1939
|
* EntityRefRegistryService — resolves entity-ref option loader functions.
|
|
1857
|
-
*
|
|
1940
|
+
* Entity-ref options come ONLY from this registry.
|
|
1858
1941
|
* Never pass loaders via @Input() on field components.
|
|
1859
1942
|
*
|
|
1860
1943
|
* Consumers register loaders via provideNgxDynamicEntity({ entityRefs: { clients: () => [...] } }).
|
|
@@ -2046,7 +2129,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
2046
2129
|
/**
|
|
2047
2130
|
* CascadeDataService — loads entity-reference options and applies parent→child cascades.
|
|
2048
2131
|
*
|
|
2049
|
-
* Loaders come from the registry only
|
|
2132
|
+
* Loaders come from the registry only. This service owns the three things a
|
|
2050
2133
|
* field component should not: resolving the loader, coercing whatever it returned
|
|
2051
2134
|
* (array | Promise | Observable) into a Promise, and filtering by the parent's value.
|
|
2052
2135
|
*/
|
|
@@ -2226,6 +2309,9 @@ class EntityRefFieldComponent {
|
|
|
2226
2309
|
return;
|
|
2227
2310
|
void this.reload();
|
|
2228
2311
|
this.watchParent();
|
|
2312
|
+
this.control?.valueChanges
|
|
2313
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
2314
|
+
.subscribe(value => this.publishSelection(String(value ?? '')));
|
|
2229
2315
|
}
|
|
2230
2316
|
/** Reload options against the parent's current value. */
|
|
2231
2317
|
async reload() {
|
|
@@ -2243,8 +2329,20 @@ class EntityRefFieldComponent {
|
|
|
2243
2329
|
this.loading.set(false);
|
|
2244
2330
|
}
|
|
2245
2331
|
}
|
|
2246
|
-
|
|
2247
|
-
|
|
2332
|
+
/**
|
|
2333
|
+
* Publish the selected option so autoPatch can copy from its record.
|
|
2334
|
+
*
|
|
2335
|
+
* Driven from `valueChanges` rather than the select's `change` event: that event is the
|
|
2336
|
+
* same one the `formControl` directive listens to, so reading the control inside a
|
|
2337
|
+
* template `(change)` handler can see the previous value. `valueChanges` fires after the
|
|
2338
|
+
* model has updated — including Playwright `selectOption`, which updates the control
|
|
2339
|
+
* through the value accessor.
|
|
2340
|
+
*/
|
|
2341
|
+
onSelectionChange(value) {
|
|
2342
|
+
this.publishSelection(value ?? String(this.control?.value ?? ''));
|
|
2343
|
+
}
|
|
2344
|
+
publishSelection(current) {
|
|
2345
|
+
const selected = this.options().find(o => String(o.value) === current);
|
|
2248
2346
|
this.selectionBus.emit(this.field.id, selected ?? null);
|
|
2249
2347
|
}
|
|
2250
2348
|
getLabel(value) {
|
|
@@ -2293,7 +2391,6 @@ class EntityRefFieldComponent {
|
|
|
2293
2391
|
[id]="'field-' + field.id"
|
|
2294
2392
|
[formControl]="$any(control)"
|
|
2295
2393
|
[attr.disabled]="field.disabled ? true : null"
|
|
2296
|
-
(change)="onSelectionChange()"
|
|
2297
2394
|
>
|
|
2298
2395
|
<option value="">{{ placeholder || 'Select...' }}</option>
|
|
2299
2396
|
@for (option of options(); track option.value) {
|
|
@@ -2309,11 +2406,12 @@ class EntityRefFieldComponent {
|
|
|
2309
2406
|
}
|
|
2310
2407
|
}
|
|
2311
2408
|
</div>
|
|
2312
|
-
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }] });
|
|
2409
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2313
2410
|
}
|
|
2314
2411
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: EntityRefFieldComponent, decorators: [{
|
|
2315
2412
|
type: Component,
|
|
2316
2413
|
args: [{
|
|
2414
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
2317
2415
|
selector: 'ngx-entity-ref-field',
|
|
2318
2416
|
standalone: true,
|
|
2319
2417
|
imports: [ReactiveFormsModule],
|
|
@@ -2334,7 +2432,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
2334
2432
|
[id]="'field-' + field.id"
|
|
2335
2433
|
[formControl]="$any(control)"
|
|
2336
2434
|
[attr.disabled]="field.disabled ? true : null"
|
|
2337
|
-
(change)="onSelectionChange()"
|
|
2338
2435
|
>
|
|
2339
2436
|
<option value="">{{ placeholder || 'Select...' }}</option>
|
|
2340
2437
|
@for (option of options(); track option.value) {
|
|
@@ -2460,7 +2557,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
2460
2557
|
|
|
2461
2558
|
/**
|
|
2462
2559
|
* DynamicFieldComponent — mounts the correct field component for a given NestedFieldConfig.
|
|
2463
|
-
* Uses ViewContainerRef.createComponent() and passes inputs via setInput()
|
|
2560
|
+
* Uses ViewContainerRef.createComponent() and passes inputs via setInput(); see
|
|
2561
|
+
* DynamicFieldComponentContract for the five inputs every field component receives.
|
|
2464
2562
|
* Never add field-type-specific logic here — this component must remain generic.
|
|
2465
2563
|
*
|
|
2466
2564
|
* The 5 inputs (field, control, language, readonly, masked) are passed to every mounted
|
|
@@ -2477,6 +2575,12 @@ class DynamicFieldComponent {
|
|
|
2477
2575
|
fieldHost;
|
|
2478
2576
|
fieldRegistry = inject(FieldRegistryService);
|
|
2479
2577
|
rbacService = inject(RbacService);
|
|
2578
|
+
/**
|
|
2579
|
+
* Passed into `createComponent` so a hosted field sees this host's injector — including
|
|
2580
|
+
* `DynamicFormComponent`'s scoped `EntityRefSelectionService`. Without it, a standalone
|
|
2581
|
+
* field resolves the root bus and `autoPatch` never runs.
|
|
2582
|
+
*/
|
|
2583
|
+
injector = inject(Injector);
|
|
2480
2584
|
componentRef = null;
|
|
2481
2585
|
ngOnChanges(_changes) {
|
|
2482
2586
|
this.mountField();
|
|
@@ -2498,9 +2602,12 @@ class DynamicFieldComponent {
|
|
|
2498
2602
|
return;
|
|
2499
2603
|
}
|
|
2500
2604
|
this.fieldHost.clear();
|
|
2501
|
-
this.componentRef = this.fieldHost.createComponent(ComponentClass
|
|
2605
|
+
this.componentRef = this.fieldHost.createComponent(ComponentClass, {
|
|
2606
|
+
injector: this.injector,
|
|
2607
|
+
});
|
|
2502
2608
|
this.setInputs(this.componentRef, masked);
|
|
2503
|
-
this.
|
|
2609
|
+
this.watchControl();
|
|
2610
|
+
this.componentRef.changeDetectorRef.detectChanges();
|
|
2504
2611
|
}
|
|
2505
2612
|
/**
|
|
2506
2613
|
* Field types are opt-in (see `provideFieldTypes` / `provideBuiltInFieldTypes`), so an
|
|
@@ -2516,7 +2623,44 @@ class DynamicFieldComponent {
|
|
|
2516
2623
|
}
|
|
2517
2624
|
static warnedTypes = new Set();
|
|
2518
2625
|
/**
|
|
2519
|
-
*
|
|
2626
|
+
* Re-check the hosted field component.
|
|
2627
|
+
*
|
|
2628
|
+
* The field components are OnPush, so they re-render on an input change, an event from
|
|
2629
|
+
* their own template, or a signal they read. `form.markAllAsTouched()` is none of those —
|
|
2630
|
+
* it flips `touched` from outside, and every field's error message depends on it. Without
|
|
2631
|
+
* this, a blocked submit would mark the form touched and no error would appear.
|
|
2632
|
+
*/
|
|
2633
|
+
refresh() {
|
|
2634
|
+
this.componentRef?.changeDetectorRef.detectChanges();
|
|
2635
|
+
}
|
|
2636
|
+
/**
|
|
2637
|
+
* Re-check the hosted component whenever its control changes from outside.
|
|
2638
|
+
*
|
|
2639
|
+
* The field components are OnPush, so they re-render on an input change or an event from
|
|
2640
|
+
* their own template. A control mutated externally is neither — and that is not an edge
|
|
2641
|
+
* case: patchForm, reset, autoPatch and patchOnTrue all do exactly that. Without this, a
|
|
2642
|
+
* field patched from a record would keep rendering its previous value.
|
|
2643
|
+
*
|
|
2644
|
+
* `detectChanges()` rather than `markForCheck()`: a selection that arrives from a native
|
|
2645
|
+
* `change` event (or Playwright) may not be followed by another Angular tick, so a dirty
|
|
2646
|
+
* flag would never be flushed. Running CD on the hosted component now is the copy showing up.
|
|
2647
|
+
*
|
|
2648
|
+
* Done here rather than in each of the eighteen field components: the host owns the
|
|
2649
|
+
* component reference, so one subscription covers every type including custom ones.
|
|
2650
|
+
*/
|
|
2651
|
+
watchControl() {
|
|
2652
|
+
this.controlSub?.unsubscribe();
|
|
2653
|
+
if (!this.control)
|
|
2654
|
+
return;
|
|
2655
|
+
this.controlSub = this.control.valueChanges.subscribe(() => this.refresh());
|
|
2656
|
+
}
|
|
2657
|
+
controlSub;
|
|
2658
|
+
ngOnDestroy() {
|
|
2659
|
+
this.controlSub?.unsubscribe();
|
|
2660
|
+
}
|
|
2661
|
+
/**
|
|
2662
|
+
* Pass all 5 contract inputs via ComponentRef.setInput() — uniform for all types.
|
|
2663
|
+
* The public shape is DynamicFieldComponentContract.
|
|
2520
2664
|
* Must use setInput(), not property assignment: the field components declare inputs with
|
|
2521
2665
|
* definite assignment and no initializer, and the lib compiles with
|
|
2522
2666
|
* useDefineForClassFields:false, so `'field' in instance` is false until first set.
|
|
@@ -2557,7 +2701,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
2557
2701
|
args: ['fieldHost', { read: ViewContainerRef, static: true }]
|
|
2558
2702
|
}] } });
|
|
2559
2703
|
|
|
2560
|
-
/**
|
|
2704
|
+
/** Renders a nested fieldset container for a group field's children. */
|
|
2561
2705
|
class GroupFieldComponent {
|
|
2562
2706
|
field;
|
|
2563
2707
|
control;
|
|
@@ -2597,11 +2741,11 @@ class GroupFieldComponent {
|
|
|
2597
2741
|
</div>
|
|
2598
2742
|
}
|
|
2599
2743
|
</fieldset>
|
|
2600
|
-
`, isInline: true, styles: [".ngx-field--group{border:1px solid #e2e8f0;border-radius:8px;padding:16px;margin-bottom:16px;background:#fafafa}.ngx-field__legend{font-weight:600;font-size:14px;padding:0 8px;color:#334155}.ngx-field__group-children{display:flex;flex-direction:column;gap:12px}\n"], dependencies: [{ kind: "ngmodule", type: i0.forwardRef(() => ReactiveFormsModule) }, { kind: "component", type: i0.forwardRef(() => DynamicFieldComponent), selector: "ngx-dynamic-field", inputs: ["field", "control", "config", "currentTabId", "language", "readonly", "userRoles"] }] });
|
|
2744
|
+
`, isInline: true, styles: [".ngx-field--group{border:1px solid #e2e8f0;border-radius:8px;padding:16px;margin-bottom:16px;background:#fafafa}.ngx-field__legend{font-weight:600;font-size:14px;padding:0 8px;color:#334155}.ngx-field__group-children{display:flex;flex-direction:column;gap:12px}\n"], dependencies: [{ kind: "ngmodule", type: i0.forwardRef(() => ReactiveFormsModule) }, { kind: "component", type: i0.forwardRef(() => DynamicFieldComponent), selector: "ngx-dynamic-field", inputs: ["field", "control", "config", "currentTabId", "language", "readonly", "userRoles"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2601
2745
|
}
|
|
2602
2746
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: GroupFieldComponent, decorators: [{
|
|
2603
2747
|
type: Component,
|
|
2604
|
-
args: [{ selector: 'ngx-group-field', standalone: true, imports: [ReactiveFormsModule, forwardRef(() => DynamicFieldComponent)], template: `
|
|
2748
|
+
args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'ngx-group-field', standalone: true, imports: [ReactiveFormsModule, forwardRef(() => DynamicFieldComponent)], template: `
|
|
2605
2749
|
<fieldset class="ngx-field ngx-field--group"
|
|
2606
2750
|
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
2607
2751
|
<legend class="ngx-field__legend">{{ label }}</legend>
|
|
@@ -2646,6 +2790,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
2646
2790
|
*/
|
|
2647
2791
|
class ValidatorRegistryService {
|
|
2648
2792
|
consumerRegistry = inject(VALIDATOR_REGISTRY, { optional: true }) ?? new Map();
|
|
2793
|
+
asyncRegistry = inject(ASYNC_VALIDATOR_REGISTRY, { optional: true }) ?? new Map();
|
|
2649
2794
|
/**
|
|
2650
2795
|
* Resolve a validator key string to a ValidatorFn.
|
|
2651
2796
|
* Supports parameterized built-ins like 'min:0', 'maxLength:255'.
|
|
@@ -2706,6 +2851,27 @@ class ValidatorRegistryService {
|
|
|
2706
2851
|
}
|
|
2707
2852
|
return fnList;
|
|
2708
2853
|
}
|
|
2854
|
+
/**
|
|
2855
|
+
* Resolve the async validators a field names, in `validators.customAsync`.
|
|
2856
|
+
*
|
|
2857
|
+
* Async validators are deliberately a separate registry and a separate config key: Angular
|
|
2858
|
+
* attaches them through `setAsyncValidators`, runs them only once the synchronous ones
|
|
2859
|
+
* pass, and holds the control in `pending` until they settle. Mixing them into the
|
|
2860
|
+
* synchronous list would silently never run them.
|
|
2861
|
+
*
|
|
2862
|
+
* An unknown name is skipped rather than throwing, matching the synchronous behaviour.
|
|
2863
|
+
*/
|
|
2864
|
+
resolveAsyncFromConfig(config) {
|
|
2865
|
+
if (!config || Array.isArray(config))
|
|
2866
|
+
return [];
|
|
2867
|
+
const out = [];
|
|
2868
|
+
for (const key of config.customAsync ?? []) {
|
|
2869
|
+
const fn = this.asyncRegistry.get(key);
|
|
2870
|
+
if (fn)
|
|
2871
|
+
out.push(fn);
|
|
2872
|
+
}
|
|
2873
|
+
return out;
|
|
2874
|
+
}
|
|
2709
2875
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ValidatorRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
2710
2876
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ValidatorRegistryService, providedIn: 'root' });
|
|
2711
2877
|
}
|
|
@@ -2714,8 +2880,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
2714
2880
|
args: [{ providedIn: 'root' }]
|
|
2715
2881
|
}] });
|
|
2716
2882
|
|
|
2717
|
-
/**
|
|
2883
|
+
/** Renders repeating rows for a FormArray, with Add Item and Remove. */
|
|
2718
2884
|
class ArrayFieldComponent {
|
|
2885
|
+
/**
|
|
2886
|
+
* addItem/removeItem are public and may be called from outside this component's template,
|
|
2887
|
+
* which under OnPush would not re-render. The host also watches the control, but this
|
|
2888
|
+
* keeps the component correct on its own.
|
|
2889
|
+
*/
|
|
2890
|
+
cdr = inject(ChangeDetectorRef);
|
|
2719
2891
|
field;
|
|
2720
2892
|
control;
|
|
2721
2893
|
config;
|
|
@@ -2742,9 +2914,11 @@ class ArrayFieldComponent {
|
|
|
2742
2914
|
}
|
|
2743
2915
|
const group = this.fb.group(controls);
|
|
2744
2916
|
this.formArray.push(group);
|
|
2917
|
+
this.cdr.markForCheck();
|
|
2745
2918
|
}
|
|
2746
2919
|
removeItem(index) {
|
|
2747
2920
|
this.formArray.removeAt(index);
|
|
2921
|
+
this.cdr.markForCheck();
|
|
2748
2922
|
}
|
|
2749
2923
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ArrayFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2750
2924
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: ArrayFieldComponent, isStandalone: true, selector: "ngx-array-field", inputs: { field: "field", control: "control", config: "config", language: "language", readonly: "readonly", masked: "masked", userRoles: "userRoles" }, ngImport: i0, template: `
|
|
@@ -2797,11 +2971,11 @@ class ArrayFieldComponent {
|
|
|
2797
2971
|
</div>
|
|
2798
2972
|
}
|
|
2799
2973
|
</div>
|
|
2800
|
-
`, isInline: true, styles: [".ngx-field--array{border:1px solid #e2e8f0;border-radius:8px;padding:16px;margin-bottom:16px;background:#f8fafc}.ngx-field__array-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}.ngx-field__array-add-btn{background:#2563eb;color:#fff;border:none;padding:6px 12px;border-radius:4px;font-weight:500;cursor:pointer}.ngx-field__array-add-btn:hover{background:#1d4ed8}.ngx-field__array-list{display:flex;flex-direction:column;gap:12px}.ngx-field__array-item{background:#fff;border:1px solid #cbd5e1;border-radius:6px;padding:12px}.ngx-field__array-item-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;border-bottom:1px solid #f1f5f9;padding-bottom:6px}.ngx-field__array-item-title{font-weight:600;font-size:13px;color:#475569}.ngx-field__array-remove-btn{background:#ef4444;color:#fff;border:none;padding:4px 8px;border-radius:4px;font-size:12px;cursor:pointer}.ngx-field__array-empty{color:#94a3b8;font-style:italic;font-size:13px}\n"], dependencies: [{ kind: "ngmodule", type: i0.forwardRef(() => ReactiveFormsModule) }, { kind: "component", type: i0.forwardRef(() => DynamicFieldComponent), selector: "ngx-dynamic-field", inputs: ["field", "control", "config", "currentTabId", "language", "readonly", "userRoles"] }] });
|
|
2974
|
+
`, isInline: true, styles: [".ngx-field--array{border:1px solid #e2e8f0;border-radius:8px;padding:16px;margin-bottom:16px;background:#f8fafc}.ngx-field__array-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}.ngx-field__array-add-btn{background:#2563eb;color:#fff;border:none;padding:6px 12px;border-radius:4px;font-weight:500;cursor:pointer}.ngx-field__array-add-btn:hover{background:#1d4ed8}.ngx-field__array-list{display:flex;flex-direction:column;gap:12px}.ngx-field__array-item{background:#fff;border:1px solid #cbd5e1;border-radius:6px;padding:12px}.ngx-field__array-item-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;border-bottom:1px solid #f1f5f9;padding-bottom:6px}.ngx-field__array-item-title{font-weight:600;font-size:13px;color:#475569}.ngx-field__array-remove-btn{background:#ef4444;color:#fff;border:none;padding:4px 8px;border-radius:4px;font-size:12px;cursor:pointer}.ngx-field__array-empty{color:#94a3b8;font-style:italic;font-size:13px}\n"], dependencies: [{ kind: "ngmodule", type: i0.forwardRef(() => ReactiveFormsModule) }, { kind: "component", type: i0.forwardRef(() => DynamicFieldComponent), selector: "ngx-dynamic-field", inputs: ["field", "control", "config", "currentTabId", "language", "readonly", "userRoles"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2801
2975
|
}
|
|
2802
2976
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ArrayFieldComponent, decorators: [{
|
|
2803
2977
|
type: Component,
|
|
2804
|
-
args: [{ selector: 'ngx-array-field', standalone: true, imports: [ReactiveFormsModule, forwardRef(() => DynamicFieldComponent)], template: `
|
|
2978
|
+
args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'ngx-array-field', standalone: true, imports: [ReactiveFormsModule, forwardRef(() => DynamicFieldComponent)], template: `
|
|
2805
2979
|
<div class="ngx-field ngx-field--array"
|
|
2806
2980
|
[attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
|
|
2807
2981
|
<div class="ngx-field__array-header">
|
|
@@ -3052,11 +3226,12 @@ class ImageFieldComponent {
|
|
|
3052
3226
|
}
|
|
3053
3227
|
}
|
|
3054
3228
|
</div>
|
|
3055
|
-
`, isInline: true });
|
|
3229
|
+
`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3056
3230
|
}
|
|
3057
3231
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ImageFieldComponent, decorators: [{
|
|
3058
3232
|
type: Component,
|
|
3059
3233
|
args: [{
|
|
3234
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
3060
3235
|
selector: 'ngx-image-field',
|
|
3061
3236
|
standalone: true,
|
|
3062
3237
|
imports: [],
|
|
@@ -3206,11 +3381,12 @@ class FileFieldComponent {
|
|
|
3206
3381
|
}
|
|
3207
3382
|
}
|
|
3208
3383
|
</div>
|
|
3209
|
-
`, isInline: true });
|
|
3384
|
+
`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3210
3385
|
}
|
|
3211
3386
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: FileFieldComponent, decorators: [{
|
|
3212
3387
|
type: Component,
|
|
3213
3388
|
args: [{
|
|
3389
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
3214
3390
|
selector: 'ngx-file-field',
|
|
3215
3391
|
standalone: true,
|
|
3216
3392
|
imports: [],
|
|
@@ -3440,7 +3616,26 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
3440
3616
|
*/
|
|
3441
3617
|
class DynamicFormComponent {
|
|
3442
3618
|
// ─── Inputs ───────────────────────────────────────────────────────────────
|
|
3443
|
-
|
|
3619
|
+
/**
|
|
3620
|
+
* The schema to render.
|
|
3621
|
+
*
|
|
3622
|
+
* An accessor pair rather than a plain field: `ngOnChanges` normalises the option shape,
|
|
3623
|
+
* and that result is kept here instead of being written back over the input. Reassigning
|
|
3624
|
+
* an `@Input` works, but it mutates what the parent handed us — and a parent using OnPush
|
|
3625
|
+
* or signals would never observe the replacement anyway.
|
|
3626
|
+
*
|
|
3627
|
+
* Reads of `this.config` get the normalised copy once it exists, so every internal caller
|
|
3628
|
+
* is unaffected.
|
|
3629
|
+
*/
|
|
3630
|
+
set config(value) {
|
|
3631
|
+
this.rawConfig = value;
|
|
3632
|
+
this.normalizedConfig = undefined; // re-normalised in ngOnChanges
|
|
3633
|
+
}
|
|
3634
|
+
get config() {
|
|
3635
|
+
return this.normalizedConfig ?? this.rawConfig;
|
|
3636
|
+
}
|
|
3637
|
+
rawConfig;
|
|
3638
|
+
normalizedConfig;
|
|
3444
3639
|
rules;
|
|
3445
3640
|
initialData;
|
|
3446
3641
|
/** Session-original values for `VALUE_CHANGED` rules. Captured from the first build when omitted. */
|
|
@@ -3474,6 +3669,11 @@ class DynamicFormComponent {
|
|
|
3474
3669
|
formReset = new EventEmitter();
|
|
3475
3670
|
/** The visible tab changed. Lets a host track it without reaching into this component. */
|
|
3476
3671
|
activeTabChange = new EventEmitter();
|
|
3672
|
+
/**
|
|
3673
|
+
* Emitted when a `beforeSave` hook aborted the save — by returning false or throwing.
|
|
3674
|
+
* `formSubmit` does not fire in that case.
|
|
3675
|
+
*/
|
|
3676
|
+
saveRejected = new EventEmitter();
|
|
3477
3677
|
// ─── Services ─────────────────────────────────────────────────────────────
|
|
3478
3678
|
fb = inject(FormBuilder);
|
|
3479
3679
|
validatorRegistry = inject(ValidatorRegistryService);
|
|
@@ -3493,6 +3693,10 @@ class DynamicFormComponent {
|
|
|
3493
3693
|
sessionBaseline = signal({});
|
|
3494
3694
|
/** Critical fields the user has explicitly unlocked for editing. */
|
|
3495
3695
|
unlockedFields = signal(new Set());
|
|
3696
|
+
/** The active tab's field panel, focused after a tab switch. */
|
|
3697
|
+
formPanel;
|
|
3698
|
+
/** The hosted field components, so an external control-state change can reach them. */
|
|
3699
|
+
fieldHosts;
|
|
3496
3700
|
// ─── Form ─────────────────────────────────────────────────────────────────
|
|
3497
3701
|
form;
|
|
3498
3702
|
valueSub;
|
|
@@ -3618,9 +3822,19 @@ class DynamicFormComponent {
|
|
|
3618
3822
|
get hasRuleErrors() {
|
|
3619
3823
|
return Object.keys(this.ruleValidationErrors).length > 0;
|
|
3620
3824
|
}
|
|
3825
|
+
/**
|
|
3826
|
+
* True while an async validator is still running.
|
|
3827
|
+
*
|
|
3828
|
+
* Angular reports a control with an outstanding async check as `pending`, and `invalid` is
|
|
3829
|
+
* false until it settles — so without this a form could be submitted in the gap before a
|
|
3830
|
+
* uniqueness check came back.
|
|
3831
|
+
*/
|
|
3832
|
+
get isValidating() {
|
|
3833
|
+
return this.form.pending;
|
|
3834
|
+
}
|
|
3621
3835
|
/** Single source of truth for both the submit guard and the button's disabled state. */
|
|
3622
3836
|
get submitBlocked() {
|
|
3623
|
-
return this.form.invalid || this.hasRuleErrors;
|
|
3837
|
+
return this.form.invalid || this.form.pending || this.hasRuleErrors;
|
|
3624
3838
|
}
|
|
3625
3839
|
ngOnInit() {
|
|
3626
3840
|
this.selectionSub = this.entityRefSelection.selection$.subscribe(({ fieldId, option }) => {
|
|
@@ -3631,12 +3845,15 @@ class DynamicFormComponent {
|
|
|
3631
3845
|
if (changes['config'] || changes['userRoles']) {
|
|
3632
3846
|
this.permissionsCache = null;
|
|
3633
3847
|
}
|
|
3634
|
-
if (changes['config'] && this.
|
|
3848
|
+
if (changes['config'] && this.rawConfig) {
|
|
3635
3849
|
// Configs arrive as plain JSON from storage or an API, where TypeScript cannot enforce
|
|
3636
3850
|
// the option shape. Normalise here, at the library boundary, so everything downstream
|
|
3637
3851
|
// can rely on an option being a LocalizedText. Returns the same object when it already
|
|
3638
3852
|
// is, so a well-formed config costs nothing.
|
|
3639
|
-
|
|
3853
|
+
// Kept in a field of our own rather than written back to the @Input. Reassigning an
|
|
3854
|
+
// input works, but it surprises a parent that holds the same object — and a parent
|
|
3855
|
+
// using OnPush or signals would not see the replacement anyway.
|
|
3856
|
+
this.normalizedConfig = normalizeConfigOptions(this.config);
|
|
3640
3857
|
}
|
|
3641
3858
|
if (changes['config'] || changes['initialData']) {
|
|
3642
3859
|
this.buildForm();
|
|
@@ -3650,9 +3867,12 @@ class DynamicFormComponent {
|
|
|
3650
3867
|
this.selectionSub?.unsubscribe();
|
|
3651
3868
|
}
|
|
3652
3869
|
setActiveTab(tabId) {
|
|
3653
|
-
|
|
3870
|
+
const changed = this.activeTab() !== tabId;
|
|
3871
|
+
if (changed)
|
|
3654
3872
|
this.activeTabChange.emit(tabId);
|
|
3655
3873
|
this.activeTab.set(tabId);
|
|
3874
|
+
if (changed)
|
|
3875
|
+
this.focusActivePanel();
|
|
3656
3876
|
const parent = findTab(this.tabs, tabId);
|
|
3657
3877
|
if (parent?.children?.length) {
|
|
3658
3878
|
this.activeSubTab.set(parent.children[0].id);
|
|
@@ -3662,7 +3882,10 @@ class DynamicFormComponent {
|
|
|
3662
3882
|
}
|
|
3663
3883
|
}
|
|
3664
3884
|
setActiveSubTab(subTabId) {
|
|
3885
|
+
const changed = this.activeSubTab() !== subTabId;
|
|
3665
3886
|
this.activeSubTab.set(subTabId);
|
|
3887
|
+
if (changed)
|
|
3888
|
+
this.focusActivePanel();
|
|
3666
3889
|
}
|
|
3667
3890
|
getFieldSpan(field) {
|
|
3668
3891
|
const span = field.colSpan ?? 12;
|
|
@@ -3770,7 +3993,8 @@ class DynamicFormComponent {
|
|
|
3770
3993
|
}
|
|
3771
3994
|
else {
|
|
3772
3995
|
const validators = this.validatorRegistry.resolveFromConfig(field.validators);
|
|
3773
|
-
|
|
3996
|
+
const asyncValidators = this.validatorRegistry.resolveAsyncFromConfig(field.validators);
|
|
3997
|
+
group[field.id] = this.fb.control({ value: field.defaultValue ?? null, disabled: field.disabled ?? false }, validators, asyncValidators);
|
|
3774
3998
|
}
|
|
3775
3999
|
}
|
|
3776
4000
|
/** One row of an `array` field: a FormGroup when the field declares columns, else a bare control. */
|
|
@@ -3801,6 +4025,16 @@ class DynamicFormComponent {
|
|
|
3801
4025
|
group.patchValue(item);
|
|
3802
4026
|
return group;
|
|
3803
4027
|
}
|
|
4028
|
+
/**
|
|
4029
|
+
* Mark every control touched, then re-check the field components.
|
|
4030
|
+
*
|
|
4031
|
+
* They are OnPush and `touched` is flipped from outside their own template, so the second
|
|
4032
|
+
* half is what makes the error messages actually appear.
|
|
4033
|
+
*/
|
|
4034
|
+
markAllTouched() {
|
|
4035
|
+
this.form.markAllAsTouched();
|
|
4036
|
+
this.fieldHosts?.forEach(host => host.refresh());
|
|
4037
|
+
}
|
|
3804
4038
|
/**
|
|
3805
4039
|
* Bring a record up to the config's `version` before it reaches the form.
|
|
3806
4040
|
*
|
|
@@ -3970,6 +4204,10 @@ class DynamicFormComponent {
|
|
|
3970
4204
|
const control = this.resolveTargetControl(autoPatch, targetId);
|
|
3971
4205
|
control?.patchValue(value);
|
|
3972
4206
|
}
|
|
4207
|
+
// Readonly targets interpolate `control.value` under OnPush. `detectChanges` on the
|
|
4208
|
+
// hosted component flushes that write immediately — `markForCheck` waits for a later
|
|
4209
|
+
// tick that a Playwright selection never schedules.
|
|
4210
|
+
this.fieldHosts?.forEach(host => host.refresh());
|
|
3973
4211
|
}
|
|
3974
4212
|
/**
|
|
3975
4213
|
* `patchOnTrue` — when a boolean/checkbox field transitions to `true`,
|
|
@@ -4115,24 +4353,70 @@ class DynamicFormComponent {
|
|
|
4115
4353
|
};
|
|
4116
4354
|
return findInGroup(this.form);
|
|
4117
4355
|
}
|
|
4356
|
+
/**
|
|
4357
|
+
* Move focus into the newly shown panel.
|
|
4358
|
+
*
|
|
4359
|
+
* Activating a tab replaces everything below it while focus stays on the tab button, so a
|
|
4360
|
+
* screen-reader or keyboard user is given no indication that the content changed and must
|
|
4361
|
+
* tab back through the strip to reach it. Deferred a frame because the panel's contents
|
|
4362
|
+
* are rendered by the change detection this call is part of.
|
|
4363
|
+
*/
|
|
4364
|
+
focusActivePanel() {
|
|
4365
|
+
if (typeof requestAnimationFrame !== 'function')
|
|
4366
|
+
return;
|
|
4367
|
+
requestAnimationFrame(() => this.formPanel?.nativeElement?.focus?.());
|
|
4368
|
+
}
|
|
4369
|
+
/** Accessible name for the field panel — it is the tabpanel for the active tab. */
|
|
4370
|
+
get activeTabLabel() {
|
|
4371
|
+
const tab = this.activeSubTabConfig ?? this.activeTabConfig;
|
|
4372
|
+
return tab ? this.resolveTabLabel(tab) : '';
|
|
4373
|
+
}
|
|
4374
|
+
/**
|
|
4375
|
+
* Why a referenced field is flagged.
|
|
4376
|
+
*
|
|
4377
|
+
* `hasDrift` was written by the builder and read by nothing at runtime, so a field whose
|
|
4378
|
+
* source definition had changed since it was linked looked entirely normal to the person
|
|
4379
|
+
* filling the form in.
|
|
4380
|
+
*/
|
|
4381
|
+
driftHint(field) {
|
|
4382
|
+
const source = field.referencedEntityKey
|
|
4383
|
+
? `"${field.referencedEntityKey}"`
|
|
4384
|
+
: 'its source entity';
|
|
4385
|
+
return `This field is linked to ${source} and its definition there has changed since it was linked.`;
|
|
4386
|
+
}
|
|
4118
4387
|
resolveTabLabel(tab) {
|
|
4119
4388
|
return resolveLabel(tab.label, this.language);
|
|
4120
4389
|
}
|
|
4121
4390
|
async submit() {
|
|
4122
4391
|
if (!this.canSubmit || this.submitBlocked) {
|
|
4123
|
-
this.
|
|
4392
|
+
this.markAllTouched();
|
|
4124
4393
|
return;
|
|
4125
4394
|
}
|
|
4126
4395
|
const rawData = this.extractRecord();
|
|
4127
4396
|
let processedData = rawData;
|
|
4128
|
-
const hookKey = `${this.config?.entity}:beforeSave`;
|
|
4129
|
-
if (this.hookRegistry.has(hookKey)) {
|
|
4130
|
-
processedData = await this.hookRegistry.run(hookKey, processedData);
|
|
4131
|
-
}
|
|
4132
4397
|
this.isSaving.set(true);
|
|
4133
4398
|
try {
|
|
4399
|
+
// beforeSave can now abort the save: returning false, or throwing, stops it. Previously
|
|
4400
|
+
// its return value simply replaced the payload and the submit proceeded regardless, so
|
|
4401
|
+
// a hook that wanted to veto — a server-side check, a confirmation — had no way to.
|
|
4402
|
+
const hookKey = `${this.config?.entity}:beforeSave`;
|
|
4403
|
+
if (this.hookRegistry.has(hookKey)) {
|
|
4404
|
+
const result = await this.hookRegistry.run(hookKey, processedData);
|
|
4405
|
+
if (result === false) {
|
|
4406
|
+
this.saveRejected.emit({ reason: 'beforeSave returned false' });
|
|
4407
|
+
return;
|
|
4408
|
+
}
|
|
4409
|
+
// Anything else is the payload to submit; `undefined` means "unchanged".
|
|
4410
|
+
if (result !== undefined && result !== true)
|
|
4411
|
+
processedData = result;
|
|
4412
|
+
}
|
|
4134
4413
|
this.formSubmit.emit(processedData);
|
|
4135
4414
|
}
|
|
4415
|
+
catch (err) {
|
|
4416
|
+
// A hook that throws aborts the save and reports why, rather than the error escaping
|
|
4417
|
+
// into an unhandled rejection with the form left looking as though it saved.
|
|
4418
|
+
this.saveRejected.emit({ reason: err instanceof Error ? err.message : String(err), error: err });
|
|
4419
|
+
}
|
|
4136
4420
|
finally {
|
|
4137
4421
|
this.isSaving.set(false);
|
|
4138
4422
|
}
|
|
@@ -4148,11 +4432,11 @@ class DynamicFormComponent {
|
|
|
4148
4432
|
this.formReset.emit();
|
|
4149
4433
|
}
|
|
4150
4434
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: DynamicFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
4151
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: DynamicFormComponent, isStandalone: true, selector: "ngx-dynamic-form", inputs: { config: "config", rules: "rules", initialData: "initialData", originalData: "originalData", userRoles: "userRoles", language: "language", readonly: "readonly", readOnlyFields: "readOnlyFields", showInfoBanners: "showInfoBanners", loading: "loading", error: "error", changeDebounceMs: "changeDebounceMs", preview: "preview" }, outputs: { formSubmit: "formSubmit", formChange: "formChange", formReset: "formReset", activeTabChange: "activeTabChange" }, host: { listeners: { "window:keydown": "handleKeyboardEvent($event)" } }, providers: [EntityRefSelectionService], usesOnChanges: true, ngImport: i0, template: "@if (canView) {\n <form [formGroup]=\"form\" (ngSubmit)=\"submit()\" class=\"ngx-form\" novalidate>\n\n <!-- Error message banner -->\n @if (error) {\n <div class=\"ngx-form__error-banner\" role=\"alert\" data-testid=\"form-error\">{{ error }}</div>\n }\n\n <!-- Rules Info Banners -->\n @for (infoKey of showInfoBanners ? Object.keys(ruleResult().infoBanners) : []; track infoKey) {\n <div class=\"ngx-form__info-banner\" role=\"status\" [attr.data-testid]=\"'info-banner-' + infoKey\">\n \u2139\uFE0F {{ ruleResult().infoBanners[infoKey] }}\n </div>\n }\n\n <!-- Rules Validation Errors & Warnings -->\n @for (errKey of Object.keys(ruleResult().validationErrors); track errKey) {\n <div class=\"ngx-form__error-banner\" role=\"alert\" [attr.data-testid]=\"'rule-error-' + errKey\">\n \u26A0\uFE0F {{ ruleResult().validationErrors[errKey] }}\n </div>\n }\n @for (warnKey of Object.keys(ruleResult().validationWarnings); track warnKey) {\n <div class=\"ngx-form__warning-banner\" role=\"status\" [attr.data-testid]=\"'rule-warning-' + warnKey\">\n \u26A1 {{ ruleResult().validationWarnings[warnKey] }}\n </div>\n }\n\n <!-- Deferred VALUE_CHANGED notice for criticalField edits (vs session baseline) -->\n @if (changedCriticalFields().length > 0) {\n <div class=\"ngx-form__warning-banner\" role=\"status\" data-testid=\"critical-change-banner\">\n \uD83D\uDD12 Critical field changed:\n @for (field of changedCriticalFields(); track field.id) {\n <span>{{ resolveFieldLabel(field) }}{{ $last ? '' : ', ' }}</span>\n }\n \u2014 this differs from the value at the start of this session.\n </div>\n }\n\n <!-- Primary Tab bar \u2014 only if visible tabs defined -->\n @if (visibleTabs.length > 0) {\n <div class=\"ngx-form__tabs\" role=\"tablist\" data-testid=\"tab-strip\">\n @for (tab of visibleTabs; track tab.id) {\n <button\n type=\"button\"\n class=\"ngx-form__tab\"\n [attr.data-testid]=\"'tab-' + tab.id\"\n [class.ngx-form__tab--active]=\"activeTab() === tab.id\"\n (click)=\"setActiveTab(tab.id)\"\n role=\"tab\"\n [attr.aria-selected]=\"activeTab() === tab.id\"\n >\n {{ resolveTabLabel(tab) }}\n </button>\n }\n </div>\n }\n\n <!-- Sub-tab bar \u2014 only if active tab has sub-tabs -->\n @if (visibleSubTabs.length > 0) {\n <div class=\"ngx-form__tabs ngx-form__tabs--sub\" role=\"tablist\" data-testid=\"subtab-strip\" style=\"margin-top: -12px; margin-bottom: 12px; font-size: 13px;\">\n @for (subTab of visibleSubTabs; track subTab.id) {\n <button\n type=\"button\"\n class=\"ngx-form__tab ngx-form__tab--sub\"\n [attr.data-testid]=\"'subtab-' + subTab.id\"\n [class.ngx-form__tab--active]=\"(activeSubTabConfig?.id) === subTab.id\"\n (click)=\"setActiveSubTab(subTab.id)\"\n role=\"tab\"\n [attr.aria-selected]=\"(activeSubTabConfig?.id) === subTab.id\"\n >\n {{ resolveTabLabel(subTab) }}\n </button>\n }\n </div>\n }\n\n
|
|
4435
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: DynamicFormComponent, isStandalone: true, selector: "ngx-dynamic-form", inputs: { config: "config", rules: "rules", initialData: "initialData", originalData: "originalData", userRoles: "userRoles", language: "language", readonly: "readonly", readOnlyFields: "readOnlyFields", showInfoBanners: "showInfoBanners", loading: "loading", error: "error", changeDebounceMs: "changeDebounceMs", preview: "preview" }, outputs: { formSubmit: "formSubmit", formChange: "formChange", formReset: "formReset", activeTabChange: "activeTabChange", saveRejected: "saveRejected" }, host: { listeners: { "window:keydown": "handleKeyboardEvent($event)" } }, providers: [EntityRefSelectionService], viewQueries: [{ propertyName: "formPanel", first: true, predicate: ["formPanel"], descendants: true }, { propertyName: "fieldHosts", predicate: DynamicFieldComponent, descendants: true }], usesOnChanges: true, ngImport: i0, template: "@if (canView) {\n <form [formGroup]=\"form\" (ngSubmit)=\"submit()\" class=\"ngx-form\" novalidate>\n\n <!-- Error message banner -->\n @if (error) {\n <div class=\"ngx-form__error-banner\" role=\"alert\" data-testid=\"form-error\">{{ error }}</div>\n }\n\n <!-- Rules Info Banners -->\n @for (infoKey of showInfoBanners ? Object.keys(ruleResult().infoBanners) : []; track infoKey) {\n <div class=\"ngx-form__info-banner\" role=\"status\" [attr.data-testid]=\"'info-banner-' + infoKey\">\n \u2139\uFE0F {{ ruleResult().infoBanners[infoKey] }}\n </div>\n }\n\n <!-- Rules Validation Errors & Warnings -->\n @for (errKey of Object.keys(ruleResult().validationErrors); track errKey) {\n <div class=\"ngx-form__error-banner\" role=\"alert\" [attr.data-testid]=\"'rule-error-' + errKey\">\n \u26A0\uFE0F {{ ruleResult().validationErrors[errKey] }}\n </div>\n }\n @for (warnKey of Object.keys(ruleResult().validationWarnings); track warnKey) {\n <div class=\"ngx-form__warning-banner\" role=\"status\" [attr.data-testid]=\"'rule-warning-' + warnKey\">\n \u26A1 {{ ruleResult().validationWarnings[warnKey] }}\n </div>\n }\n\n <!-- Deferred VALUE_CHANGED notice for criticalField edits (vs session baseline) -->\n @if (changedCriticalFields().length > 0) {\n <div class=\"ngx-form__warning-banner\" role=\"status\" data-testid=\"critical-change-banner\">\n \uD83D\uDD12 Critical field changed:\n @for (field of changedCriticalFields(); track field.id) {\n <span>{{ resolveFieldLabel(field) }}{{ $last ? '' : ', ' }}</span>\n }\n \u2014 this differs from the value at the start of this session.\n </div>\n }\n\n <!-- Primary Tab bar \u2014 only if visible tabs defined -->\n @if (visibleTabs.length > 0) {\n <div class=\"ngx-form__tabs\" role=\"tablist\" data-testid=\"tab-strip\">\n @for (tab of visibleTabs; track tab.id) {\n <button\n type=\"button\"\n class=\"ngx-form__tab\"\n [attr.data-testid]=\"'tab-' + tab.id\"\n [class.ngx-form__tab--active]=\"activeTab() === tab.id\"\n (click)=\"setActiveTab(tab.id)\"\n role=\"tab\"\n [attr.aria-selected]=\"activeTab() === tab.id\"\n >\n {{ resolveTabLabel(tab) }}\n </button>\n }\n </div>\n }\n\n <!-- Sub-tab bar \u2014 only if active tab has sub-tabs -->\n @if (visibleSubTabs.length > 0) {\n <div class=\"ngx-form__tabs ngx-form__tabs--sub\" role=\"tablist\" data-testid=\"subtab-strip\" style=\"margin-top: -12px; margin-bottom: 12px; font-size: 13px;\">\n @for (subTab of visibleSubTabs; track subTab.id) {\n <button\n type=\"button\"\n class=\"ngx-form__tab ngx-form__tab--sub\"\n [attr.data-testid]=\"'subtab-' + subTab.id\"\n [class.ngx-form__tab--active]=\"(activeSubTabConfig?.id) === subTab.id\"\n (click)=\"setActiveSubTab(subTab.id)\"\n role=\"tab\"\n [attr.aria-selected]=\"(activeSubTabConfig?.id) === subTab.id\"\n >\n {{ resolveTabLabel(subTab) }}\n </button>\n }\n </div>\n }\n\n <!--\n One tabpanel per tab, wrapping both branches. Focus moves here on a tab switch, and a\n module tab is still a tab \u2014 putting the panel semantics on only the fields branch left\n a module tab with nowhere for focus to land.\n -->\n <div\n role=\"tabpanel\"\n tabindex=\"-1\"\n #formPanel\n class=\"ngx-form__tabpanel\"\n [attr.aria-label]=\"activeTabLabel\"\n >\n @if (activeTabModuleComponent) {\n <div class=\"ngx-form__module-panel\" data-testid=\"module-panel\">\n <ng-container *ngComponentOutlet=\"activeTabModuleComponent; inputs: (activeSubTabConfig || activeTabConfig)?.moduleInputs\"></ng-container>\n </div>\n } @else {\n <!-- Fields for active tab (Responsive 12-column grid) -->\n <div class=\"ngx-form__panel\" data-testid=\"form-panel\">\n @for (field of fieldsForActiveTab; track field.id) {\n <div\n class=\"ngx-form__field-slot\"\n [style.gridColumn]=\"getFieldSpan(field)\"\n [attr.id]=\"'field-container-' + field.id\"\n >\n <ngx-dynamic-field\n [field]=\"field\"\n [control]=\"getControl(field.id, (activeSubTabConfig || activeTabConfig)?.id)!\"\n [config]=\"config\"\n [currentTabId]=\"(activeSubTabConfig || activeTabConfig)?.id\"\n [language]=\"language\"\n [readonly]=\"isFieldReadonly(field)\"\n [userRoles]=\"userRoles\"\n />\n @if (field.hasDrift) {\n <!--\n Drift was previously visible only in the builder, so a referenced field whose\n source had changed looked completely normal to the person filling the form in.\n role=\"status\" rather than \"alert\": it is information, not an error.\n -->\n <span\n class=\"ngx-field__drift\"\n role=\"status\"\n [attr.data-testid]=\"'field-' + field.id + '-drift'\"\n [title]=\"driftHint(field)\"\n >\u26A0 {{ driftHint(field) }}</span>\n }\n @if (field.criticalField && !readonly) {\n <button\n type=\"button\"\n class=\"ngx-form__lock\"\n [attr.data-testid]=\"'lock-' + field.id\"\n [attr.aria-pressed]=\"!isFieldLocked(field)\"\n [attr.aria-label]=\"\n (isFieldLocked(field) ? 'Unlock ' : 'Lock ') + resolveFieldLabel(field)\n \"\n [title]=\"\n isFieldLocked(field)\n ? 'Critical field \u2014 click to unlock for editing'\n : 'Critical field \u2014 click to lock'\n \"\n (click)=\"toggleFieldLock(field)\"\n >\n {{ isFieldLocked(field) ? '\uD83D\uDD12' : '\uD83D\uDD13' }}\n </button>\n }\n </div>\n }\n </div>\n }\n </div>\n\n <!-- Submit button \u2014 gated by RBAC and isSaving lock -->\n @if (!readonly && permissions.canEdit) {\n <div class=\"ngx-form__actions\" data-testid=\"form-actions\">\n <button\n type=\"button\"\n class=\"ngx-form__reset\"\n data-testid=\"form-reset\"\n (click)=\"reset()\"\n [disabled]=\"isSaving()\"\n >\n Reset\n </button>\n <button\n type=\"submit\"\n class=\"ngx-form__submit\"\n data-testid=\"form-submit\"\n [disabled]=\"submitBlocked\"\n title=\"Shortcut: Ctrl+S\"\n >\n @if (isSaving() || loading) {\n Saving\u2026\n } @else {\n Save\n }\n </button>\n </div>\n }\n\n </form>\n} @else {\n <!-- permissions.view denied: render nothing of the record.\n Presentational only \u2014 the server must not send data this user may not see. -->\n <div class=\"ngx-form__denied\" role=\"status\" data-testid=\"form-access-denied\">\n You do not have permission to view this record.\n </div>\n}\n", styles: [".ngx-form__panel{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:16px 20px;align-items:start}.ngx-form__field-slot{display:flex;align-items:flex-start;gap:6px;min-width:0}.ngx-form__field-slot ngx-dynamic-field{flex:1;min-width:0}.ngx-form__lock{flex:0 0 auto;margin-top:22px;background:none;border:1px solid transparent;border-radius:6px;cursor:pointer;font-size:14px;line-height:1;padding:4px 6px}.ngx-form__lock:hover{border-color:#d1d5db;background:#f9fafb}@media (max-width: 768px){.ngx-form__panel{grid-template-columns:1fr}.ngx-form__field-slot{grid-column:span 12!important}}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: DynamicFieldComponent, selector: "ngx-dynamic-field", inputs: ["field", "control", "config", "currentTabId", "language", "readonly", "userRoles"] }, { kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletContent", "ngComponentOutletNgModule", "ngComponentOutletNgModuleFactory"] }] });
|
|
4152
4436
|
}
|
|
4153
4437
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: DynamicFormComponent, decorators: [{
|
|
4154
4438
|
type: Component,
|
|
4155
|
-
args: [{ selector: 'ngx-dynamic-form', standalone: true, imports: [ReactiveFormsModule, DynamicFieldComponent, NgComponentOutlet], providers: [EntityRefSelectionService], template: "@if (canView) {\n <form [formGroup]=\"form\" (ngSubmit)=\"submit()\" class=\"ngx-form\" novalidate>\n\n <!-- Error message banner -->\n @if (error) {\n <div class=\"ngx-form__error-banner\" role=\"alert\" data-testid=\"form-error\">{{ error }}</div>\n }\n\n <!-- Rules Info Banners -->\n @for (infoKey of showInfoBanners ? Object.keys(ruleResult().infoBanners) : []; track infoKey) {\n <div class=\"ngx-form__info-banner\" role=\"status\" [attr.data-testid]=\"'info-banner-' + infoKey\">\n \u2139\uFE0F {{ ruleResult().infoBanners[infoKey] }}\n </div>\n }\n\n <!-- Rules Validation Errors & Warnings -->\n @for (errKey of Object.keys(ruleResult().validationErrors); track errKey) {\n <div class=\"ngx-form__error-banner\" role=\"alert\" [attr.data-testid]=\"'rule-error-' + errKey\">\n \u26A0\uFE0F {{ ruleResult().validationErrors[errKey] }}\n </div>\n }\n @for (warnKey of Object.keys(ruleResult().validationWarnings); track warnKey) {\n <div class=\"ngx-form__warning-banner\" role=\"status\" [attr.data-testid]=\"'rule-warning-' + warnKey\">\n \u26A1 {{ ruleResult().validationWarnings[warnKey] }}\n </div>\n }\n\n <!-- Deferred VALUE_CHANGED notice for criticalField edits (vs session baseline) -->\n @if (changedCriticalFields().length > 0) {\n <div class=\"ngx-form__warning-banner\" role=\"status\" data-testid=\"critical-change-banner\">\n \uD83D\uDD12 Critical field changed:\n @for (field of changedCriticalFields(); track field.id) {\n <span>{{ resolveFieldLabel(field) }}{{ $last ? '' : ', ' }}</span>\n }\n \u2014 this differs from the value at the start of this session.\n </div>\n }\n\n <!-- Primary Tab bar \u2014 only if visible tabs defined -->\n @if (visibleTabs.length > 0) {\n <div class=\"ngx-form__tabs\" role=\"tablist\" data-testid=\"tab-strip\">\n @for (tab of visibleTabs; track tab.id) {\n <button\n type=\"button\"\n class=\"ngx-form__tab\"\n [attr.data-testid]=\"'tab-' + tab.id\"\n [class.ngx-form__tab--active]=\"activeTab() === tab.id\"\n (click)=\"setActiveTab(tab.id)\"\n role=\"tab\"\n [attr.aria-selected]=\"activeTab() === tab.id\"\n >\n {{ resolveTabLabel(tab) }}\n </button>\n }\n </div>\n }\n\n <!-- Sub-tab bar \u2014 only if active tab has sub-tabs -->\n @if (visibleSubTabs.length > 0) {\n <div class=\"ngx-form__tabs ngx-form__tabs--sub\" role=\"tablist\" data-testid=\"subtab-strip\" style=\"margin-top: -12px; margin-bottom: 12px; font-size: 13px;\">\n @for (subTab of visibleSubTabs; track subTab.id) {\n <button\n type=\"button\"\n class=\"ngx-form__tab ngx-form__tab--sub\"\n [attr.data-testid]=\"'subtab-' + subTab.id\"\n [class.ngx-form__tab--active]=\"(activeSubTabConfig?.id) === subTab.id\"\n (click)=\"setActiveSubTab(subTab.id)\"\n role=\"tab\"\n [attr.aria-selected]=\"(activeSubTabConfig?.id) === subTab.id\"\n >\n {{ resolveTabLabel(subTab) }}\n </button>\n }\n </div>\n }\n\n
|
|
4439
|
+
args: [{ selector: 'ngx-dynamic-form', standalone: true, imports: [ReactiveFormsModule, DynamicFieldComponent, NgComponentOutlet], providers: [EntityRefSelectionService], template: "@if (canView) {\n <form [formGroup]=\"form\" (ngSubmit)=\"submit()\" class=\"ngx-form\" novalidate>\n\n <!-- Error message banner -->\n @if (error) {\n <div class=\"ngx-form__error-banner\" role=\"alert\" data-testid=\"form-error\">{{ error }}</div>\n }\n\n <!-- Rules Info Banners -->\n @for (infoKey of showInfoBanners ? Object.keys(ruleResult().infoBanners) : []; track infoKey) {\n <div class=\"ngx-form__info-banner\" role=\"status\" [attr.data-testid]=\"'info-banner-' + infoKey\">\n \u2139\uFE0F {{ ruleResult().infoBanners[infoKey] }}\n </div>\n }\n\n <!-- Rules Validation Errors & Warnings -->\n @for (errKey of Object.keys(ruleResult().validationErrors); track errKey) {\n <div class=\"ngx-form__error-banner\" role=\"alert\" [attr.data-testid]=\"'rule-error-' + errKey\">\n \u26A0\uFE0F {{ ruleResult().validationErrors[errKey] }}\n </div>\n }\n @for (warnKey of Object.keys(ruleResult().validationWarnings); track warnKey) {\n <div class=\"ngx-form__warning-banner\" role=\"status\" [attr.data-testid]=\"'rule-warning-' + warnKey\">\n \u26A1 {{ ruleResult().validationWarnings[warnKey] }}\n </div>\n }\n\n <!-- Deferred VALUE_CHANGED notice for criticalField edits (vs session baseline) -->\n @if (changedCriticalFields().length > 0) {\n <div class=\"ngx-form__warning-banner\" role=\"status\" data-testid=\"critical-change-banner\">\n \uD83D\uDD12 Critical field changed:\n @for (field of changedCriticalFields(); track field.id) {\n <span>{{ resolveFieldLabel(field) }}{{ $last ? '' : ', ' }}</span>\n }\n \u2014 this differs from the value at the start of this session.\n </div>\n }\n\n <!-- Primary Tab bar \u2014 only if visible tabs defined -->\n @if (visibleTabs.length > 0) {\n <div class=\"ngx-form__tabs\" role=\"tablist\" data-testid=\"tab-strip\">\n @for (tab of visibleTabs; track tab.id) {\n <button\n type=\"button\"\n class=\"ngx-form__tab\"\n [attr.data-testid]=\"'tab-' + tab.id\"\n [class.ngx-form__tab--active]=\"activeTab() === tab.id\"\n (click)=\"setActiveTab(tab.id)\"\n role=\"tab\"\n [attr.aria-selected]=\"activeTab() === tab.id\"\n >\n {{ resolveTabLabel(tab) }}\n </button>\n }\n </div>\n }\n\n <!-- Sub-tab bar \u2014 only if active tab has sub-tabs -->\n @if (visibleSubTabs.length > 0) {\n <div class=\"ngx-form__tabs ngx-form__tabs--sub\" role=\"tablist\" data-testid=\"subtab-strip\" style=\"margin-top: -12px; margin-bottom: 12px; font-size: 13px;\">\n @for (subTab of visibleSubTabs; track subTab.id) {\n <button\n type=\"button\"\n class=\"ngx-form__tab ngx-form__tab--sub\"\n [attr.data-testid]=\"'subtab-' + subTab.id\"\n [class.ngx-form__tab--active]=\"(activeSubTabConfig?.id) === subTab.id\"\n (click)=\"setActiveSubTab(subTab.id)\"\n role=\"tab\"\n [attr.aria-selected]=\"(activeSubTabConfig?.id) === subTab.id\"\n >\n {{ resolveTabLabel(subTab) }}\n </button>\n }\n </div>\n }\n\n <!--\n One tabpanel per tab, wrapping both branches. Focus moves here on a tab switch, and a\n module tab is still a tab \u2014 putting the panel semantics on only the fields branch left\n a module tab with nowhere for focus to land.\n -->\n <div\n role=\"tabpanel\"\n tabindex=\"-1\"\n #formPanel\n class=\"ngx-form__tabpanel\"\n [attr.aria-label]=\"activeTabLabel\"\n >\n @if (activeTabModuleComponent) {\n <div class=\"ngx-form__module-panel\" data-testid=\"module-panel\">\n <ng-container *ngComponentOutlet=\"activeTabModuleComponent; inputs: (activeSubTabConfig || activeTabConfig)?.moduleInputs\"></ng-container>\n </div>\n } @else {\n <!-- Fields for active tab (Responsive 12-column grid) -->\n <div class=\"ngx-form__panel\" data-testid=\"form-panel\">\n @for (field of fieldsForActiveTab; track field.id) {\n <div\n class=\"ngx-form__field-slot\"\n [style.gridColumn]=\"getFieldSpan(field)\"\n [attr.id]=\"'field-container-' + field.id\"\n >\n <ngx-dynamic-field\n [field]=\"field\"\n [control]=\"getControl(field.id, (activeSubTabConfig || activeTabConfig)?.id)!\"\n [config]=\"config\"\n [currentTabId]=\"(activeSubTabConfig || activeTabConfig)?.id\"\n [language]=\"language\"\n [readonly]=\"isFieldReadonly(field)\"\n [userRoles]=\"userRoles\"\n />\n @if (field.hasDrift) {\n <!--\n Drift was previously visible only in the builder, so a referenced field whose\n source had changed looked completely normal to the person filling the form in.\n role=\"status\" rather than \"alert\": it is information, not an error.\n -->\n <span\n class=\"ngx-field__drift\"\n role=\"status\"\n [attr.data-testid]=\"'field-' + field.id + '-drift'\"\n [title]=\"driftHint(field)\"\n >\u26A0 {{ driftHint(field) }}</span>\n }\n @if (field.criticalField && !readonly) {\n <button\n type=\"button\"\n class=\"ngx-form__lock\"\n [attr.data-testid]=\"'lock-' + field.id\"\n [attr.aria-pressed]=\"!isFieldLocked(field)\"\n [attr.aria-label]=\"\n (isFieldLocked(field) ? 'Unlock ' : 'Lock ') + resolveFieldLabel(field)\n \"\n [title]=\"\n isFieldLocked(field)\n ? 'Critical field \u2014 click to unlock for editing'\n : 'Critical field \u2014 click to lock'\n \"\n (click)=\"toggleFieldLock(field)\"\n >\n {{ isFieldLocked(field) ? '\uD83D\uDD12' : '\uD83D\uDD13' }}\n </button>\n }\n </div>\n }\n </div>\n }\n </div>\n\n <!-- Submit button \u2014 gated by RBAC and isSaving lock -->\n @if (!readonly && permissions.canEdit) {\n <div class=\"ngx-form__actions\" data-testid=\"form-actions\">\n <button\n type=\"button\"\n class=\"ngx-form__reset\"\n data-testid=\"form-reset\"\n (click)=\"reset()\"\n [disabled]=\"isSaving()\"\n >\n Reset\n </button>\n <button\n type=\"submit\"\n class=\"ngx-form__submit\"\n data-testid=\"form-submit\"\n [disabled]=\"submitBlocked\"\n title=\"Shortcut: Ctrl+S\"\n >\n @if (isSaving() || loading) {\n Saving\u2026\n } @else {\n Save\n }\n </button>\n </div>\n }\n\n </form>\n} @else {\n <!-- permissions.view denied: render nothing of the record.\n Presentational only \u2014 the server must not send data this user may not see. -->\n <div class=\"ngx-form__denied\" role=\"status\" data-testid=\"form-access-denied\">\n You do not have permission to view this record.\n </div>\n}\n", styles: [".ngx-form__panel{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:16px 20px;align-items:start}.ngx-form__field-slot{display:flex;align-items:flex-start;gap:6px;min-width:0}.ngx-form__field-slot ngx-dynamic-field{flex:1;min-width:0}.ngx-form__lock{flex:0 0 auto;margin-top:22px;background:none;border:1px solid transparent;border-radius:6px;cursor:pointer;font-size:14px;line-height:1;padding:4px 6px}.ngx-form__lock:hover{border-color:#d1d5db;background:#f9fafb}@media (max-width: 768px){.ngx-form__panel{grid-template-columns:1fr}.ngx-form__field-slot{grid-column:span 12!important}}\n"] }]
|
|
4156
4440
|
}], propDecorators: { config: [{
|
|
4157
4441
|
type: Input
|
|
4158
4442
|
}], rules: [{
|
|
@@ -4187,6 +4471,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
4187
4471
|
type: Output
|
|
4188
4472
|
}], activeTabChange: [{
|
|
4189
4473
|
type: Output
|
|
4474
|
+
}], saveRejected: [{
|
|
4475
|
+
type: Output
|
|
4476
|
+
}], formPanel: [{
|
|
4477
|
+
type: ViewChild,
|
|
4478
|
+
args: ['formPanel']
|
|
4479
|
+
}], fieldHosts: [{
|
|
4480
|
+
type: ViewChildren,
|
|
4481
|
+
args: [DynamicFieldComponent]
|
|
4190
4482
|
}], handleKeyboardEvent: [{
|
|
4191
4483
|
type: HostListener,
|
|
4192
4484
|
args: ['window:keydown', ['$event']]
|
|
@@ -4637,7 +4929,7 @@ class DynamicRecordFormComponent {
|
|
|
4637
4929
|
}
|
|
4638
4930
|
}
|
|
4639
4931
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: DynamicRecordFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
4640
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: DynamicRecordFormComponent, isStandalone: true, selector: "ngx-dynamic-record-form", inputs: { config: "config", rules: "rules", initialData: "initialData", userRoles: "userRoles", language: "language", readonly: "readonly", loading: "loading", error: "error", isReadOnly: "isReadOnly", readOnlyFields: "readOnlyFields", viewMode: "viewMode" }, outputs: { formSubmit: "formSubmit", formChange: "formChange", formReset: "formReset", sectionSave: "sectionSave" }, viewQueries: [{ propertyName: "dynamicFormComp", first: true, predicate: DynamicFormComponent, descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"ngx-record-editor\">\n <!-- Record Header -->\n <div class=\"ngx-record-editor__header\">\n <div class=\"ngx-record-editor__title-group\">\n @if (profileImageUrl) {\n <img\n class=\"ngx-record-editor__avatar\"\n data-testid=\"header-profile-image\"\n [src]=\"profileImageUrl\"\n [alt]=\"recordTitle\"\n />\n } @else {\n <div class=\"ngx-record-editor__avatar\" data-testid=\"header-avatar-initial\">\n {{ avatarLetter }}\n </div>\n }\n <div>\n <h2 class=\"ngx-record-editor__title\" data-testid=\"record-title\">\n {{ recordTitle }}\n </h2>\n <span class=\"ngx-record-editor__subtitle\">Entity: {{ config.entity }}</span>\n </div>\n </div>\n\n <!-- Record-level status switch, outside section editing -->\n @if (headerToggleField; as toggleField) {\n <label class=\"ngx-record-editor__header-toggle\">\n <span>{{ formatFieldLabel(toggleField) }}</span>\n <input\n type=\"checkbox\"\n role=\"switch\"\n data-testid=\"header-toggle\"\n [checked]=\"headerToggleValue\"\n [disabled]=\"recordReadOnly\"\n [attr.aria-checked]=\"headerToggleValue\"\n (change)=\"toggleHeaderStatus()\"\n />\n <span class=\"ngx-record-editor__header-toggle-text\">{{ headerToggleValue ? 'Yes' : 'No' }}</span>\n </label>\n }\n </div>\n\n <!-- Dismissible info banners (rules `info` action) -->\n @for (banner of infoBanners; track banner.fieldId) {\n <div\n class=\"ngx-record-editor__banner ngx-record-editor__banner--info\"\n role=\"status\"\n [attr.data-testid]=\"'info-banner-' + banner.fieldId\"\n >\n <span>\u2139\uFE0F {{ banner.message }}</span>\n <button\n type=\"button\"\n class=\"ngx-record-editor__banner-dismiss\"\n [attr.data-testid]=\"'dismiss-' + banner.fieldId\"\n [attr.aria-label]=\"'Dismiss: ' + banner.message\"\n (click)=\"dismissInfoBanner(banner.fieldId)\"\n >\u2715</button>\n </div>\n }\n\n <!-- Modification baseline banner -->\n @if (isModified()) {\n <div class=\"ngx-record-editor__banner ngx-record-editor__banner--warning\">\n \uD83D\uDD12 Record modified from session baseline \u2014 unsaved changes pending.\n </div>\n }\n\n <!-- Summary Panel (showOnMinimize fields with interactive jump) -->\n @if (summaryFields().length > 0) {\n <div class=\"ngx-record-editor__summary-panel\" data-testid=\"summary-panel\">\n @for (sf of summaryFields(); track sf.id) {\n <div\n class=\"ngx-record-editor__summary-item\"\n [attr.data-testid]=\"'summary-' + sf.id\"\n (click)=\"jumpToField(sf.id)\"\n [title]=\"'Click to jump to ' + formatFieldLabel(sf)\"\n >\n <span class=\"ngx-record-editor__summary-label\">{{ formatFieldLabel(sf) }}</span>\n <span class=\"ngx-record-editor__summary-val\">{{ formatFieldValue(sf) }}</span>\n </div>\n }\n </div>\n }\n\n <!-- Section edit bar: one tab is edited at a time, and validated on its own -->\n @if (viewMode && !recordReadOnly) {\n <div class=\"ngx-record-editor__section-bar\">\n @if (isEditingActiveTab) {\n <span class=\"ngx-record-editor__section-hint\">Editing this section</span>\n <button type=\"button\" class=\"ngx-form__reset\" data-testid=\"cancel-section\" (click)=\"cancelSection()\">\n Cancel\n </button>\n <button type=\"button\" class=\"ngx-form__submit\" data-testid=\"save-section\" (click)=\"saveSection()\">\n Save section\n </button>\n } @else {\n <button type=\"button\" class=\"ngx-form__reset\" data-testid=\"edit-section\" (click)=\"editSection()\">\n Edit section\n </button>\n }\n </div>\n }\n\n @if (Object.keys(sectionErrors()).length > 0) {\n <div class=\"ngx-record-editor__banner ngx-record-editor__banner--error\" role=\"alert\" data-testid=\"section-errors\">\n @for (key of Object.keys(sectionErrors()); track key) {\n <div>\u26A0\uFE0F {{ sectionErrors()[key] }}</div>\n }\n </div>\n }\n\n <!-- Array rows: listed here, edited in the drawer below -->\n @for (arrayField of arrayFieldsForActiveTab; track arrayField.id) {\n <div class=\"ngx-record-editor__rows\" [attr.data-testid]=\"'rows-' + arrayField.id\">\n <div class=\"ngx-record-editor__rows-head\">\n <span class=\"ngx-record-editor__summary-label\">{{ formatFieldLabel(arrayField) }}</span>\n @if (!sectionReadOnly) {\n <button\n type=\"button\"\n class=\"ngx-form__reset\"\n [attr.data-testid]=\"'add-row-' + arrayField.id\"\n (click)=\"openAddRow(arrayField)\"\n >+ Row</button>\n }\n </div>\n\n @for (row of rowsOf(arrayField); track $index) {\n <div class=\"ngx-record-editor__row\" [attr.data-testid]=\"'row-' + arrayField.id + '-' + $index\">\n <span class=\"ngx-record-editor__row-text\">{{ rowSummary(arrayField, row) }}</span>\n @if (!sectionReadOnly) {\n <button type=\"button\" class=\"ngx-record-editor__row-btn\"\n [attr.data-testid]=\"'edit-row-' + arrayField.id + '-' + $index\"\n (click)=\"openEditRow(arrayField, $index)\">Edit</button>\n <button type=\"button\" class=\"ngx-record-editor__row-btn\"\n [attr.data-testid]=\"'delete-row-' + arrayField.id + '-' + $index\"\n (click)=\"deleteRow(arrayField, $index)\">Delete</button>\n }\n </div>\n } @empty {\n <p class=\"ngx-record-editor__row-empty\">No rows yet.</p>\n }\n </div>\n }\n\n <!--\n Row drawer. Deliberately outside the tab panel: building a row's FormGroup inside the\n tab view races the tab's own initialisation (the reference hit this with FormArray).\n -->\n @if (inlineRowField(); as rowField) {\n <div class=\"ngx-record-editor__drawer\" data-testid=\"row-drawer\" [formGroup]=\"inlineRowForm!\">\n <div class=\"ngx-record-editor__rows-head\">\n <strong>{{ inlineRowIndex() === null ? 'Add' : 'Edit' }} {{ formatFieldLabel(rowField) }} row</strong>\n </div>\n\n <div class=\"ngx-form__panel\">\n @for (child of rowField.children ?? []; track child.id) {\n <ngx-dynamic-field\n [field]=\"child\"\n [control]=\"inlineRowForm!.get(child.id)!\"\n [config]=\"config\"\n [language]=\"language\"\n [readonly]=\"false\"\n [userRoles]=\"userRoles\"\n [style.gridColumn]=\"'span 12'\"\n />\n }\n </div>\n\n <div class=\"ngx-record-editor__section-bar\">\n <button type=\"button\" class=\"ngx-form__reset\" data-testid=\"cancel-row\" (click)=\"cancelRow()\">Cancel</button>\n <button type=\"button\" class=\"ngx-form__submit\" data-testid=\"save-row\" (click)=\"saveRow()\">Save row</button>\n </div>\n </div>\n }\n\n <!-- Main Dynamic Form -->\n <ngx-dynamic-form\n [config]=\"config\"\n [rules]=\"rules\"\n [initialData]=\"initialData\"\n [originalData]=\"originalBaseline()\"\n [userRoles]=\"userRoles\"\n [language]=\"language\"\n [readonly]=\"sectionReadOnly\"\n [readOnlyFields]=\"readOnlyFields\"\n [showInfoBanners]=\"false\"\n [loading]=\"loading\"\n [error]=\"error\"\n (formChange)=\"onFormChange($event)\"\n (formSubmit)=\"onFormSubmit($event)\"\n (formReset)=\"onFormReset()\"\n (activeTabChange)=\"onActiveTabChange($event)\"\n />\n</div>\n", styles: [".ngx-record-editor{display:flex;flex-direction:column;gap:16px;background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:20px;box-shadow:0 4px 6px -1px #0000000d}.ngx-record-editor__header{display:flex;align-items:center;justify-content:space-between;padding-bottom:16px;border-bottom:1px solid #f3f4f6}.ngx-record-editor__title-group{display:flex;align-items:center;gap:12px}.ngx-record-editor__avatar{width:48px;height:48px;border-radius:50%;object-fit:cover;background:#e0e7ff;color:#4f46e5;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:18px}.ngx-record-editor__title{font-size:20px;font-weight:700;color:#111827;margin:0}.ngx-record-editor__subtitle{font-size:13px;color:#6b7280}.ngx-record-editor__header-toggle{display:flex;align-items:center;gap:8px;font-size:13px;color:#374151}.ngx-record-editor__header-toggle-text{font-weight:600}.ngx-record-editor__banner{padding:10px 14px;border-radius:8px;font-size:13px;font-weight:500}.ngx-record-editor__banner--info{background:#eff6ff;border:1px solid #dbeafe;color:#1d4ed8;display:flex;align-items:center;justify-content:space-between;gap:12px}.ngx-record-editor__banner-dismiss{background:none;border:none;cursor:pointer;color:inherit;font-size:14px;line-height:1;padding:2px 6px;border-radius:4px}.ngx-record-editor__banner-dismiss:hover{background:#0000000f}.ngx-record-editor__banner--error{background:#fef2f2;border:1px solid #fecaca;color:#b91c1c}.ngx-record-editor__section-bar{display:flex;align-items:center;justify-content:flex-end;gap:8px}.ngx-record-editor__section-hint{margin-right:auto;font-size:12px;color:#6b7280}.ngx-record-editor__banner--warning{background:#fffbeb;border:1px solid #fef3c7;color:#b45309}.ngx-record-editor__rows{border:1px solid #f3f4f6;border-radius:8px;padding:12px 14px;display:flex;flex-direction:column;gap:6px}.ngx-record-editor__rows-head{display:flex;align-items:center;justify-content:space-between;gap:8px}.ngx-record-editor__row{display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:6px;background:#f9fafb}.ngx-record-editor__row-text{flex:1;font-size:13px;overflow-wrap:anywhere}.ngx-record-editor__row-btn{background:none;border:1px solid #e5e7eb;border-radius:6px;cursor:pointer;font-size:12px;padding:2px 8px}.ngx-record-editor__row-empty{font-size:12px;color:#6b7280;margin:0}.ngx-record-editor__drawer{border:1px solid #dbeafe;background:#f8fafc;border-radius:8px;padding:14px;display:flex;flex-direction:column;gap:12px}.ngx-record-editor__summary-panel{background:#f9fafb;border:1px solid #f3f4f6;border-radius:8px;padding:12px 16px;display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px}.ngx-record-editor__summary-item{display:flex;flex-direction:column;cursor:pointer;padding:6px 10px;border-radius:6px;transition:background .15s}.ngx-record-editor__summary-item:hover{background:#eff6ff}.ngx-record-editor__summary-label{font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:#6b7280;font-weight:600}.ngx-record-editor__summary-val{font-size:14px;font-weight:600;color:#1f2937}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: DynamicFormComponent, selector: "ngx-dynamic-form", inputs: ["config", "rules", "initialData", "originalData", "userRoles", "language", "readonly", "readOnlyFields", "showInfoBanners", "loading", "error", "changeDebounceMs", "preview"], outputs: ["formSubmit", "formChange", "formReset", "activeTabChange"] }, { kind: "component", type: DynamicFieldComponent, selector: "ngx-dynamic-field", inputs: ["field", "control", "config", "currentTabId", "language", "readonly", "userRoles"] }] });
|
|
4932
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: DynamicRecordFormComponent, isStandalone: true, selector: "ngx-dynamic-record-form", inputs: { config: "config", rules: "rules", initialData: "initialData", userRoles: "userRoles", language: "language", readonly: "readonly", loading: "loading", error: "error", isReadOnly: "isReadOnly", readOnlyFields: "readOnlyFields", viewMode: "viewMode" }, outputs: { formSubmit: "formSubmit", formChange: "formChange", formReset: "formReset", sectionSave: "sectionSave" }, viewQueries: [{ propertyName: "dynamicFormComp", first: true, predicate: DynamicFormComponent, descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"ngx-record-editor\">\n <!-- Record Header -->\n <div class=\"ngx-record-editor__header\">\n <div class=\"ngx-record-editor__title-group\">\n @if (profileImageUrl) {\n <img\n class=\"ngx-record-editor__avatar\"\n data-testid=\"header-profile-image\"\n [src]=\"profileImageUrl\"\n [alt]=\"recordTitle\"\n />\n } @else {\n <div class=\"ngx-record-editor__avatar\" data-testid=\"header-avatar-initial\">\n {{ avatarLetter }}\n </div>\n }\n <div>\n <h2 class=\"ngx-record-editor__title\" data-testid=\"record-title\">\n {{ recordTitle }}\n </h2>\n <span class=\"ngx-record-editor__subtitle\">Entity: {{ config.entity }}</span>\n </div>\n </div>\n\n <!-- Record-level status switch, outside section editing -->\n @if (headerToggleField; as toggleField) {\n <label class=\"ngx-record-editor__header-toggle\">\n <span>{{ formatFieldLabel(toggleField) }}</span>\n <input\n type=\"checkbox\"\n role=\"switch\"\n data-testid=\"header-toggle\"\n [checked]=\"headerToggleValue\"\n [disabled]=\"recordReadOnly\"\n [attr.aria-checked]=\"headerToggleValue\"\n (change)=\"toggleHeaderStatus()\"\n />\n <span class=\"ngx-record-editor__header-toggle-text\">{{ headerToggleValue ? 'Yes' : 'No' }}</span>\n </label>\n }\n </div>\n\n <!-- Dismissible info banners (rules `info` action) -->\n @for (banner of infoBanners; track banner.fieldId) {\n <div\n class=\"ngx-record-editor__banner ngx-record-editor__banner--info\"\n role=\"status\"\n [attr.data-testid]=\"'info-banner-' + banner.fieldId\"\n >\n <span>\u2139\uFE0F {{ banner.message }}</span>\n <button\n type=\"button\"\n class=\"ngx-record-editor__banner-dismiss\"\n [attr.data-testid]=\"'dismiss-' + banner.fieldId\"\n [attr.aria-label]=\"'Dismiss: ' + banner.message\"\n (click)=\"dismissInfoBanner(banner.fieldId)\"\n >\u2715</button>\n </div>\n }\n\n <!-- Modification baseline banner -->\n @if (isModified()) {\n <div class=\"ngx-record-editor__banner ngx-record-editor__banner--warning\">\n \uD83D\uDD12 Record modified from session baseline \u2014 unsaved changes pending.\n </div>\n }\n\n <!-- Summary Panel (showOnMinimize fields with interactive jump) -->\n @if (summaryFields().length > 0) {\n <div class=\"ngx-record-editor__summary-panel\" data-testid=\"summary-panel\">\n @for (sf of summaryFields(); track sf.id) {\n <div\n class=\"ngx-record-editor__summary-item\"\n [attr.data-testid]=\"'summary-' + sf.id\"\n (click)=\"jumpToField(sf.id)\"\n [title]=\"'Click to jump to ' + formatFieldLabel(sf)\"\n >\n <span class=\"ngx-record-editor__summary-label\">{{ formatFieldLabel(sf) }}</span>\n <span class=\"ngx-record-editor__summary-val\">{{ formatFieldValue(sf) }}</span>\n </div>\n }\n </div>\n }\n\n <!-- Section edit bar: one tab is edited at a time, and validated on its own -->\n @if (viewMode && !recordReadOnly) {\n <div class=\"ngx-record-editor__section-bar\">\n @if (isEditingActiveTab) {\n <span class=\"ngx-record-editor__section-hint\">Editing this section</span>\n <button type=\"button\" class=\"ngx-form__reset\" data-testid=\"cancel-section\" (click)=\"cancelSection()\">\n Cancel\n </button>\n <button type=\"button\" class=\"ngx-form__submit\" data-testid=\"save-section\" (click)=\"saveSection()\">\n Save section\n </button>\n } @else {\n <button type=\"button\" class=\"ngx-form__reset\" data-testid=\"edit-section\" (click)=\"editSection()\">\n Edit section\n </button>\n }\n </div>\n }\n\n @if (Object.keys(sectionErrors()).length > 0) {\n <div class=\"ngx-record-editor__banner ngx-record-editor__banner--error\" role=\"alert\" data-testid=\"section-errors\">\n @for (key of Object.keys(sectionErrors()); track key) {\n <div>\u26A0\uFE0F {{ sectionErrors()[key] }}</div>\n }\n </div>\n }\n\n <!-- Array rows: listed here, edited in the drawer below -->\n @for (arrayField of arrayFieldsForActiveTab; track arrayField.id) {\n <div class=\"ngx-record-editor__rows\" [attr.data-testid]=\"'rows-' + arrayField.id\">\n <div class=\"ngx-record-editor__rows-head\">\n <span class=\"ngx-record-editor__summary-label\">{{ formatFieldLabel(arrayField) }}</span>\n @if (!sectionReadOnly) {\n <button\n type=\"button\"\n class=\"ngx-form__reset\"\n [attr.data-testid]=\"'add-row-' + arrayField.id\"\n (click)=\"openAddRow(arrayField)\"\n >+ Row</button>\n }\n </div>\n\n @for (row of rowsOf(arrayField); track $index) {\n <div class=\"ngx-record-editor__row\" [attr.data-testid]=\"'row-' + arrayField.id + '-' + $index\">\n <span class=\"ngx-record-editor__row-text\">{{ rowSummary(arrayField, row) }}</span>\n @if (!sectionReadOnly) {\n <button type=\"button\" class=\"ngx-record-editor__row-btn\"\n [attr.data-testid]=\"'edit-row-' + arrayField.id + '-' + $index\"\n (click)=\"openEditRow(arrayField, $index)\">Edit</button>\n <button type=\"button\" class=\"ngx-record-editor__row-btn\"\n [attr.data-testid]=\"'delete-row-' + arrayField.id + '-' + $index\"\n (click)=\"deleteRow(arrayField, $index)\">Delete</button>\n }\n </div>\n } @empty {\n <p class=\"ngx-record-editor__row-empty\">No rows yet.</p>\n }\n </div>\n }\n\n <!--\n Row drawer. Deliberately outside the tab panel: building a row's FormGroup inside the\n tab view races the tab's own initialisation (the reference hit this with FormArray).\n -->\n @if (inlineRowField(); as rowField) {\n <div class=\"ngx-record-editor__drawer\" data-testid=\"row-drawer\" [formGroup]=\"inlineRowForm!\">\n <div class=\"ngx-record-editor__rows-head\">\n <strong>{{ inlineRowIndex() === null ? 'Add' : 'Edit' }} {{ formatFieldLabel(rowField) }} row</strong>\n </div>\n\n <div class=\"ngx-form__panel\">\n @for (child of rowField.children ?? []; track child.id) {\n <ngx-dynamic-field\n [field]=\"child\"\n [control]=\"inlineRowForm!.get(child.id)!\"\n [config]=\"config\"\n [language]=\"language\"\n [readonly]=\"false\"\n [userRoles]=\"userRoles\"\n [style.gridColumn]=\"'span 12'\"\n />\n }\n </div>\n\n <div class=\"ngx-record-editor__section-bar\">\n <button type=\"button\" class=\"ngx-form__reset\" data-testid=\"cancel-row\" (click)=\"cancelRow()\">Cancel</button>\n <button type=\"button\" class=\"ngx-form__submit\" data-testid=\"save-row\" (click)=\"saveRow()\">Save row</button>\n </div>\n </div>\n }\n\n <!-- Main Dynamic Form -->\n <ngx-dynamic-form\n [config]=\"config\"\n [rules]=\"rules\"\n [initialData]=\"initialData\"\n [originalData]=\"originalBaseline()\"\n [userRoles]=\"userRoles\"\n [language]=\"language\"\n [readonly]=\"sectionReadOnly\"\n [readOnlyFields]=\"readOnlyFields\"\n [showInfoBanners]=\"false\"\n [loading]=\"loading\"\n [error]=\"error\"\n (formChange)=\"onFormChange($event)\"\n (formSubmit)=\"onFormSubmit($event)\"\n (formReset)=\"onFormReset()\"\n (activeTabChange)=\"onActiveTabChange($event)\"\n />\n</div>\n", styles: [".ngx-record-editor{display:flex;flex-direction:column;gap:16px;background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:20px;box-shadow:0 4px 6px -1px #0000000d}.ngx-record-editor__header{display:flex;align-items:center;justify-content:space-between;padding-bottom:16px;border-bottom:1px solid #f3f4f6}.ngx-record-editor__title-group{display:flex;align-items:center;gap:12px}.ngx-record-editor__avatar{width:48px;height:48px;border-radius:50%;object-fit:cover;background:#e0e7ff;color:#4f46e5;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:18px}.ngx-record-editor__title{font-size:20px;font-weight:700;color:#111827;margin:0}.ngx-record-editor__subtitle{font-size:13px;color:#6b7280}.ngx-record-editor__header-toggle{display:flex;align-items:center;gap:8px;font-size:13px;color:#374151}.ngx-record-editor__header-toggle-text{font-weight:600}.ngx-record-editor__banner{padding:10px 14px;border-radius:8px;font-size:13px;font-weight:500}.ngx-record-editor__banner--info{background:#eff6ff;border:1px solid #dbeafe;color:#1d4ed8;display:flex;align-items:center;justify-content:space-between;gap:12px}.ngx-record-editor__banner-dismiss{background:none;border:none;cursor:pointer;color:inherit;font-size:14px;line-height:1;padding:2px 6px;border-radius:4px}.ngx-record-editor__banner-dismiss:hover{background:#0000000f}.ngx-record-editor__banner--error{background:#fef2f2;border:1px solid #fecaca;color:#b91c1c}.ngx-record-editor__section-bar{display:flex;align-items:center;justify-content:flex-end;gap:8px}.ngx-record-editor__section-hint{margin-right:auto;font-size:12px;color:#6b7280}.ngx-record-editor__banner--warning{background:#fffbeb;border:1px solid #fef3c7;color:#b45309}.ngx-record-editor__rows{border:1px solid #f3f4f6;border-radius:8px;padding:12px 14px;display:flex;flex-direction:column;gap:6px}.ngx-record-editor__rows-head{display:flex;align-items:center;justify-content:space-between;gap:8px}.ngx-record-editor__row{display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:6px;background:#f9fafb}.ngx-record-editor__row-text{flex:1;font-size:13px;overflow-wrap:anywhere}.ngx-record-editor__row-btn{background:none;border:1px solid #e5e7eb;border-radius:6px;cursor:pointer;font-size:12px;padding:2px 8px}.ngx-record-editor__row-empty{font-size:12px;color:#6b7280;margin:0}.ngx-record-editor__drawer{border:1px solid #dbeafe;background:#f8fafc;border-radius:8px;padding:14px;display:flex;flex-direction:column;gap:12px}.ngx-record-editor__summary-panel{background:#f9fafb;border:1px solid #f3f4f6;border-radius:8px;padding:12px 16px;display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px}.ngx-record-editor__summary-item{display:flex;flex-direction:column;cursor:pointer;padding:6px 10px;border-radius:6px;transition:background .15s}.ngx-record-editor__summary-item:hover{background:#eff6ff}.ngx-record-editor__summary-label{font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:#6b7280;font-weight:600}.ngx-record-editor__summary-val{font-size:14px;font-weight:600;color:#1f2937}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: DynamicFormComponent, selector: "ngx-dynamic-form", inputs: ["config", "rules", "initialData", "originalData", "userRoles", "language", "readonly", "readOnlyFields", "showInfoBanners", "loading", "error", "changeDebounceMs", "preview"], outputs: ["formSubmit", "formChange", "formReset", "activeTabChange", "saveRejected"] }, { kind: "component", type: DynamicFieldComponent, selector: "ngx-dynamic-field", inputs: ["field", "control", "config", "currentTabId", "language", "readonly", "userRoles"] }] });
|
|
4641
4933
|
}
|
|
4642
4934
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: DynamicRecordFormComponent, decorators: [{
|
|
4643
4935
|
type: Component,
|
|
@@ -4683,5 +4975,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
4683
4975
|
* Generated bundle index. Do not edit.
|
|
4684
4976
|
*/
|
|
4685
4977
|
|
|
4686
|
-
export { ArrayFieldComponent, BooleanFieldComponent, COMMON_MODULES_REGISTRY, CONFIG_SOURCE, CascadeDataService, CheckboxFieldComponent, ConfigSourceService, CurrencyFieldComponent, DateFieldComponent, DropdownFieldComponent, DynamicFieldComponent, DynamicFormComponent, DynamicRecordFormComponent, ENTITY_REF_CACHE_STORE, ENTITY_REF_REGISTRY, EmailFieldComponent, EntityRefFieldComponent, EntityRefRegistryService, EntityRefSelectionService, EntityReferenceService, FIELD_TYPE_REGISTRY, FIELD_TYPE_SETS, FieldRegistryService, FileFieldComponent, GroupFieldComponent, HOOK_REGISTRY, HookRegistryService, ImageFieldComponent, InMemoryEntityRefCacheStore, LOOKUP_REGISTRY, LookupRegistryService, MASKED_ROLES, MonthYearFieldComponent, MultiSelectFieldComponent, NumberFieldComponent, PasswordFieldComponent, RECORD_MIGRATIONS, RadioFieldComponent, RbacService, RulesEvaluationService, SYSTEM_DEFAULT_CAN_EDIT, TextFieldComponent, TextareaFieldComponent, UPLOAD_HANDLER, VALIDATOR_REGISTRY, ValidatorRegistryService, builtInFieldTypes, provideBuiltInFieldTypes, provideFieldTypes, provideNgxDynamicEntity, refreshChoiceOptions };
|
|
4978
|
+
export { ASYNC_VALIDATOR_REGISTRY, ArrayFieldComponent, BooleanFieldComponent, COMMON_MODULES_REGISTRY, CONFIG_SOURCE, CascadeDataService, CheckboxFieldComponent, ConfigSourceService, CurrencyFieldComponent, DEFAULT_VALIDATION_MESSAGES, DateFieldComponent, DropdownFieldComponent, DynamicFieldComponent, DynamicFormComponent, DynamicRecordFormComponent, ENTITY_REF_CACHE_STORE, ENTITY_REF_REGISTRY, EmailFieldComponent, EntityRefFieldComponent, EntityRefRegistryService, EntityRefSelectionService, EntityReferenceService, FIELD_TYPE_REGISTRY, FIELD_TYPE_SETS, FieldRegistryService, FileFieldComponent, GroupFieldComponent, HOOK_REGISTRY, HookRegistryService, ImageFieldComponent, InMemoryEntityRefCacheStore, LOOKUP_REGISTRY, LookupRegistryService, MASKED_ROLES, MonthYearFieldComponent, MultiSelectFieldComponent, NumberFieldComponent, PasswordFieldComponent, RECORD_MIGRATIONS, RadioFieldComponent, RbacService, RulesEvaluationService, SYSTEM_DEFAULT_CAN_EDIT, TextFieldComponent, TextareaFieldComponent, UPLOAD_HANDLER, VALIDATION_MESSAGES, VALIDATOR_REGISTRY, ValidationMessagesService, ValidatorRegistryService, builtInFieldTypes, provideBuiltInFieldTypes, provideFieldTypes, provideNgxDynamicEntity, refreshChoiceOptions };
|
|
4687
4979
|
//# sourceMappingURL=ngx-dynamic-entity.mjs.map
|