ngx-dynamic-entity 1.0.0 → 1.1.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.
Files changed (47) hide show
  1. package/README.md +129 -139
  2. package/esm2022/lib/field-types/array-field.component.mjs +101 -101
  3. package/esm2022/lib/field-types/boolean-field.component.mjs +59 -59
  4. package/esm2022/lib/field-types/checkbox-field.component.mjs +45 -45
  5. package/esm2022/lib/field-types/currency-field.component.mjs +51 -51
  6. package/esm2022/lib/field-types/date-field.component.mjs +43 -43
  7. package/esm2022/lib/field-types/dropdown-field.component.mjs +81 -81
  8. package/esm2022/lib/field-types/email-field.component.mjs +61 -61
  9. package/esm2022/lib/field-types/entity-ref-field.component.mjs +67 -67
  10. package/esm2022/lib/field-types/file-field.component.mjs +85 -85
  11. package/esm2022/lib/field-types/group-field.component.mjs +47 -47
  12. package/esm2022/lib/field-types/image-field.component.mjs +89 -89
  13. package/esm2022/lib/field-types/month-year-field.component.mjs +77 -77
  14. package/esm2022/lib/field-types/multi-select-field.component.mjs +63 -63
  15. package/esm2022/lib/field-types/number-field.component.mjs +65 -65
  16. package/esm2022/lib/field-types/password-field.component.mjs +65 -65
  17. package/esm2022/lib/field-types/radio-field.component.mjs +63 -63
  18. package/esm2022/lib/field-types/text-field.component.mjs +59 -59
  19. package/esm2022/lib/field-types/textarea-field.component.mjs +45 -45
  20. package/esm2022/lib/form/dynamic-field/dynamic-field.component.mjs +1 -1
  21. package/esm2022/lib/form/dynamic-form.component.mjs +183 -10
  22. package/esm2022/lib/form/dynamic-record-form.component.mjs +36 -11
  23. package/esm2022/lib/providers/provide-field-types.mjs +1 -1
  24. package/esm2022/lib/providers/provide-ngx-dynamic-entity.mjs +3 -2
  25. package/esm2022/lib/services/cascade-data.service.mjs +1 -1
  26. package/esm2022/lib/services/entity-ref-cache.mjs +1 -1
  27. package/esm2022/lib/services/entity-ref-registry.service.mjs +1 -1
  28. package/esm2022/lib/services/entity-ref-selection.service.mjs +1 -1
  29. package/esm2022/lib/services/entity-reference.service.mjs +1 -1
  30. package/esm2022/lib/services/field-registry.service.mjs +1 -1
  31. package/esm2022/lib/services/file-upload.service.mjs +1 -1
  32. package/esm2022/lib/services/rbac.service.mjs +1 -1
  33. package/esm2022/lib/services/validator-registry.service.mjs +12 -1
  34. package/esm2022/lib/tokens/injection-tokens.mjs +17 -1
  35. package/esm2022/public-api.mjs +18 -2
  36. package/fesm2022/ngx-dynamic-entity.mjs +1392 -1166
  37. package/fesm2022/ngx-dynamic-entity.mjs.map +1 -1
  38. package/lib/field-types/dropdown-field.component.d.ts +1 -1
  39. package/lib/field-types/entity-ref-field.component.d.ts +1 -1
  40. package/lib/field-types/multi-select-field.component.d.ts +1 -1
  41. package/lib/field-types/radio-field.component.d.ts +1 -1
  42. package/lib/form/dynamic-form.component.d.ts +93 -6
  43. package/lib/form/dynamic-record-form.component.d.ts +13 -2
  44. package/lib/providers/provide-ngx-dynamic-entity.d.ts +8 -2
  45. package/lib/tokens/injection-tokens.d.ts +24 -2
  46. package/package.json +17 -18
  47. package/public-api.d.ts +10 -0
@@ -2,7 +2,7 @@ import * as i0 from '@angular/core';
2
2
  import { InjectionToken, makeEnvironmentProviders, Input, Component, signal, inject, Injectable, DestroyRef, isDevMode, ViewContainerRef, ViewChild, forwardRef, Optional, Inject, EventEmitter, computed, HostListener, Output } from '@angular/core';
3
3
  import * as i1 from '@angular/forms';
4
4
  import { ReactiveFormsModule, Validators, FormBuilder, FormArray, FormGroup } from '@angular/forms';
5
- import { resolveLabel, normalizeLookupValues, lookupValuesToOptions, valuesMatch, getOptionStoredValue, resolveOptionLabel, resolveOptionValue, buildReferenceCacheKey, normalizeReferenceOptions, entityKeyFromCacheKey, applyCascadeFilter, resolveEffectiveMask, shouldMaskField, findTab, fileRefName, evaluateFormRules, filterRulesForTab, evaluateFieldVisibility, normalizeConfigOptions, getTabData, getValueByPath, setTabData, setValueByPath, normalizeArrayStructures, getTabPath, applyAutoPatch, applyPatchOnTrue, formatDisplayValue } from '@dynamic-entity/core';
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';
6
6
  export { formatDisplayValue, normalizeConfig, normalizeOption, resolveLabel, resolveOptionLabel, resolveOptionValue, valuesMatch } from '@dynamic-entity/core';
7
7
  import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
8
8
  import { Subject, isObservable, firstValueFrom } from 'rxjs';
@@ -70,6 +70,22 @@ const ENTITY_REF_REGISTRY = new InjectionToken('ENTITY_REF_REGISTRY');
70
70
  * })
71
71
  */
72
72
  const LOOKUP_REGISTRY = new InjectionToken('LOOKUP_REGISTRY');
73
+ /**
74
+ * Ordered steps that move a saved record forward when a config's `version` increases.
75
+ *
76
+ * Without these, `EntityFormConfig.version` and `VersionedRecord._configVersion` are
77
+ * declarations nothing acts on: a schema can be edited freely while records keep their old
78
+ * shape, and nothing reconciles the two. Registering migrations makes the version numbers
79
+ * mean something.
80
+ *
81
+ * @example
82
+ * provideNgxDynamicEntity({
83
+ * migrations: [
84
+ * { from: 1, to: 2, description: 'split name', migrate: r => ({ ...r, firstName: r['name'] }) },
85
+ * ],
86
+ * })
87
+ */
88
+ const RECORD_MIGRATIONS = new InjectionToken('RECORD_MIGRATIONS');
73
89
  /** Registry: validator key → ValidatorFn */
74
90
  const VALIDATOR_REGISTRY = new InjectionToken('VALIDATOR_REGISTRY');
75
91
  /** Registry: hook key → async (data, context) => data */
@@ -141,6 +157,7 @@ const provideNgxDynamicEntity = (config = {}) => makeEnvironmentProviders([
141
157
  { provide: LOOKUP_REGISTRY, useValue: new Map(Object.entries(config.lookups ?? {})) },
142
158
  { provide: VALIDATOR_REGISTRY, useValue: new Map(Object.entries(config.validators ?? {})) },
143
159
  { provide: HOOK_REGISTRY, useValue: new Map(Object.entries(config.hooks ?? {})) },
160
+ { provide: RECORD_MIGRATIONS, useValue: config.migrations ?? [] },
144
161
  ]);
145
162
 
146
163
  class TextFieldComponent {
@@ -172,35 +189,35 @@ class TextFieldComponent {
172
189
  return 'Invalid value.';
173
190
  }
174
191
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: TextFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
175
- 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: `
176
- <div
177
- class="ngx-field ngx-field--text"
178
- [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type"
179
- [class.ngx-field--readonly]="readonly"
180
- [class.ngx-field--masked]="masked"
181
- [class.ngx-field--invalid]="control && control.invalid && control.touched"
182
- >
183
- <label class="ngx-field__label" [attr.for]="field.id">{{ label }}@if (field.validators?.required) { <span class="ngx-field__req">*</span> }</label>
184
- @if (masked) {
185
- <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
186
- } @else if (readonly) {
187
- <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ control.value ?? '—' }}</span>
188
- } @else {
189
- <input
190
- [id]="field.id"
191
- type="text"
192
- class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
193
- [formControl]="$any(control)"
194
- [placeholder]="placeholder"
195
- [attr.disabled]="field.disabled ? true : null"
196
- [attr.aria-invalid]="control.invalid && control.touched"
197
- [attr.aria-describedby]="errorMessage ? field.id + '-error' : null"
198
- />
199
- @if (errorMessage) {
200
- <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'" [id]="field.id + '-error'" role="alert">{{ errorMessage }}</span>
201
- }
202
- }
203
- </div>
192
+ 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: `
193
+ <div
194
+ class="ngx-field ngx-field--text"
195
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type"
196
+ [class.ngx-field--readonly]="readonly"
197
+ [class.ngx-field--masked]="masked"
198
+ [class.ngx-field--invalid]="control && control.invalid && control.touched"
199
+ >
200
+ <label class="ngx-field__label" [attr.for]="field.id">{{ label }}@if (field.validators?.required) { <span class="ngx-field__req">*</span> }</label>
201
+ @if (masked) {
202
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
203
+ } @else if (readonly) {
204
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ control.value ?? '—' }}</span>
205
+ } @else {
206
+ <input
207
+ [id]="field.id"
208
+ type="text"
209
+ class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
210
+ [formControl]="$any(control)"
211
+ [placeholder]="placeholder"
212
+ [attr.disabled]="field.disabled ? true : null"
213
+ [attr.aria-invalid]="control.invalid && control.touched"
214
+ [attr.aria-describedby]="errorMessage ? field.id + '-error' : null"
215
+ />
216
+ @if (errorMessage) {
217
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'" [id]="field.id + '-error'" role="alert">{{ errorMessage }}</span>
218
+ }
219
+ }
220
+ </div>
204
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"] }] });
205
222
  }
206
223
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: TextFieldComponent, decorators: [{
@@ -209,35 +226,35 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
209
226
  selector: 'ngx-text-field',
210
227
  standalone: true,
211
228
  imports: [ReactiveFormsModule],
212
- template: `
213
- <div
214
- class="ngx-field ngx-field--text"
215
- [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type"
216
- [class.ngx-field--readonly]="readonly"
217
- [class.ngx-field--masked]="masked"
218
- [class.ngx-field--invalid]="control && control.invalid && control.touched"
219
- >
220
- <label class="ngx-field__label" [attr.for]="field.id">{{ label }}@if (field.validators?.required) { <span class="ngx-field__req">*</span> }</label>
221
- @if (masked) {
222
- <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
223
- } @else if (readonly) {
224
- <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ control.value ?? '—' }}</span>
225
- } @else {
226
- <input
227
- [id]="field.id"
228
- type="text"
229
- class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
230
- [formControl]="$any(control)"
231
- [placeholder]="placeholder"
232
- [attr.disabled]="field.disabled ? true : null"
233
- [attr.aria-invalid]="control.invalid && control.touched"
234
- [attr.aria-describedby]="errorMessage ? field.id + '-error' : null"
235
- />
236
- @if (errorMessage) {
237
- <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'" [id]="field.id + '-error'" role="alert">{{ errorMessage }}</span>
238
- }
239
- }
240
- </div>
229
+ template: `
230
+ <div
231
+ class="ngx-field ngx-field--text"
232
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type"
233
+ [class.ngx-field--readonly]="readonly"
234
+ [class.ngx-field--masked]="masked"
235
+ [class.ngx-field--invalid]="control && control.invalid && control.touched"
236
+ >
237
+ <label class="ngx-field__label" [attr.for]="field.id">{{ label }}@if (field.validators?.required) { <span class="ngx-field__req">*</span> }</label>
238
+ @if (masked) {
239
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
240
+ } @else if (readonly) {
241
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ control.value ?? '—' }}</span>
242
+ } @else {
243
+ <input
244
+ [id]="field.id"
245
+ type="text"
246
+ class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
247
+ [formControl]="$any(control)"
248
+ [placeholder]="placeholder"
249
+ [attr.disabled]="field.disabled ? true : null"
250
+ [attr.aria-invalid]="control.invalid && control.touched"
251
+ [attr.aria-describedby]="errorMessage ? field.id + '-error' : null"
252
+ />
253
+ @if (errorMessage) {
254
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'" [id]="field.id + '-error'" role="alert">{{ errorMessage }}</span>
255
+ }
256
+ }
257
+ </div>
241
258
  `,
242
259
  }]
243
260
  }], propDecorators: { field: [{
@@ -265,28 +282,28 @@ class TextareaFieldComponent {
265
282
  return resolveLabel(this.field?.placeholder, this.language);
266
283
  }
267
284
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: TextareaFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
268
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: TextareaFieldComponent, isStandalone: true, selector: "ngx-textarea-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
269
- <div class="ngx-field ngx-field--textarea"
270
- [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
271
- <label class="ngx-field__label" [attr.for]="field.id">{{ label }}</label>
272
- @if (masked) {
273
- <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
274
- } @else if (readonly) {
275
- <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ control.value }}</span>
276
- } @else {
277
- <textarea
278
- [id]="field.id"
279
- class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
280
- [formControl]="$any(control)"
281
- [placeholder]="placeholder"
282
- [attr.disabled]="field.disabled ? true : null"
283
- rows="3"
284
- ></textarea>
285
- @if (control.invalid && control.touched) {
286
- <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
287
- }
288
- }
289
- </div>
285
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: TextareaFieldComponent, isStandalone: true, selector: "ngx-textarea-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
286
+ <div class="ngx-field ngx-field--textarea"
287
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
288
+ <label class="ngx-field__label" [attr.for]="field.id">{{ label }}</label>
289
+ @if (masked) {
290
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
291
+ } @else if (readonly) {
292
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ control.value }}</span>
293
+ } @else {
294
+ <textarea
295
+ [id]="field.id"
296
+ class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
297
+ [formControl]="$any(control)"
298
+ [placeholder]="placeholder"
299
+ [attr.disabled]="field.disabled ? true : null"
300
+ rows="3"
301
+ ></textarea>
302
+ @if (control.invalid && control.touched) {
303
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
304
+ }
305
+ }
306
+ </div>
290
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"] }] });
291
308
  }
292
309
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: TextareaFieldComponent, decorators: [{
@@ -295,28 +312,28 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
295
312
  selector: 'ngx-textarea-field',
296
313
  standalone: true,
297
314
  imports: [ReactiveFormsModule],
298
- template: `
299
- <div class="ngx-field ngx-field--textarea"
300
- [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
301
- <label class="ngx-field__label" [attr.for]="field.id">{{ label }}</label>
302
- @if (masked) {
303
- <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
304
- } @else if (readonly) {
305
- <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ control.value }}</span>
306
- } @else {
307
- <textarea
308
- [id]="field.id"
309
- class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
310
- [formControl]="$any(control)"
311
- [placeholder]="placeholder"
312
- [attr.disabled]="field.disabled ? true : null"
313
- rows="3"
314
- ></textarea>
315
- @if (control.invalid && control.touched) {
316
- <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
317
- }
318
- }
319
- </div>
315
+ template: `
316
+ <div class="ngx-field ngx-field--textarea"
317
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
318
+ <label class="ngx-field__label" [attr.for]="field.id">{{ label }}</label>
319
+ @if (masked) {
320
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
321
+ } @else if (readonly) {
322
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ control.value }}</span>
323
+ } @else {
324
+ <textarea
325
+ [id]="field.id"
326
+ class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
327
+ [formControl]="$any(control)"
328
+ [placeholder]="placeholder"
329
+ [attr.disabled]="field.disabled ? true : null"
330
+ rows="3"
331
+ ></textarea>
332
+ @if (control.invalid && control.touched) {
333
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
334
+ }
335
+ }
336
+ </div>
320
337
  `,
321
338
  }]
322
339
  }], propDecorators: { field: [{
@@ -356,38 +373,38 @@ class NumberFieldComponent {
356
373
  return 'Invalid number.';
357
374
  }
358
375
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: NumberFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
359
- 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: `
360
- <div
361
- class="ngx-field ngx-field--number"
362
- [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type"
363
- [class.ngx-field--readonly]="readonly"
364
- [class.ngx-field--masked]="masked"
365
- [class.ngx-field--invalid]="control && control.invalid && control.touched"
366
- >
367
- <label class="ngx-field__label" [attr.for]="field.id">
368
- {{ label }}
369
- @if (field.validators?.required) { <span class="ngx-field__req">*</span> }
370
- </label>
371
- @if (masked) {
372
- <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
373
- } @else if (readonly) {
374
- <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ control.value ?? '—' }}</span>
375
- } @else {
376
- <input
377
- [id]="field.id"
378
- type="number"
379
- class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
380
- [formControl]="$any(control)"
381
- [placeholder]="placeholder"
382
- [attr.disabled]="field.disabled ? true : null"
383
- [attr.aria-invalid]="control.invalid && control.touched"
384
- [attr.aria-describedby]="errorMessage ? field.id + '-error' : null"
385
- />
386
- @if (errorMessage) {
387
- <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'" [id]="field.id + '-error'" role="alert">{{ errorMessage }}</span>
388
- }
389
- }
390
- </div>
376
+ 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: `
377
+ <div
378
+ class="ngx-field ngx-field--number"
379
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type"
380
+ [class.ngx-field--readonly]="readonly"
381
+ [class.ngx-field--masked]="masked"
382
+ [class.ngx-field--invalid]="control && control.invalid && control.touched"
383
+ >
384
+ <label class="ngx-field__label" [attr.for]="field.id">
385
+ {{ label }}
386
+ @if (field.validators?.required) { <span class="ngx-field__req">*</span> }
387
+ </label>
388
+ @if (masked) {
389
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
390
+ } @else if (readonly) {
391
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ control.value ?? '—' }}</span>
392
+ } @else {
393
+ <input
394
+ [id]="field.id"
395
+ type="number"
396
+ class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
397
+ [formControl]="$any(control)"
398
+ [placeholder]="placeholder"
399
+ [attr.disabled]="field.disabled ? true : null"
400
+ [attr.aria-invalid]="control.invalid && control.touched"
401
+ [attr.aria-describedby]="errorMessage ? field.id + '-error' : null"
402
+ />
403
+ @if (errorMessage) {
404
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'" [id]="field.id + '-error'" role="alert">{{ errorMessage }}</span>
405
+ }
406
+ }
407
+ </div>
391
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"] }] });
392
409
  }
393
410
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: NumberFieldComponent, decorators: [{
@@ -396,38 +413,38 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
396
413
  selector: 'ngx-number-field',
397
414
  standalone: true,
398
415
  imports: [ReactiveFormsModule],
399
- template: `
400
- <div
401
- class="ngx-field ngx-field--number"
402
- [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type"
403
- [class.ngx-field--readonly]="readonly"
404
- [class.ngx-field--masked]="masked"
405
- [class.ngx-field--invalid]="control && control.invalid && control.touched"
406
- >
407
- <label class="ngx-field__label" [attr.for]="field.id">
408
- {{ label }}
409
- @if (field.validators?.required) { <span class="ngx-field__req">*</span> }
410
- </label>
411
- @if (masked) {
412
- <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
413
- } @else if (readonly) {
414
- <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ control.value ?? '—' }}</span>
415
- } @else {
416
- <input
417
- [id]="field.id"
418
- type="number"
419
- class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
420
- [formControl]="$any(control)"
421
- [placeholder]="placeholder"
422
- [attr.disabled]="field.disabled ? true : null"
423
- [attr.aria-invalid]="control.invalid && control.touched"
424
- [attr.aria-describedby]="errorMessage ? field.id + '-error' : null"
425
- />
426
- @if (errorMessage) {
427
- <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'" [id]="field.id + '-error'" role="alert">{{ errorMessage }}</span>
428
- }
429
- }
430
- </div>
416
+ template: `
417
+ <div
418
+ class="ngx-field ngx-field--number"
419
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type"
420
+ [class.ngx-field--readonly]="readonly"
421
+ [class.ngx-field--masked]="masked"
422
+ [class.ngx-field--invalid]="control && control.invalid && control.touched"
423
+ >
424
+ <label class="ngx-field__label" [attr.for]="field.id">
425
+ {{ label }}
426
+ @if (field.validators?.required) { <span class="ngx-field__req">*</span> }
427
+ </label>
428
+ @if (masked) {
429
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
430
+ } @else if (readonly) {
431
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ control.value ?? '—' }}</span>
432
+ } @else {
433
+ <input
434
+ [id]="field.id"
435
+ type="number"
436
+ class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
437
+ [formControl]="$any(control)"
438
+ [placeholder]="placeholder"
439
+ [attr.disabled]="field.disabled ? true : null"
440
+ [attr.aria-invalid]="control.invalid && control.touched"
441
+ [attr.aria-describedby]="errorMessage ? field.id + '-error' : null"
442
+ />
443
+ @if (errorMessage) {
444
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'" [id]="field.id + '-error'" role="alert">{{ errorMessage }}</span>
445
+ }
446
+ }
447
+ </div>
431
448
  `,
432
449
  }]
433
450
  }], propDecorators: { field: [{
@@ -466,31 +483,31 @@ class CurrencyFieldComponent {
466
483
  }
467
484
  }
468
485
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: CurrencyFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
469
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: CurrencyFieldComponent, isStandalone: true, selector: "ngx-currency-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
470
- <div class="ngx-field ngx-field--currency"
471
- [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
472
- <label class="ngx-field__label">{{ label }}</label>
473
- @if (masked) {
474
- <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
475
- } @else if (readonly) {
476
- <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ symbol }}{{ control.value }}</span>
477
- } @else {
478
- <div class="ngx-field__currency-wrap">
479
- <span class="ngx-field__currency-symbol">{{ symbol }}</span>
480
- <input
481
- class="ngx-field__input ngx-field__input--currency" [attr.data-testid]="'field-' + field.id + '-input'"
482
- type="number"
483
- step="0.01"
484
- [formControl]="$any(control)"
485
- [placeholder]="placeholder || '0.00'"
486
- [attr.disabled]="field.disabled ? true : null"
487
- />
488
- </div>
489
- @if (control.invalid && control.touched) {
490
- <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
491
- }
492
- }
493
- </div>
486
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: CurrencyFieldComponent, isStandalone: true, selector: "ngx-currency-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
487
+ <div class="ngx-field ngx-field--currency"
488
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
489
+ <label class="ngx-field__label">{{ label }}</label>
490
+ @if (masked) {
491
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
492
+ } @else if (readonly) {
493
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ symbol }}{{ control.value }}</span>
494
+ } @else {
495
+ <div class="ngx-field__currency-wrap">
496
+ <span class="ngx-field__currency-symbol">{{ symbol }}</span>
497
+ <input
498
+ class="ngx-field__input ngx-field__input--currency" [attr.data-testid]="'field-' + field.id + '-input'"
499
+ type="number"
500
+ step="0.01"
501
+ [formControl]="$any(control)"
502
+ [placeholder]="placeholder || '0.00'"
503
+ [attr.disabled]="field.disabled ? true : null"
504
+ />
505
+ </div>
506
+ @if (control.invalid && control.touched) {
507
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
508
+ }
509
+ }
510
+ </div>
494
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"] }] });
495
512
  }
496
513
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: CurrencyFieldComponent, decorators: [{
@@ -499,31 +516,31 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
499
516
  selector: 'ngx-currency-field',
500
517
  standalone: true,
501
518
  imports: [ReactiveFormsModule],
502
- template: `
503
- <div class="ngx-field ngx-field--currency"
504
- [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
505
- <label class="ngx-field__label">{{ label }}</label>
506
- @if (masked) {
507
- <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
508
- } @else if (readonly) {
509
- <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ symbol }}{{ control.value }}</span>
510
- } @else {
511
- <div class="ngx-field__currency-wrap">
512
- <span class="ngx-field__currency-symbol">{{ symbol }}</span>
513
- <input
514
- class="ngx-field__input ngx-field__input--currency" [attr.data-testid]="'field-' + field.id + '-input'"
515
- type="number"
516
- step="0.01"
517
- [formControl]="$any(control)"
518
- [placeholder]="placeholder || '0.00'"
519
- [attr.disabled]="field.disabled ? true : null"
520
- />
521
- </div>
522
- @if (control.invalid && control.touched) {
523
- <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
524
- }
525
- }
526
- </div>
519
+ template: `
520
+ <div class="ngx-field ngx-field--currency"
521
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
522
+ <label class="ngx-field__label">{{ label }}</label>
523
+ @if (masked) {
524
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
525
+ } @else if (readonly) {
526
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ symbol }}{{ control.value }}</span>
527
+ } @else {
528
+ <div class="ngx-field__currency-wrap">
529
+ <span class="ngx-field__currency-symbol">{{ symbol }}</span>
530
+ <input
531
+ class="ngx-field__input ngx-field__input--currency" [attr.data-testid]="'field-' + field.id + '-input'"
532
+ type="number"
533
+ step="0.01"
534
+ [formControl]="$any(control)"
535
+ [placeholder]="placeholder || '0.00'"
536
+ [attr.disabled]="field.disabled ? true : null"
537
+ />
538
+ </div>
539
+ @if (control.invalid && control.touched) {
540
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
541
+ }
542
+ }
543
+ </div>
527
544
  `,
528
545
  }]
529
546
  }], propDecorators: { field: [{
@@ -552,36 +569,36 @@ class EmailFieldComponent {
552
569
  return resolveLabel(this.field?.placeholder, this.language);
553
570
  }
554
571
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: EmailFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
555
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: EmailFieldComponent, isStandalone: true, selector: "ngx-email-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
556
- <div class="ngx-field ngx-field--email"
557
- [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
558
- <label class="ngx-field__label">{{ label }}</label>
559
- @if (masked) {
560
- <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
561
- } @else if (readonly) {
562
- <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">
563
- <a [href]="'mailto:' + control.value" class="ngx-field__email-link">{{ control.value || '—' }}</a>
564
- </span>
565
- } @else {
566
- <input
567
- class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
568
- type="email"
569
- autocomplete="email"
570
- [formControl]="$any(control)"
571
- [placeholder]="placeholder || 'you@example.com'"
572
- [attr.disabled]="field.disabled ? true : null"
573
- />
574
- @if (control.invalid && control.touched) {
575
- @if (control.errors?.['required']) {
576
- <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">Email is required</span>
577
- } @else if (control.errors?.['email']) {
578
- <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">Enter a valid email address</span>
579
- } @else {
580
- <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
581
- }
582
- }
583
- }
584
- </div>
572
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: EmailFieldComponent, isStandalone: true, selector: "ngx-email-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
573
+ <div class="ngx-field ngx-field--email"
574
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
575
+ <label class="ngx-field__label">{{ label }}</label>
576
+ @if (masked) {
577
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
578
+ } @else if (readonly) {
579
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">
580
+ <a [href]="'mailto:' + control.value" class="ngx-field__email-link">{{ control.value || '—' }}</a>
581
+ </span>
582
+ } @else {
583
+ <input
584
+ class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
585
+ type="email"
586
+ autocomplete="email"
587
+ [formControl]="$any(control)"
588
+ [placeholder]="placeholder || 'you@example.com'"
589
+ [attr.disabled]="field.disabled ? true : null"
590
+ />
591
+ @if (control.invalid && control.touched) {
592
+ @if (control.errors?.['required']) {
593
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">Email is required</span>
594
+ } @else if (control.errors?.['email']) {
595
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">Enter a valid email address</span>
596
+ } @else {
597
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
598
+ }
599
+ }
600
+ }
601
+ </div>
585
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"] }] });
586
603
  }
587
604
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: EmailFieldComponent, decorators: [{
@@ -590,36 +607,36 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
590
607
  selector: 'ngx-email-field',
591
608
  standalone: true,
592
609
  imports: [ReactiveFormsModule],
593
- template: `
594
- <div class="ngx-field ngx-field--email"
595
- [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
596
- <label class="ngx-field__label">{{ label }}</label>
597
- @if (masked) {
598
- <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
599
- } @else if (readonly) {
600
- <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">
601
- <a [href]="'mailto:' + control.value" class="ngx-field__email-link">{{ control.value || '—' }}</a>
602
- </span>
603
- } @else {
604
- <input
605
- class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
606
- type="email"
607
- autocomplete="email"
608
- [formControl]="$any(control)"
609
- [placeholder]="placeholder || 'you@example.com'"
610
- [attr.disabled]="field.disabled ? true : null"
611
- />
612
- @if (control.invalid && control.touched) {
613
- @if (control.errors?.['required']) {
614
- <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">Email is required</span>
615
- } @else if (control.errors?.['email']) {
616
- <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">Enter a valid email address</span>
617
- } @else {
618
- <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
619
- }
620
- }
621
- }
622
- </div>
610
+ template: `
611
+ <div class="ngx-field ngx-field--email"
612
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
613
+ <label class="ngx-field__label">{{ label }}</label>
614
+ @if (masked) {
615
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
616
+ } @else if (readonly) {
617
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">
618
+ <a [href]="'mailto:' + control.value" class="ngx-field__email-link">{{ control.value || '—' }}</a>
619
+ </span>
620
+ } @else {
621
+ <input
622
+ class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
623
+ type="email"
624
+ autocomplete="email"
625
+ [formControl]="$any(control)"
626
+ [placeholder]="placeholder || 'you@example.com'"
627
+ [attr.disabled]="field.disabled ? true : null"
628
+ />
629
+ @if (control.invalid && control.touched) {
630
+ @if (control.errors?.['required']) {
631
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">Email is required</span>
632
+ } @else if (control.errors?.['email']) {
633
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">Enter a valid email address</span>
634
+ } @else {
635
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
636
+ }
637
+ }
638
+ }
639
+ </div>
623
640
  `,
624
641
  }]
625
642
  }], propDecorators: { field: [{
@@ -652,38 +669,38 @@ class PasswordFieldComponent {
652
669
  this.visible.update(v => !v);
653
670
  }
654
671
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: PasswordFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
655
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: PasswordFieldComponent, isStandalone: true, selector: "ngx-password-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
656
- <div class="ngx-field ngx-field--password"
657
- [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
658
- <label class="ngx-field__label">{{ label }}</label>
659
- @if (masked) {
660
- <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
661
- } @else if (readonly) {
662
- <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">••••••••</span>
663
- } @else {
664
- <div class="ngx-field__password-wrap">
665
- <input
666
- class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
667
- [type]="visible() ? 'text' : 'password'"
668
- autocomplete="current-password"
669
- [formControl]="$any(control)"
670
- [placeholder]="placeholder || '••••••••'"
671
- [attr.disabled]="field.disabled ? true : null"
672
- />
673
- <button
674
- type="button"
675
- class="ngx-field__eye-btn"
676
- (click)="toggleVisibility()"
677
- [attr.aria-label]="visible() ? 'Hide password' : 'Show password'"
678
- >
679
- {{ visible() ? '🙈' : '👁' }}
680
- </button>
681
- </div>
682
- @if (control.invalid && control.touched) {
683
- <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
684
- }
685
- }
686
- </div>
672
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: PasswordFieldComponent, isStandalone: true, selector: "ngx-password-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
673
+ <div class="ngx-field ngx-field--password"
674
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
675
+ <label class="ngx-field__label">{{ label }}</label>
676
+ @if (masked) {
677
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
678
+ } @else if (readonly) {
679
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">••••••••</span>
680
+ } @else {
681
+ <div class="ngx-field__password-wrap">
682
+ <input
683
+ class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
684
+ [type]="visible() ? 'text' : 'password'"
685
+ autocomplete="current-password"
686
+ [formControl]="$any(control)"
687
+ [placeholder]="placeholder || '••••••••'"
688
+ [attr.disabled]="field.disabled ? true : null"
689
+ />
690
+ <button
691
+ type="button"
692
+ class="ngx-field__eye-btn"
693
+ (click)="toggleVisibility()"
694
+ [attr.aria-label]="visible() ? 'Hide password' : 'Show password'"
695
+ >
696
+ {{ visible() ? '🙈' : '👁' }}
697
+ </button>
698
+ </div>
699
+ @if (control.invalid && control.touched) {
700
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
701
+ }
702
+ }
703
+ </div>
687
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"] }] });
688
705
  }
689
706
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: PasswordFieldComponent, decorators: [{
@@ -692,38 +709,38 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
692
709
  selector: 'ngx-password-field',
693
710
  standalone: true,
694
711
  imports: [ReactiveFormsModule],
695
- template: `
696
- <div class="ngx-field ngx-field--password"
697
- [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
698
- <label class="ngx-field__label">{{ label }}</label>
699
- @if (masked) {
700
- <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
701
- } @else if (readonly) {
702
- <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">••••••••</span>
703
- } @else {
704
- <div class="ngx-field__password-wrap">
705
- <input
706
- class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
707
- [type]="visible() ? 'text' : 'password'"
708
- autocomplete="current-password"
709
- [formControl]="$any(control)"
710
- [placeholder]="placeholder || '••••••••'"
711
- [attr.disabled]="field.disabled ? true : null"
712
- />
713
- <button
714
- type="button"
715
- class="ngx-field__eye-btn"
716
- (click)="toggleVisibility()"
717
- [attr.aria-label]="visible() ? 'Hide password' : 'Show password'"
718
- >
719
- {{ visible() ? '🙈' : '👁' }}
720
- </button>
721
- </div>
722
- @if (control.invalid && control.touched) {
723
- <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
724
- }
725
- }
726
- </div>
712
+ template: `
713
+ <div class="ngx-field ngx-field--password"
714
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
715
+ <label class="ngx-field__label">{{ label }}</label>
716
+ @if (masked) {
717
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
718
+ } @else if (readonly) {
719
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">••••••••</span>
720
+ } @else {
721
+ <div class="ngx-field__password-wrap">
722
+ <input
723
+ class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
724
+ [type]="visible() ? 'text' : 'password'"
725
+ autocomplete="current-password"
726
+ [formControl]="$any(control)"
727
+ [placeholder]="placeholder || '••••••••'"
728
+ [attr.disabled]="field.disabled ? true : null"
729
+ />
730
+ <button
731
+ type="button"
732
+ class="ngx-field__eye-btn"
733
+ (click)="toggleVisibility()"
734
+ [attr.aria-label]="visible() ? 'Hide password' : 'Show password'"
735
+ >
736
+ {{ visible() ? '🙈' : '👁' }}
737
+ </button>
738
+ </div>
739
+ @if (control.invalid && control.touched) {
740
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
741
+ }
742
+ }
743
+ </div>
727
744
  `,
728
745
  }]
729
746
  }], propDecorators: { field: [{
@@ -748,28 +765,28 @@ class CheckboxFieldComponent {
748
765
  return resolveLabel(this.field?.label, this.language);
749
766
  }
750
767
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: CheckboxFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
751
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: CheckboxFieldComponent, isStandalone: true, selector: "ngx-checkbox-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
752
- <div class="ngx-field ngx-field--checkbox"
753
- [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
754
- @if (masked) {
755
- <label class="ngx-field__label">{{ label }}</label>
756
- <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
757
- } @else if (readonly) {
758
- <label class="ngx-field__label">{{ label }}</label>
759
- <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ control.value ? 'Yes' : 'No' }}</span>
760
- } @else {
761
- <label class="ngx-field__label" [attr.for]="field.id">
762
- <input
763
- [id]="field.id"
764
- class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
765
- type="checkbox"
766
- [formControl]="$any(control)"
767
- [attr.disabled]="field.disabled ? true : null"
768
- />
769
- {{ label }}
770
- </label>
771
- }
772
- </div>
768
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: CheckboxFieldComponent, isStandalone: true, selector: "ngx-checkbox-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
769
+ <div class="ngx-field ngx-field--checkbox"
770
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
771
+ @if (masked) {
772
+ <label class="ngx-field__label">{{ label }}</label>
773
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
774
+ } @else if (readonly) {
775
+ <label class="ngx-field__label">{{ label }}</label>
776
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ control.value ? 'Yes' : 'No' }}</span>
777
+ } @else {
778
+ <label class="ngx-field__label" [attr.for]="field.id">
779
+ <input
780
+ [id]="field.id"
781
+ class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
782
+ type="checkbox"
783
+ [formControl]="$any(control)"
784
+ [attr.disabled]="field.disabled ? true : null"
785
+ />
786
+ {{ label }}
787
+ </label>
788
+ }
789
+ </div>
773
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"] }] });
774
791
  }
775
792
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: CheckboxFieldComponent, decorators: [{
@@ -778,28 +795,28 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
778
795
  selector: 'ngx-checkbox-field',
779
796
  standalone: true,
780
797
  imports: [ReactiveFormsModule],
781
- template: `
782
- <div class="ngx-field ngx-field--checkbox"
783
- [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
784
- @if (masked) {
785
- <label class="ngx-field__label">{{ label }}</label>
786
- <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
787
- } @else if (readonly) {
788
- <label class="ngx-field__label">{{ label }}</label>
789
- <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ control.value ? 'Yes' : 'No' }}</span>
790
- } @else {
791
- <label class="ngx-field__label" [attr.for]="field.id">
792
- <input
793
- [id]="field.id"
794
- class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
795
- type="checkbox"
796
- [formControl]="$any(control)"
797
- [attr.disabled]="field.disabled ? true : null"
798
- />
799
- {{ label }}
800
- </label>
801
- }
802
- </div>
798
+ template: `
799
+ <div class="ngx-field ngx-field--checkbox"
800
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
801
+ @if (masked) {
802
+ <label class="ngx-field__label">{{ label }}</label>
803
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
804
+ } @else if (readonly) {
805
+ <label class="ngx-field__label">{{ label }}</label>
806
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ control.value ? 'Yes' : 'No' }}</span>
807
+ } @else {
808
+ <label class="ngx-field__label" [attr.for]="field.id">
809
+ <input
810
+ [id]="field.id"
811
+ class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
812
+ type="checkbox"
813
+ [formControl]="$any(control)"
814
+ [attr.disabled]="field.disabled ? true : null"
815
+ />
816
+ {{ label }}
817
+ </label>
818
+ }
819
+ </div>
803
820
  `,
804
821
  }]
805
822
  }], propDecorators: { field: [{
@@ -829,35 +846,35 @@ class BooleanFieldComponent {
829
846
  return resolveLabel(this.field?.label, this.language);
830
847
  }
831
848
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: BooleanFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
832
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: BooleanFieldComponent, isStandalone: true, selector: "ngx-boolean-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
833
- <div class="ngx-field ngx-field--boolean"
834
- [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
835
- @if (masked) {
836
- <label class="ngx-field__label">{{ label }}</label>
837
- <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
838
- } @else if (readonly) {
839
- <label class="ngx-field__label">{{ label }}</label>
840
- <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ control.value ? 'Yes' : 'No' }}</span>
841
- } @else {
842
- <div class="ngx-field__toggle-wrap">
843
- <label class="ngx-field__toggle-label">{{ label }}</label>
844
- <label class="ngx-field__toggle-switch" [class.ngx-field__toggle-switch--checked]="control.value">
845
- <input
846
- class="ngx-field__toggle-input"
847
- type="checkbox"
848
- role="switch"
849
- [formControl]="$any(control)"
850
- [attr.disabled]="field.disabled ? true : null"
851
- [attr.aria-checked]="control.value"
852
- />
853
- <span class="ngx-field__toggle-track">
854
- <span class="ngx-field__toggle-thumb"></span>
855
- </span>
856
- <span class="ngx-field__toggle-text">{{ control.value ? 'Yes' : 'No' }}</span>
857
- </label>
858
- </div>
859
- }
860
- </div>
849
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: BooleanFieldComponent, isStandalone: true, selector: "ngx-boolean-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
850
+ <div class="ngx-field ngx-field--boolean"
851
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
852
+ @if (masked) {
853
+ <label class="ngx-field__label">{{ label }}</label>
854
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
855
+ } @else if (readonly) {
856
+ <label class="ngx-field__label">{{ label }}</label>
857
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ control.value ? 'Yes' : 'No' }}</span>
858
+ } @else {
859
+ <div class="ngx-field__toggle-wrap">
860
+ <label class="ngx-field__toggle-label">{{ label }}</label>
861
+ <label class="ngx-field__toggle-switch" [class.ngx-field__toggle-switch--checked]="control.value">
862
+ <input
863
+ class="ngx-field__toggle-input"
864
+ type="checkbox"
865
+ role="switch"
866
+ [formControl]="$any(control)"
867
+ [attr.disabled]="field.disabled ? true : null"
868
+ [attr.aria-checked]="control.value"
869
+ />
870
+ <span class="ngx-field__toggle-track">
871
+ <span class="ngx-field__toggle-thumb"></span>
872
+ </span>
873
+ <span class="ngx-field__toggle-text">{{ control.value ? 'Yes' : 'No' }}</span>
874
+ </label>
875
+ </div>
876
+ }
877
+ </div>
861
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"] }] });
862
879
  }
863
880
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: BooleanFieldComponent, decorators: [{
@@ -866,35 +883,35 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
866
883
  selector: 'ngx-boolean-field',
867
884
  standalone: true,
868
885
  imports: [ReactiveFormsModule],
869
- template: `
870
- <div class="ngx-field ngx-field--boolean"
871
- [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
872
- @if (masked) {
873
- <label class="ngx-field__label">{{ label }}</label>
874
- <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
875
- } @else if (readonly) {
876
- <label class="ngx-field__label">{{ label }}</label>
877
- <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ control.value ? 'Yes' : 'No' }}</span>
878
- } @else {
879
- <div class="ngx-field__toggle-wrap">
880
- <label class="ngx-field__toggle-label">{{ label }}</label>
881
- <label class="ngx-field__toggle-switch" [class.ngx-field__toggle-switch--checked]="control.value">
882
- <input
883
- class="ngx-field__toggle-input"
884
- type="checkbox"
885
- role="switch"
886
- [formControl]="$any(control)"
887
- [attr.disabled]="field.disabled ? true : null"
888
- [attr.aria-checked]="control.value"
889
- />
890
- <span class="ngx-field__toggle-track">
891
- <span class="ngx-field__toggle-thumb"></span>
892
- </span>
893
- <span class="ngx-field__toggle-text">{{ control.value ? 'Yes' : 'No' }}</span>
894
- </label>
895
- </div>
896
- }
897
- </div>
886
+ template: `
887
+ <div class="ngx-field ngx-field--boolean"
888
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
889
+ @if (masked) {
890
+ <label class="ngx-field__label">{{ label }}</label>
891
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
892
+ } @else if (readonly) {
893
+ <label class="ngx-field__label">{{ label }}</label>
894
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ control.value ? 'Yes' : 'No' }}</span>
895
+ } @else {
896
+ <div class="ngx-field__toggle-wrap">
897
+ <label class="ngx-field__toggle-label">{{ label }}</label>
898
+ <label class="ngx-field__toggle-switch" [class.ngx-field__toggle-switch--checked]="control.value">
899
+ <input
900
+ class="ngx-field__toggle-input"
901
+ type="checkbox"
902
+ role="switch"
903
+ [formControl]="$any(control)"
904
+ [attr.disabled]="field.disabled ? true : null"
905
+ [attr.aria-checked]="control.value"
906
+ />
907
+ <span class="ngx-field__toggle-track">
908
+ <span class="ngx-field__toggle-thumb"></span>
909
+ </span>
910
+ <span class="ngx-field__toggle-text">{{ control.value ? 'Yes' : 'No' }}</span>
911
+ </label>
912
+ </div>
913
+ }
914
+ </div>
898
915
  `,
899
916
  }]
900
917
  }], propDecorators: { field: [{
@@ -930,27 +947,27 @@ class DateFieldComponent {
930
947
  }
931
948
  }
932
949
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: DateFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
933
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: DateFieldComponent, isStandalone: true, selector: "ngx-date-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
934
- <div class="ngx-field ngx-field--date"
935
- [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
936
- <label class="ngx-field__label" [attr.for]="field.id">{{ label }}</label>
937
- @if (masked) {
938
- <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
939
- } @else if (readonly) {
940
- <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ formatDate(control.value) }}</span>
941
- } @else {
942
- <input
943
- [id]="field.id"
944
- class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
945
- type="date"
946
- [formControl]="$any(control)"
947
- [attr.disabled]="field.disabled ? true : null"
948
- />
949
- @if (control.invalid && control.touched) {
950
- <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
951
- }
952
- }
953
- </div>
950
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: DateFieldComponent, isStandalone: true, selector: "ngx-date-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
951
+ <div class="ngx-field ngx-field--date"
952
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
953
+ <label class="ngx-field__label" [attr.for]="field.id">{{ label }}</label>
954
+ @if (masked) {
955
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
956
+ } @else if (readonly) {
957
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ formatDate(control.value) }}</span>
958
+ } @else {
959
+ <input
960
+ [id]="field.id"
961
+ class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
962
+ type="date"
963
+ [formControl]="$any(control)"
964
+ [attr.disabled]="field.disabled ? true : null"
965
+ />
966
+ @if (control.invalid && control.touched) {
967
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
968
+ }
969
+ }
970
+ </div>
954
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"] }] });
955
972
  }
956
973
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: DateFieldComponent, decorators: [{
@@ -959,27 +976,27 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
959
976
  selector: 'ngx-date-field',
960
977
  standalone: true,
961
978
  imports: [ReactiveFormsModule],
962
- template: `
963
- <div class="ngx-field ngx-field--date"
964
- [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
965
- <label class="ngx-field__label" [attr.for]="field.id">{{ label }}</label>
966
- @if (masked) {
967
- <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
968
- } @else if (readonly) {
969
- <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ formatDate(control.value) }}</span>
970
- } @else {
971
- <input
972
- [id]="field.id"
973
- class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
974
- type="date"
975
- [formControl]="$any(control)"
976
- [attr.disabled]="field.disabled ? true : null"
977
- />
978
- @if (control.invalid && control.touched) {
979
- <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
980
- }
981
- }
982
- </div>
979
+ template: `
980
+ <div class="ngx-field ngx-field--date"
981
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
982
+ <label class="ngx-field__label" [attr.for]="field.id">{{ label }}</label>
983
+ @if (masked) {
984
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
985
+ } @else if (readonly) {
986
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ formatDate(control.value) }}</span>
987
+ } @else {
988
+ <input
989
+ [id]="field.id"
990
+ class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
991
+ type="date"
992
+ [formControl]="$any(control)"
993
+ [attr.disabled]="field.disabled ? true : null"
994
+ />
995
+ @if (control.invalid && control.touched) {
996
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
997
+ }
998
+ }
999
+ </div>
983
1000
  `,
984
1001
  }]
985
1002
  }], propDecorators: { field: [{
@@ -1042,44 +1059,44 @@ class MonthYearFieldComponent {
1042
1059
  return monthName && year ? `${monthName} ${year}` : String(value);
1043
1060
  }
1044
1061
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: MonthYearFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1045
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: MonthYearFieldComponent, isStandalone: true, selector: "ngx-month-year-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
1046
- <div class="ngx-field ngx-field--month-year"
1047
- [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
1048
- <label class="ngx-field__label">{{ label }}</label>
1049
- @if (masked) {
1050
- <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
1051
- } @else if (readonly) {
1052
- <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ formatValue(control.value) }}</span>
1053
- } @else {
1054
- <div class="ngx-field__month-year-wrap">
1055
- <select
1056
- class="ngx-field__input ngx-field__input--month" [attr.data-testid]="'field-' + field.id + '-month'"
1057
- [value]="selectedMonth"
1058
- (change)="onMonthChange($any($event.target).value)"
1059
- [attr.disabled]="field.disabled ? true : null"
1060
- >
1061
- <option value="">Month</option>
1062
- @for (m of months; track m.value) {
1063
- <option [value]="m.value">{{ m.label }}</option>
1064
- }
1065
- </select>
1066
- <select
1067
- class="ngx-field__input ngx-field__input--year" [attr.data-testid]="'field-' + field.id + '-year'"
1068
- [value]="selectedYear"
1069
- (change)="onYearChange($any($event.target).value)"
1070
- [attr.disabled]="field.disabled ? true : null"
1071
- >
1072
- <option value="">Year</option>
1073
- @for (y of years; track y) {
1074
- <option [value]="y">{{ y }}</option>
1075
- }
1076
- </select>
1077
- </div>
1078
- @if (control.invalid && control.touched) {
1079
- <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
1080
- }
1081
- }
1082
- </div>
1062
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: MonthYearFieldComponent, isStandalone: true, selector: "ngx-month-year-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
1063
+ <div class="ngx-field ngx-field--month-year"
1064
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
1065
+ <label class="ngx-field__label">{{ label }}</label>
1066
+ @if (masked) {
1067
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
1068
+ } @else if (readonly) {
1069
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ formatValue(control.value) }}</span>
1070
+ } @else {
1071
+ <div class="ngx-field__month-year-wrap">
1072
+ <select
1073
+ class="ngx-field__input ngx-field__input--month" [attr.data-testid]="'field-' + field.id + '-month'"
1074
+ [value]="selectedMonth"
1075
+ (change)="onMonthChange($any($event.target).value)"
1076
+ [attr.disabled]="field.disabled ? true : null"
1077
+ >
1078
+ <option value="">Month</option>
1079
+ @for (m of months; track m.value) {
1080
+ <option [value]="m.value">{{ m.label }}</option>
1081
+ }
1082
+ </select>
1083
+ <select
1084
+ class="ngx-field__input ngx-field__input--year" [attr.data-testid]="'field-' + field.id + '-year'"
1085
+ [value]="selectedYear"
1086
+ (change)="onYearChange($any($event.target).value)"
1087
+ [attr.disabled]="field.disabled ? true : null"
1088
+ >
1089
+ <option value="">Year</option>
1090
+ @for (y of years; track y) {
1091
+ <option [value]="y">{{ y }}</option>
1092
+ }
1093
+ </select>
1094
+ </div>
1095
+ @if (control.invalid && control.touched) {
1096
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
1097
+ }
1098
+ }
1099
+ </div>
1083
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"] }] });
1084
1101
  }
1085
1102
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: MonthYearFieldComponent, decorators: [{
@@ -1088,44 +1105,44 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
1088
1105
  selector: 'ngx-month-year-field',
1089
1106
  standalone: true,
1090
1107
  imports: [ReactiveFormsModule],
1091
- template: `
1092
- <div class="ngx-field ngx-field--month-year"
1093
- [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
1094
- <label class="ngx-field__label">{{ label }}</label>
1095
- @if (masked) {
1096
- <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
1097
- } @else if (readonly) {
1098
- <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ formatValue(control.value) }}</span>
1099
- } @else {
1100
- <div class="ngx-field__month-year-wrap">
1101
- <select
1102
- class="ngx-field__input ngx-field__input--month" [attr.data-testid]="'field-' + field.id + '-month'"
1103
- [value]="selectedMonth"
1104
- (change)="onMonthChange($any($event.target).value)"
1105
- [attr.disabled]="field.disabled ? true : null"
1106
- >
1107
- <option value="">Month</option>
1108
- @for (m of months; track m.value) {
1109
- <option [value]="m.value">{{ m.label }}</option>
1110
- }
1111
- </select>
1112
- <select
1113
- class="ngx-field__input ngx-field__input--year" [attr.data-testid]="'field-' + field.id + '-year'"
1114
- [value]="selectedYear"
1115
- (change)="onYearChange($any($event.target).value)"
1116
- [attr.disabled]="field.disabled ? true : null"
1117
- >
1118
- <option value="">Year</option>
1119
- @for (y of years; track y) {
1120
- <option [value]="y">{{ y }}</option>
1121
- }
1122
- </select>
1123
- </div>
1124
- @if (control.invalid && control.touched) {
1125
- <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
1126
- }
1127
- }
1128
- </div>
1108
+ template: `
1109
+ <div class="ngx-field ngx-field--month-year"
1110
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
1111
+ <label class="ngx-field__label">{{ label }}</label>
1112
+ @if (masked) {
1113
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
1114
+ } @else if (readonly) {
1115
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ formatValue(control.value) }}</span>
1116
+ } @else {
1117
+ <div class="ngx-field__month-year-wrap">
1118
+ <select
1119
+ class="ngx-field__input ngx-field__input--month" [attr.data-testid]="'field-' + field.id + '-month'"
1120
+ [value]="selectedMonth"
1121
+ (change)="onMonthChange($any($event.target).value)"
1122
+ [attr.disabled]="field.disabled ? true : null"
1123
+ >
1124
+ <option value="">Month</option>
1125
+ @for (m of months; track m.value) {
1126
+ <option [value]="m.value">{{ m.label }}</option>
1127
+ }
1128
+ </select>
1129
+ <select
1130
+ class="ngx-field__input ngx-field__input--year" [attr.data-testid]="'field-' + field.id + '-year'"
1131
+ [value]="selectedYear"
1132
+ (change)="onYearChange($any($event.target).value)"
1133
+ [attr.disabled]="field.disabled ? true : null"
1134
+ >
1135
+ <option value="">Year</option>
1136
+ @for (y of years; track y) {
1137
+ <option [value]="y">{{ y }}</option>
1138
+ }
1139
+ </select>
1140
+ </div>
1141
+ @if (control.invalid && control.touched) {
1142
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
1143
+ }
1144
+ }
1145
+ </div>
1129
1146
  `,
1130
1147
  }]
1131
1148
  }], propDecorators: { field: [{
@@ -1448,46 +1465,46 @@ class DropdownFieldComponent {
1448
1465
  return 'Invalid selection.';
1449
1466
  }
1450
1467
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: DropdownFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1451
- 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: `
1452
- <div
1453
- class="ngx-field ngx-field--dropdown"
1454
- [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type"
1455
- [class.ngx-field--readonly]="readonly"
1456
- [class.ngx-field--masked]="masked"
1457
- [class.ngx-field--invalid]="control && control.invalid && control.touched"
1458
- >
1459
- <label class="ngx-field__label" [attr.for]="field.id">
1460
- {{ label }}
1461
- @if (field.validators?.required) { <span class="ngx-field__req">*</span> }
1462
- </label>
1463
- @if (masked) {
1464
- <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
1465
- } @else if (readonly) {
1466
- <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ getLabel(control.value) }}</span>
1467
- } @else {
1468
- <select
1469
- [id]="field.id"
1470
- class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
1471
- [formControl]="$any(control)"
1472
- [compareWith]="compareFn"
1473
- [attr.disabled]="field.disabled ? true : null"
1474
- [attr.aria-invalid]="control.invalid && control.touched"
1475
- [attr.aria-describedby]="errorMessage ? field.id + '-error' : null"
1476
- >
1477
- <option [value]="''">{{ placeholder || 'Select...' }}</option>
1478
- @for (option of options(); track getOptLabel(option)) {
1479
- @if (isObjectVal(option)) {
1480
- <option [ngValue]="getOptStoredVal(option)">{{ getOptLabel(option) }}</option>
1481
- } @else {
1482
- <option [value]="getOptStoredVal(option)">{{ getOptLabel(option) }}</option>
1483
- }
1484
- }
1485
- </select>
1486
- @if (errorMessage) {
1487
- <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'" [id]="field.id + '-error'" role="alert">{{ errorMessage }}</span>
1488
- }
1489
- }
1490
- </div>
1468
+ 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: `
1469
+ <div
1470
+ class="ngx-field ngx-field--dropdown"
1471
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type"
1472
+ [class.ngx-field--readonly]="readonly"
1473
+ [class.ngx-field--masked]="masked"
1474
+ [class.ngx-field--invalid]="control && control.invalid && control.touched"
1475
+ >
1476
+ <label class="ngx-field__label" [attr.for]="field.id">
1477
+ {{ label }}
1478
+ @if (field.validators?.required) { <span class="ngx-field__req">*</span> }
1479
+ </label>
1480
+ @if (masked) {
1481
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
1482
+ } @else if (readonly) {
1483
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ getLabel(control.value) }}</span>
1484
+ } @else {
1485
+ <select
1486
+ [id]="field.id"
1487
+ class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
1488
+ [formControl]="$any(control)"
1489
+ [compareWith]="compareFn"
1490
+ [attr.disabled]="field.disabled ? true : null"
1491
+ [attr.aria-invalid]="control.invalid && control.touched"
1492
+ [attr.aria-describedby]="errorMessage ? field.id + '-error' : null"
1493
+ >
1494
+ <option [value]="''">{{ placeholder || 'Select...' }}</option>
1495
+ @for (option of options(); track getOptLabel(option)) {
1496
+ @if (isObjectVal(option)) {
1497
+ <option [ngValue]="getOptStoredVal(option)">{{ getOptLabel(option) }}</option>
1498
+ } @else {
1499
+ <option [value]="getOptStoredVal(option)">{{ getOptLabel(option) }}</option>
1500
+ }
1501
+ }
1502
+ </select>
1503
+ @if (errorMessage) {
1504
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'" [id]="field.id + '-error'" role="alert">{{ errorMessage }}</span>
1505
+ }
1506
+ }
1507
+ </div>
1491
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"] }] });
1492
1509
  }
1493
1510
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: DropdownFieldComponent, decorators: [{
@@ -1496,46 +1513,46 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
1496
1513
  selector: 'ngx-dropdown-field',
1497
1514
  standalone: true,
1498
1515
  imports: [ReactiveFormsModule],
1499
- template: `
1500
- <div
1501
- class="ngx-field ngx-field--dropdown"
1502
- [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type"
1503
- [class.ngx-field--readonly]="readonly"
1504
- [class.ngx-field--masked]="masked"
1505
- [class.ngx-field--invalid]="control && control.invalid && control.touched"
1506
- >
1507
- <label class="ngx-field__label" [attr.for]="field.id">
1508
- {{ label }}
1509
- @if (field.validators?.required) { <span class="ngx-field__req">*</span> }
1510
- </label>
1511
- @if (masked) {
1512
- <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
1513
- } @else if (readonly) {
1514
- <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ getLabel(control.value) }}</span>
1515
- } @else {
1516
- <select
1517
- [id]="field.id"
1518
- class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
1519
- [formControl]="$any(control)"
1520
- [compareWith]="compareFn"
1521
- [attr.disabled]="field.disabled ? true : null"
1522
- [attr.aria-invalid]="control.invalid && control.touched"
1523
- [attr.aria-describedby]="errorMessage ? field.id + '-error' : null"
1524
- >
1525
- <option [value]="''">{{ placeholder || 'Select...' }}</option>
1526
- @for (option of options(); track getOptLabel(option)) {
1527
- @if (isObjectVal(option)) {
1528
- <option [ngValue]="getOptStoredVal(option)">{{ getOptLabel(option) }}</option>
1529
- } @else {
1530
- <option [value]="getOptStoredVal(option)">{{ getOptLabel(option) }}</option>
1531
- }
1532
- }
1533
- </select>
1534
- @if (errorMessage) {
1535
- <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'" [id]="field.id + '-error'" role="alert">{{ errorMessage }}</span>
1536
- }
1537
- }
1538
- </div>
1516
+ template: `
1517
+ <div
1518
+ class="ngx-field ngx-field--dropdown"
1519
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type"
1520
+ [class.ngx-field--readonly]="readonly"
1521
+ [class.ngx-field--masked]="masked"
1522
+ [class.ngx-field--invalid]="control && control.invalid && control.touched"
1523
+ >
1524
+ <label class="ngx-field__label" [attr.for]="field.id">
1525
+ {{ label }}
1526
+ @if (field.validators?.required) { <span class="ngx-field__req">*</span> }
1527
+ </label>
1528
+ @if (masked) {
1529
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
1530
+ } @else if (readonly) {
1531
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ getLabel(control.value) }}</span>
1532
+ } @else {
1533
+ <select
1534
+ [id]="field.id"
1535
+ class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
1536
+ [formControl]="$any(control)"
1537
+ [compareWith]="compareFn"
1538
+ [attr.disabled]="field.disabled ? true : null"
1539
+ [attr.aria-invalid]="control.invalid && control.touched"
1540
+ [attr.aria-describedby]="errorMessage ? field.id + '-error' : null"
1541
+ >
1542
+ <option [value]="''">{{ placeholder || 'Select...' }}</option>
1543
+ @for (option of options(); track getOptLabel(option)) {
1544
+ @if (isObjectVal(option)) {
1545
+ <option [ngValue]="getOptStoredVal(option)">{{ getOptLabel(option) }}</option>
1546
+ } @else {
1547
+ <option [value]="getOptStoredVal(option)">{{ getOptLabel(option) }}</option>
1548
+ }
1549
+ }
1550
+ </select>
1551
+ @if (errorMessage) {
1552
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'" [id]="field.id + '-error'" role="alert">{{ errorMessage }}</span>
1553
+ }
1554
+ }
1555
+ </div>
1539
1556
  `,
1540
1557
  }]
1541
1558
  }], propDecorators: { field: [{
@@ -1612,37 +1629,37 @@ class RadioFieldComponent {
1612
1629
  return typeof value === 'object' ? resolveLabel(value, this.language) : (value ?? '—');
1613
1630
  }
1614
1631
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: RadioFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1615
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: RadioFieldComponent, isStandalone: true, selector: "ngx-radio-field", inputs: { field: "field", language: "language", control: "control", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
1616
- <div class="ngx-field ngx-field--radio"
1617
- [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
1618
- <fieldset class="ngx-field__fieldset">
1619
- <legend class="ngx-field__label">{{ label }}</legend>
1620
- @if (masked) {
1621
- <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
1622
- } @else if (readonly) {
1623
- <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ getSelectedLabel() }}</span>
1624
- } @else {
1625
- <div class="ngx-field__radio-group">
1626
- @for (option of options(); track getOptLabel(option)) {
1627
- <label class="ngx-field__radio-option" [attr.for]="getRadioId(option)">
1628
- <input
1629
- [id]="getRadioId(option)"
1630
- type="radio"
1631
- class="ngx-field__radio-input"
1632
- [formControl]="$any(control)"
1633
- [value]="getOptStoredVal(option)"
1634
- [attr.disabled]="field.disabled ? true : null"
1635
- />
1636
- <span class="ngx-field__radio-label">{{ getOptLabel(option) }}</span>
1637
- </label>
1638
- }
1639
- </div>
1640
- @if (control.invalid && control.touched) {
1641
- <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
1642
- }
1643
- }
1644
- </fieldset>
1645
- </div>
1632
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: RadioFieldComponent, isStandalone: true, selector: "ngx-radio-field", inputs: { field: "field", language: "language", control: "control", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
1633
+ <div class="ngx-field ngx-field--radio"
1634
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
1635
+ <fieldset class="ngx-field__fieldset">
1636
+ <legend class="ngx-field__label">{{ label }}</legend>
1637
+ @if (masked) {
1638
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
1639
+ } @else if (readonly) {
1640
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ getSelectedLabel() }}</span>
1641
+ } @else {
1642
+ <div class="ngx-field__radio-group">
1643
+ @for (option of options(); track getOptLabel(option)) {
1644
+ <label class="ngx-field__radio-option" [attr.for]="getRadioId(option)">
1645
+ <input
1646
+ [id]="getRadioId(option)"
1647
+ type="radio"
1648
+ class="ngx-field__radio-input"
1649
+ [formControl]="$any(control)"
1650
+ [value]="getOptStoredVal(option)"
1651
+ [attr.disabled]="field.disabled ? true : null"
1652
+ />
1653
+ <span class="ngx-field__radio-label">{{ getOptLabel(option) }}</span>
1654
+ </label>
1655
+ }
1656
+ </div>
1657
+ @if (control.invalid && control.touched) {
1658
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
1659
+ }
1660
+ }
1661
+ </fieldset>
1662
+ </div>
1646
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"] }] });
1647
1664
  }
1648
1665
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: RadioFieldComponent, decorators: [{
@@ -1651,37 +1668,37 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
1651
1668
  selector: 'ngx-radio-field',
1652
1669
  standalone: true,
1653
1670
  imports: [ReactiveFormsModule],
1654
- template: `
1655
- <div class="ngx-field ngx-field--radio"
1656
- [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
1657
- <fieldset class="ngx-field__fieldset">
1658
- <legend class="ngx-field__label">{{ label }}</legend>
1659
- @if (masked) {
1660
- <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
1661
- } @else if (readonly) {
1662
- <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ getSelectedLabel() }}</span>
1663
- } @else {
1664
- <div class="ngx-field__radio-group">
1665
- @for (option of options(); track getOptLabel(option)) {
1666
- <label class="ngx-field__radio-option" [attr.for]="getRadioId(option)">
1667
- <input
1668
- [id]="getRadioId(option)"
1669
- type="radio"
1670
- class="ngx-field__radio-input"
1671
- [formControl]="$any(control)"
1672
- [value]="getOptStoredVal(option)"
1673
- [attr.disabled]="field.disabled ? true : null"
1674
- />
1675
- <span class="ngx-field__radio-label">{{ getOptLabel(option) }}</span>
1676
- </label>
1677
- }
1678
- </div>
1679
- @if (control.invalid && control.touched) {
1680
- <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
1681
- }
1682
- }
1683
- </fieldset>
1684
- </div>
1671
+ template: `
1672
+ <div class="ngx-field ngx-field--radio"
1673
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
1674
+ <fieldset class="ngx-field__fieldset">
1675
+ <legend class="ngx-field__label">{{ label }}</legend>
1676
+ @if (masked) {
1677
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
1678
+ } @else if (readonly) {
1679
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ getSelectedLabel() }}</span>
1680
+ } @else {
1681
+ <div class="ngx-field__radio-group">
1682
+ @for (option of options(); track getOptLabel(option)) {
1683
+ <label class="ngx-field__radio-option" [attr.for]="getRadioId(option)">
1684
+ <input
1685
+ [id]="getRadioId(option)"
1686
+ type="radio"
1687
+ class="ngx-field__radio-input"
1688
+ [formControl]="$any(control)"
1689
+ [value]="getOptStoredVal(option)"
1690
+ [attr.disabled]="field.disabled ? true : null"
1691
+ />
1692
+ <span class="ngx-field__radio-label">{{ getOptLabel(option) }}</span>
1693
+ </label>
1694
+ }
1695
+ </div>
1696
+ @if (control.invalid && control.touched) {
1697
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
1698
+ }
1699
+ }
1700
+ </fieldset>
1701
+ </div>
1685
1702
  `,
1686
1703
  }]
1687
1704
  }], propDecorators: { field: [{
@@ -1751,37 +1768,37 @@ class MultiSelectFieldComponent {
1751
1768
  .join(', ');
1752
1769
  }
1753
1770
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: MultiSelectFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1754
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: MultiSelectFieldComponent, isStandalone: true, selector: "ngx-multi-select-field", inputs: { field: "field", language: "language", control: "control", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
1755
- <div class="ngx-field ngx-field--multiSelect"
1756
- [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
1757
- <label class="ngx-field__label">{{ label }}</label>
1758
- @if (masked) {
1759
- <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
1760
- } @else if (readonly) {
1761
- <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ getLabels(control.value) }}</span>
1762
- } @else {
1763
- <select
1764
- [id]="field.id"
1765
- class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
1766
- [formControl]="$any(control)"
1767
- [compareWith]="compareFn"
1768
- [attr.disabled]="field.disabled ? true : null"
1769
- multiple
1770
- size="4"
1771
- >
1772
- @for (option of options(); track getOptLabel(option)) {
1773
- @if (isObjectVal(option)) {
1774
- <option [ngValue]="getOptStoredVal(option)">{{ getOptLabel(option) }}</option>
1775
- } @else {
1776
- <option [value]="getOptStoredVal(option)">{{ getOptLabel(option) }}</option>
1777
- }
1778
- }
1779
- </select>
1780
- @if (control.invalid && control.touched) {
1781
- <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
1782
- }
1783
- }
1784
- </div>
1771
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: MultiSelectFieldComponent, isStandalone: true, selector: "ngx-multi-select-field", inputs: { field: "field", language: "language", control: "control", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
1772
+ <div class="ngx-field ngx-field--multiSelect"
1773
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
1774
+ <label class="ngx-field__label">{{ label }}</label>
1775
+ @if (masked) {
1776
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
1777
+ } @else if (readonly) {
1778
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ getLabels(control.value) }}</span>
1779
+ } @else {
1780
+ <select
1781
+ [id]="field.id"
1782
+ class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
1783
+ [formControl]="$any(control)"
1784
+ [compareWith]="compareFn"
1785
+ [attr.disabled]="field.disabled ? true : null"
1786
+ multiple
1787
+ size="4"
1788
+ >
1789
+ @for (option of options(); track getOptLabel(option)) {
1790
+ @if (isObjectVal(option)) {
1791
+ <option [ngValue]="getOptStoredVal(option)">{{ getOptLabel(option) }}</option>
1792
+ } @else {
1793
+ <option [value]="getOptStoredVal(option)">{{ getOptLabel(option) }}</option>
1794
+ }
1795
+ }
1796
+ </select>
1797
+ @if (control.invalid && control.touched) {
1798
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
1799
+ }
1800
+ }
1801
+ </div>
1785
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"] }] });
1786
1803
  }
1787
1804
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: MultiSelectFieldComponent, decorators: [{
@@ -1790,37 +1807,37 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
1790
1807
  selector: 'ngx-multi-select-field',
1791
1808
  standalone: true,
1792
1809
  imports: [ReactiveFormsModule],
1793
- template: `
1794
- <div class="ngx-field ngx-field--multiSelect"
1795
- [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
1796
- <label class="ngx-field__label">{{ label }}</label>
1797
- @if (masked) {
1798
- <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
1799
- } @else if (readonly) {
1800
- <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ getLabels(control.value) }}</span>
1801
- } @else {
1802
- <select
1803
- [id]="field.id"
1804
- class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
1805
- [formControl]="$any(control)"
1806
- [compareWith]="compareFn"
1807
- [attr.disabled]="field.disabled ? true : null"
1808
- multiple
1809
- size="4"
1810
- >
1811
- @for (option of options(); track getOptLabel(option)) {
1812
- @if (isObjectVal(option)) {
1813
- <option [ngValue]="getOptStoredVal(option)">{{ getOptLabel(option) }}</option>
1814
- } @else {
1815
- <option [value]="getOptStoredVal(option)">{{ getOptLabel(option) }}</option>
1816
- }
1817
- }
1818
- </select>
1819
- @if (control.invalid && control.touched) {
1820
- <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
1821
- }
1822
- }
1823
- </div>
1810
+ template: `
1811
+ <div class="ngx-field ngx-field--multiSelect"
1812
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
1813
+ <label class="ngx-field__label">{{ label }}</label>
1814
+ @if (masked) {
1815
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
1816
+ } @else if (readonly) {
1817
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ getLabels(control.value) }}</span>
1818
+ } @else {
1819
+ <select
1820
+ [id]="field.id"
1821
+ class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
1822
+ [formControl]="$any(control)"
1823
+ [compareWith]="compareFn"
1824
+ [attr.disabled]="field.disabled ? true : null"
1825
+ multiple
1826
+ size="4"
1827
+ >
1828
+ @for (option of options(); track getOptLabel(option)) {
1829
+ @if (isObjectVal(option)) {
1830
+ <option [ngValue]="getOptStoredVal(option)">{{ getOptLabel(option) }}</option>
1831
+ } @else {
1832
+ <option [value]="getOptStoredVal(option)">{{ getOptLabel(option) }}</option>
1833
+ }
1834
+ }
1835
+ </select>
1836
+ @if (control.invalid && control.touched) {
1837
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
1838
+ }
1839
+ }
1840
+ </div>
1824
1841
  `,
1825
1842
  }]
1826
1843
  }], propDecorators: { field: [{
@@ -2259,39 +2276,39 @@ class EntityRefFieldComponent {
2259
2276
  });
2260
2277
  }
2261
2278
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: EntityRefFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2262
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: EntityRefFieldComponent, isStandalone: true, selector: "ngx-entity-ref-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
2263
- <div class="ngx-field ngx-field--entity-ref"
2264
- [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
2265
- <label class="ngx-field__label" [attr.for]="'field-' + field.id">{{ label }}</label>
2266
- @if (masked) {
2267
- <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
2268
- } @else if (readonly) {
2269
- <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ getLabel(control.value) }}</span>
2270
- } @else {
2271
- @if (loading()) {
2272
- <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-loading'" role="status">Loading…</span>
2273
- } @else {
2274
- <select
2275
- class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
2276
- [id]="'field-' + field.id"
2277
- [formControl]="$any(control)"
2278
- [attr.disabled]="field.disabled ? true : null"
2279
- (change)="onSelectionChange()"
2280
- >
2281
- <option value="">{{ placeholder || 'Select...' }}</option>
2282
- @for (option of options(); track option.value) {
2283
- <option [value]="option.value">{{ option.label }}</option>
2284
- }
2285
- </select>
2286
- @if (awaitingParent()) {
2287
- <span class="ngx-field__hint" [attr.data-testid]="'field-' + field.id + '-hint'">Select {{ parentFieldId }} first.</span>
2288
- }
2289
- }
2290
- @if (control.invalid && control.touched) {
2291
- <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
2292
- }
2293
- }
2294
- </div>
2279
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: EntityRefFieldComponent, isStandalone: true, selector: "ngx-entity-ref-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
2280
+ <div class="ngx-field ngx-field--entity-ref"
2281
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
2282
+ <label class="ngx-field__label" [attr.for]="'field-' + field.id">{{ label }}</label>
2283
+ @if (masked) {
2284
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
2285
+ } @else if (readonly) {
2286
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ getLabel(control.value) }}</span>
2287
+ } @else {
2288
+ @if (loading()) {
2289
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-loading'" role="status">Loading…</span>
2290
+ } @else {
2291
+ <select
2292
+ class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
2293
+ [id]="'field-' + field.id"
2294
+ [formControl]="$any(control)"
2295
+ [attr.disabled]="field.disabled ? true : null"
2296
+ (change)="onSelectionChange()"
2297
+ >
2298
+ <option value="">{{ placeholder || 'Select...' }}</option>
2299
+ @for (option of options(); track option.value) {
2300
+ <option [value]="option.value">{{ option.label }}</option>
2301
+ }
2302
+ </select>
2303
+ @if (awaitingParent()) {
2304
+ <span class="ngx-field__hint" [attr.data-testid]="'field-' + field.id + '-hint'">Select {{ parentFieldId }} first.</span>
2305
+ }
2306
+ }
2307
+ @if (control.invalid && control.touched) {
2308
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
2309
+ }
2310
+ }
2311
+ </div>
2295
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"] }] });
2296
2313
  }
2297
2314
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: EntityRefFieldComponent, decorators: [{
@@ -2300,39 +2317,39 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
2300
2317
  selector: 'ngx-entity-ref-field',
2301
2318
  standalone: true,
2302
2319
  imports: [ReactiveFormsModule],
2303
- template: `
2304
- <div class="ngx-field ngx-field--entity-ref"
2305
- [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
2306
- <label class="ngx-field__label" [attr.for]="'field-' + field.id">{{ label }}</label>
2307
- @if (masked) {
2308
- <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
2309
- } @else if (readonly) {
2310
- <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ getLabel(control.value) }}</span>
2311
- } @else {
2312
- @if (loading()) {
2313
- <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-loading'" role="status">Loading…</span>
2314
- } @else {
2315
- <select
2316
- class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
2317
- [id]="'field-' + field.id"
2318
- [formControl]="$any(control)"
2319
- [attr.disabled]="field.disabled ? true : null"
2320
- (change)="onSelectionChange()"
2321
- >
2322
- <option value="">{{ placeholder || 'Select...' }}</option>
2323
- @for (option of options(); track option.value) {
2324
- <option [value]="option.value">{{ option.label }}</option>
2325
- }
2326
- </select>
2327
- @if (awaitingParent()) {
2328
- <span class="ngx-field__hint" [attr.data-testid]="'field-' + field.id + '-hint'">Select {{ parentFieldId }} first.</span>
2329
- }
2330
- }
2331
- @if (control.invalid && control.touched) {
2332
- <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
2333
- }
2334
- }
2335
- </div>
2320
+ template: `
2321
+ <div class="ngx-field ngx-field--entity-ref"
2322
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
2323
+ <label class="ngx-field__label" [attr.for]="'field-' + field.id">{{ label }}</label>
2324
+ @if (masked) {
2325
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
2326
+ } @else if (readonly) {
2327
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ getLabel(control.value) }}</span>
2328
+ } @else {
2329
+ @if (loading()) {
2330
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-loading'" role="status">Loading…</span>
2331
+ } @else {
2332
+ <select
2333
+ class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
2334
+ [id]="'field-' + field.id"
2335
+ [formControl]="$any(control)"
2336
+ [attr.disabled]="field.disabled ? true : null"
2337
+ (change)="onSelectionChange()"
2338
+ >
2339
+ <option value="">{{ placeholder || 'Select...' }}</option>
2340
+ @for (option of options(); track option.value) {
2341
+ <option [value]="option.value">{{ option.label }}</option>
2342
+ }
2343
+ </select>
2344
+ @if (awaitingParent()) {
2345
+ <span class="ngx-field__hint" [attr.data-testid]="'field-' + field.id + '-hint'">Select {{ parentFieldId }} first.</span>
2346
+ }
2347
+ }
2348
+ @if (control.invalid && control.touched) {
2349
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
2350
+ }
2351
+ }
2352
+ </div>
2336
2353
  `,
2337
2354
  }]
2338
2355
  }], propDecorators: { field: [{
@@ -2557,56 +2574,56 @@ class GroupFieldComponent {
2557
2574
  return group?.get(childId);
2558
2575
  }
2559
2576
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: GroupFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2560
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: GroupFieldComponent, isStandalone: true, selector: "ngx-group-field", inputs: { field: "field", control: "control", config: "config", language: "language", readonly: "readonly", masked: "masked", userRoles: "userRoles" }, ngImport: i0, template: `
2561
- <fieldset class="ngx-field ngx-field--group"
2562
- [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
2563
- <legend class="ngx-field__legend">{{ label }}</legend>
2564
- @if (masked) {
2565
- <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
2566
- } @else {
2567
- <div class="ngx-field__group-children">
2568
- @for (child of field.children ?? []; track child.id) {
2569
- @if (child.visibility !== false) {
2570
- <ngx-dynamic-field
2571
- [field]="child"
2572
- [control]="getChildControl(child.id)"
2573
- [config]="config"
2574
- [language]="language"
2575
- [readonly]="readonly || !!child.readonly"
2576
- [userRoles]="userRoles"
2577
- />
2578
- }
2579
- }
2580
- </div>
2581
- }
2582
- </fieldset>
2577
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: GroupFieldComponent, isStandalone: true, selector: "ngx-group-field", inputs: { field: "field", control: "control", config: "config", language: "language", readonly: "readonly", masked: "masked", userRoles: "userRoles" }, ngImport: i0, template: `
2578
+ <fieldset class="ngx-field ngx-field--group"
2579
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
2580
+ <legend class="ngx-field__legend">{{ label }}</legend>
2581
+ @if (masked) {
2582
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
2583
+ } @else {
2584
+ <div class="ngx-field__group-children">
2585
+ @for (child of field.children ?? []; track child.id) {
2586
+ @if (child.visibility !== false) {
2587
+ <ngx-dynamic-field
2588
+ [field]="child"
2589
+ [control]="getChildControl(child.id)"
2590
+ [config]="config"
2591
+ [language]="language"
2592
+ [readonly]="readonly || !!child.readonly"
2593
+ [userRoles]="userRoles"
2594
+ />
2595
+ }
2596
+ }
2597
+ </div>
2598
+ }
2599
+ </fieldset>
2583
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"] }] });
2584
2601
  }
2585
2602
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: GroupFieldComponent, decorators: [{
2586
2603
  type: Component,
2587
- args: [{ selector: 'ngx-group-field', standalone: true, imports: [ReactiveFormsModule, forwardRef(() => DynamicFieldComponent)], template: `
2588
- <fieldset class="ngx-field ngx-field--group"
2589
- [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
2590
- <legend class="ngx-field__legend">{{ label }}</legend>
2591
- @if (masked) {
2592
- <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
2593
- } @else {
2594
- <div class="ngx-field__group-children">
2595
- @for (child of field.children ?? []; track child.id) {
2596
- @if (child.visibility !== false) {
2597
- <ngx-dynamic-field
2598
- [field]="child"
2599
- [control]="getChildControl(child.id)"
2600
- [config]="config"
2601
- [language]="language"
2602
- [readonly]="readonly || !!child.readonly"
2603
- [userRoles]="userRoles"
2604
- />
2605
- }
2606
- }
2607
- </div>
2608
- }
2609
- </fieldset>
2604
+ args: [{ selector: 'ngx-group-field', standalone: true, imports: [ReactiveFormsModule, forwardRef(() => DynamicFieldComponent)], template: `
2605
+ <fieldset class="ngx-field ngx-field--group"
2606
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
2607
+ <legend class="ngx-field__legend">{{ label }}</legend>
2608
+ @if (masked) {
2609
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
2610
+ } @else {
2611
+ <div class="ngx-field__group-children">
2612
+ @for (child of field.children ?? []; track child.id) {
2613
+ @if (child.visibility !== false) {
2614
+ <ngx-dynamic-field
2615
+ [field]="child"
2616
+ [control]="getChildControl(child.id)"
2617
+ [config]="config"
2618
+ [language]="language"
2619
+ [readonly]="readonly || !!child.readonly"
2620
+ [userRoles]="userRoles"
2621
+ />
2622
+ }
2623
+ }
2624
+ </div>
2625
+ }
2626
+ </fieldset>
2610
2627
  `, 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"] }]
2611
2628
  }], propDecorators: { field: [{
2612
2629
  type: Input
@@ -2676,6 +2693,17 @@ class ValidatorRegistryService {
2676
2693
  fnList.push(Validators.maxLength(config.maxLength));
2677
2694
  if (config.pattern)
2678
2695
  fnList.push(Validators.pattern(config.pattern));
2696
+ if (config.email)
2697
+ fnList.push(Validators.email);
2698
+ // Named validators from the consumer registry. This branch previously hardcoded the
2699
+ // built-ins and consulted the registry only for the `string[]` form of this config, so
2700
+ // a validator the consumer had registered could not be named from a typed schema
2701
+ // without casting it to `any`.
2702
+ for (const key of config.custom ?? []) {
2703
+ const fn = this.resolve(key);
2704
+ if (fn)
2705
+ fnList.push(fn);
2706
+ }
2679
2707
  return fnList;
2680
2708
  }
2681
2709
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ValidatorRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
@@ -2719,110 +2747,110 @@ class ArrayFieldComponent {
2719
2747
  this.formArray.removeAt(index);
2720
2748
  }
2721
2749
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ArrayFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2722
- 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: `
2723
- <div class="ngx-field ngx-field--array"
2724
- [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
2725
- <div class="ngx-field__array-header">
2726
- <label class="ngx-field__label">{{ label }}</label>
2727
- @if (!readonly && !masked) {
2728
- <button type="button" class="ngx-field__array-add-btn"
2729
- [attr.data-testid]="'field-' + field.id + '-add'" (click)="addItem()">
2730
- + Add Item
2731
- </button>
2732
- }
2733
- </div>
2734
-
2735
- @if (masked) {
2736
- <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
2737
- } @else {
2738
- <div class="ngx-field__array-list">
2739
- @for (itemGroup of formArray.controls; track idx; let idx = $index) {
2740
- <div class="ngx-field__array-item" [attr.data-testid]="'field-' + field.id + '-row'">
2741
- <div class="ngx-field__array-item-header">
2742
- <span class="ngx-field__array-item-title">Item #{{ idx + 1 }}</span>
2743
- @if (!readonly) {
2744
- <button type="button" class="ngx-field__array-remove-btn"
2745
- [attr.data-testid]="'field-' + field.id + '-remove-' + idx" (click)="removeItem(idx)">
2746
- Remove
2747
- </button>
2748
- }
2749
- </div>
2750
- <div class="ngx-field__array-item-fields">
2751
- @for (child of field.children ?? []; track child.id) {
2752
- @if (child.visibility !== false) {
2753
- <ngx-dynamic-field
2754
- [field]="child"
2755
- [control]="getItemChildControl(itemGroup, child.id)"
2756
- [config]="config"
2757
- [language]="language"
2758
- [readonly]="readonly || !!child.readonly"
2759
- [userRoles]="userRoles"
2760
- />
2761
- }
2762
- }
2763
- </div>
2764
- </div>
2765
- }
2766
- @if (formArray.controls.length === 0) {
2767
- <div class="ngx-field__array-empty" [attr.data-testid]="'field-' + field.id + '-empty'">No items added yet.</div>
2768
- }
2769
- </div>
2770
- }
2771
- </div>
2750
+ 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: `
2751
+ <div class="ngx-field ngx-field--array"
2752
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
2753
+ <div class="ngx-field__array-header">
2754
+ <label class="ngx-field__label">{{ label }}</label>
2755
+ @if (!readonly && !masked) {
2756
+ <button type="button" class="ngx-field__array-add-btn"
2757
+ [attr.data-testid]="'field-' + field.id + '-add'" (click)="addItem()">
2758
+ + Add Item
2759
+ </button>
2760
+ }
2761
+ </div>
2762
+
2763
+ @if (masked) {
2764
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
2765
+ } @else {
2766
+ <div class="ngx-field__array-list">
2767
+ @for (itemGroup of formArray.controls; track idx; let idx = $index) {
2768
+ <div class="ngx-field__array-item" [attr.data-testid]="'field-' + field.id + '-row'">
2769
+ <div class="ngx-field__array-item-header">
2770
+ <span class="ngx-field__array-item-title">Item #{{ idx + 1 }}</span>
2771
+ @if (!readonly) {
2772
+ <button type="button" class="ngx-field__array-remove-btn"
2773
+ [attr.data-testid]="'field-' + field.id + '-remove-' + idx" (click)="removeItem(idx)">
2774
+ Remove
2775
+ </button>
2776
+ }
2777
+ </div>
2778
+ <div class="ngx-field__array-item-fields">
2779
+ @for (child of field.children ?? []; track child.id) {
2780
+ @if (child.visibility !== false) {
2781
+ <ngx-dynamic-field
2782
+ [field]="child"
2783
+ [control]="getItemChildControl(itemGroup, child.id)"
2784
+ [config]="config"
2785
+ [language]="language"
2786
+ [readonly]="readonly || !!child.readonly"
2787
+ [userRoles]="userRoles"
2788
+ />
2789
+ }
2790
+ }
2791
+ </div>
2792
+ </div>
2793
+ }
2794
+ @if (formArray.controls.length === 0) {
2795
+ <div class="ngx-field__array-empty" [attr.data-testid]="'field-' + field.id + '-empty'">No items added yet.</div>
2796
+ }
2797
+ </div>
2798
+ }
2799
+ </div>
2772
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"] }] });
2773
2801
  }
2774
2802
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ArrayFieldComponent, decorators: [{
2775
2803
  type: Component,
2776
- args: [{ selector: 'ngx-array-field', standalone: true, imports: [ReactiveFormsModule, forwardRef(() => DynamicFieldComponent)], template: `
2777
- <div class="ngx-field ngx-field--array"
2778
- [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
2779
- <div class="ngx-field__array-header">
2780
- <label class="ngx-field__label">{{ label }}</label>
2781
- @if (!readonly && !masked) {
2782
- <button type="button" class="ngx-field__array-add-btn"
2783
- [attr.data-testid]="'field-' + field.id + '-add'" (click)="addItem()">
2784
- + Add Item
2785
- </button>
2786
- }
2787
- </div>
2788
-
2789
- @if (masked) {
2790
- <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
2791
- } @else {
2792
- <div class="ngx-field__array-list">
2793
- @for (itemGroup of formArray.controls; track idx; let idx = $index) {
2794
- <div class="ngx-field__array-item" [attr.data-testid]="'field-' + field.id + '-row'">
2795
- <div class="ngx-field__array-item-header">
2796
- <span class="ngx-field__array-item-title">Item #{{ idx + 1 }}</span>
2797
- @if (!readonly) {
2798
- <button type="button" class="ngx-field__array-remove-btn"
2799
- [attr.data-testid]="'field-' + field.id + '-remove-' + idx" (click)="removeItem(idx)">
2800
- Remove
2801
- </button>
2802
- }
2803
- </div>
2804
- <div class="ngx-field__array-item-fields">
2805
- @for (child of field.children ?? []; track child.id) {
2806
- @if (child.visibility !== false) {
2807
- <ngx-dynamic-field
2808
- [field]="child"
2809
- [control]="getItemChildControl(itemGroup, child.id)"
2810
- [config]="config"
2811
- [language]="language"
2812
- [readonly]="readonly || !!child.readonly"
2813
- [userRoles]="userRoles"
2814
- />
2815
- }
2816
- }
2817
- </div>
2818
- </div>
2819
- }
2820
- @if (formArray.controls.length === 0) {
2821
- <div class="ngx-field__array-empty" [attr.data-testid]="'field-' + field.id + '-empty'">No items added yet.</div>
2822
- }
2823
- </div>
2824
- }
2825
- </div>
2804
+ args: [{ selector: 'ngx-array-field', standalone: true, imports: [ReactiveFormsModule, forwardRef(() => DynamicFieldComponent)], template: `
2805
+ <div class="ngx-field ngx-field--array"
2806
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
2807
+ <div class="ngx-field__array-header">
2808
+ <label class="ngx-field__label">{{ label }}</label>
2809
+ @if (!readonly && !masked) {
2810
+ <button type="button" class="ngx-field__array-add-btn"
2811
+ [attr.data-testid]="'field-' + field.id + '-add'" (click)="addItem()">
2812
+ + Add Item
2813
+ </button>
2814
+ }
2815
+ </div>
2816
+
2817
+ @if (masked) {
2818
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
2819
+ } @else {
2820
+ <div class="ngx-field__array-list">
2821
+ @for (itemGroup of formArray.controls; track idx; let idx = $index) {
2822
+ <div class="ngx-field__array-item" [attr.data-testid]="'field-' + field.id + '-row'">
2823
+ <div class="ngx-field__array-item-header">
2824
+ <span class="ngx-field__array-item-title">Item #{{ idx + 1 }}</span>
2825
+ @if (!readonly) {
2826
+ <button type="button" class="ngx-field__array-remove-btn"
2827
+ [attr.data-testid]="'field-' + field.id + '-remove-' + idx" (click)="removeItem(idx)">
2828
+ Remove
2829
+ </button>
2830
+ }
2831
+ </div>
2832
+ <div class="ngx-field__array-item-fields">
2833
+ @for (child of field.children ?? []; track child.id) {
2834
+ @if (child.visibility !== false) {
2835
+ <ngx-dynamic-field
2836
+ [field]="child"
2837
+ [control]="getItemChildControl(itemGroup, child.id)"
2838
+ [config]="config"
2839
+ [language]="language"
2840
+ [readonly]="readonly || !!child.readonly"
2841
+ [userRoles]="userRoles"
2842
+ />
2843
+ }
2844
+ }
2845
+ </div>
2846
+ </div>
2847
+ }
2848
+ @if (formArray.controls.length === 0) {
2849
+ <div class="ngx-field__array-empty" [attr.data-testid]="'field-' + field.id + '-empty'">No items added yet.</div>
2850
+ }
2851
+ </div>
2852
+ }
2853
+ </div>
2826
2854
  `, 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"] }]
2827
2855
  }], propDecorators: { field: [{
2828
2856
  type: Input
@@ -2980,50 +3008,50 @@ class ImageFieldComponent {
2980
3008
  this.uploads.revokePreviewUrl(this.previewCache);
2981
3009
  }
2982
3010
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ImageFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2983
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: ImageFieldComponent, isStandalone: true, selector: "ngx-image-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
2984
- <div class="ngx-field ngx-field--image"
2985
- [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
2986
- <label class="ngx-field__label" [attr.for]="'field-' + field.id">{{ label }}</label>
2987
- @if (masked) {
2988
- <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
2989
- } @else {
2990
- <div class="ngx-field__image-wrap">
2991
- @if (previewUrl()) {
2992
- <img class="ngx-field__image-preview" [src]="previewUrl()" [alt]="label" />
2993
- } @else {
2994
- <div class="ngx-field__image-placeholder">
2995
- <span>📷</span>
2996
- <span>No image</span>
2997
- </div>
2998
- }
2999
- @if (!readonly) {
3000
- <div class="ngx-field__image-actions">
3001
- <label class="ngx-field__upload-btn" [class.ngx-field__upload-btn--loading]="uploading()">
3002
- @if (uploading()) {
3003
- <span>Uploading…</span>
3004
- } @else {
3005
- <span>{{ previewUrl() ? 'Change' : 'Upload' }}</span>
3006
- <input
3007
- type="file"
3008
- class="ngx-field__file-input"
3009
- [id]="'field-' + field.id"
3010
- accept="image/*"
3011
- [disabled]="field.disabled || uploading()"
3012
- (change)="onFileSelect($any($event.target).files[0])"
3013
- />
3014
- }
3015
- </label>
3016
- @if (previewUrl()) {
3017
- <button type="button" class="ngx-field__remove-btn" (click)="remove()">Remove</button>
3018
- }
3019
- </div>
3020
- }
3021
- </div>
3022
- @if (uploadError()) {
3023
- <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">{{ uploadError() }}</span>
3024
- }
3025
- }
3026
- </div>
3011
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: ImageFieldComponent, isStandalone: true, selector: "ngx-image-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
3012
+ <div class="ngx-field ngx-field--image"
3013
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
3014
+ <label class="ngx-field__label" [attr.for]="'field-' + field.id">{{ label }}</label>
3015
+ @if (masked) {
3016
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
3017
+ } @else {
3018
+ <div class="ngx-field__image-wrap">
3019
+ @if (previewUrl()) {
3020
+ <img class="ngx-field__image-preview" [src]="previewUrl()" [alt]="label" />
3021
+ } @else {
3022
+ <div class="ngx-field__image-placeholder">
3023
+ <span>📷</span>
3024
+ <span>No image</span>
3025
+ </div>
3026
+ }
3027
+ @if (!readonly) {
3028
+ <div class="ngx-field__image-actions">
3029
+ <label class="ngx-field__upload-btn" [class.ngx-field__upload-btn--loading]="uploading()">
3030
+ @if (uploading()) {
3031
+ <span>Uploading…</span>
3032
+ } @else {
3033
+ <span>{{ previewUrl() ? 'Change' : 'Upload' }}</span>
3034
+ <input
3035
+ type="file"
3036
+ class="ngx-field__file-input"
3037
+ [id]="'field-' + field.id"
3038
+ accept="image/*"
3039
+ [disabled]="field.disabled || uploading()"
3040
+ (change)="onFileSelect($any($event.target).files[0])"
3041
+ />
3042
+ }
3043
+ </label>
3044
+ @if (previewUrl()) {
3045
+ <button type="button" class="ngx-field__remove-btn" (click)="remove()">Remove</button>
3046
+ }
3047
+ </div>
3048
+ }
3049
+ </div>
3050
+ @if (uploadError()) {
3051
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">{{ uploadError() }}</span>
3052
+ }
3053
+ }
3054
+ </div>
3027
3055
  `, isInline: true });
3028
3056
  }
3029
3057
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ImageFieldComponent, decorators: [{
@@ -3032,50 +3060,50 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
3032
3060
  selector: 'ngx-image-field',
3033
3061
  standalone: true,
3034
3062
  imports: [],
3035
- template: `
3036
- <div class="ngx-field ngx-field--image"
3037
- [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
3038
- <label class="ngx-field__label" [attr.for]="'field-' + field.id">{{ label }}</label>
3039
- @if (masked) {
3040
- <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
3041
- } @else {
3042
- <div class="ngx-field__image-wrap">
3043
- @if (previewUrl()) {
3044
- <img class="ngx-field__image-preview" [src]="previewUrl()" [alt]="label" />
3045
- } @else {
3046
- <div class="ngx-field__image-placeholder">
3047
- <span>📷</span>
3048
- <span>No image</span>
3049
- </div>
3050
- }
3051
- @if (!readonly) {
3052
- <div class="ngx-field__image-actions">
3053
- <label class="ngx-field__upload-btn" [class.ngx-field__upload-btn--loading]="uploading()">
3054
- @if (uploading()) {
3055
- <span>Uploading…</span>
3056
- } @else {
3057
- <span>{{ previewUrl() ? 'Change' : 'Upload' }}</span>
3058
- <input
3059
- type="file"
3060
- class="ngx-field__file-input"
3061
- [id]="'field-' + field.id"
3062
- accept="image/*"
3063
- [disabled]="field.disabled || uploading()"
3064
- (change)="onFileSelect($any($event.target).files[0])"
3065
- />
3066
- }
3067
- </label>
3068
- @if (previewUrl()) {
3069
- <button type="button" class="ngx-field__remove-btn" (click)="remove()">Remove</button>
3070
- }
3071
- </div>
3072
- }
3073
- </div>
3074
- @if (uploadError()) {
3075
- <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">{{ uploadError() }}</span>
3076
- }
3077
- }
3078
- </div>
3063
+ template: `
3064
+ <div class="ngx-field ngx-field--image"
3065
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
3066
+ <label class="ngx-field__label" [attr.for]="'field-' + field.id">{{ label }}</label>
3067
+ @if (masked) {
3068
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
3069
+ } @else {
3070
+ <div class="ngx-field__image-wrap">
3071
+ @if (previewUrl()) {
3072
+ <img class="ngx-field__image-preview" [src]="previewUrl()" [alt]="label" />
3073
+ } @else {
3074
+ <div class="ngx-field__image-placeholder">
3075
+ <span>📷</span>
3076
+ <span>No image</span>
3077
+ </div>
3078
+ }
3079
+ @if (!readonly) {
3080
+ <div class="ngx-field__image-actions">
3081
+ <label class="ngx-field__upload-btn" [class.ngx-field__upload-btn--loading]="uploading()">
3082
+ @if (uploading()) {
3083
+ <span>Uploading…</span>
3084
+ } @else {
3085
+ <span>{{ previewUrl() ? 'Change' : 'Upload' }}</span>
3086
+ <input
3087
+ type="file"
3088
+ class="ngx-field__file-input"
3089
+ [id]="'field-' + field.id"
3090
+ accept="image/*"
3091
+ [disabled]="field.disabled || uploading()"
3092
+ (change)="onFileSelect($any($event.target).files[0])"
3093
+ />
3094
+ }
3095
+ </label>
3096
+ @if (previewUrl()) {
3097
+ <button type="button" class="ngx-field__remove-btn" (click)="remove()">Remove</button>
3098
+ }
3099
+ </div>
3100
+ }
3101
+ </div>
3102
+ @if (uploadError()) {
3103
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">{{ uploadError() }}</span>
3104
+ }
3105
+ }
3106
+ </div>
3079
3107
  `,
3080
3108
  }]
3081
3109
  }], propDecorators: { field: [{
@@ -3136,48 +3164,48 @@ class FileFieldComponent {
3136
3164
  this.control.markAsTouched();
3137
3165
  }
3138
3166
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: FileFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3139
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: FileFieldComponent, isStandalone: true, selector: "ngx-file-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
3140
- <div class="ngx-field ngx-field--file"
3141
- [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
3142
- <label class="ngx-field__label" [attr.for]="'field-' + field.id">{{ label }}</label>
3143
- @if (masked) {
3144
- <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
3145
- } @else if (readonly) {
3146
- @if (fileUrl()) {
3147
- <a class="ngx-field__file-link" [href]="fileUrl()!" target="_blank" rel="noopener">
3148
- 📎 {{ fileName() || 'Download file' }}
3149
- </a>
3150
- } @else {
3151
- <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">—</span>
3152
- }
3153
- } @else {
3154
- <div class="ngx-field__file-wrap">
3155
- @if (fileName()) {
3156
- <div class="ngx-field__file-selected">
3157
- <span class="ngx-field__file-name">📎 {{ fileName() }}</span>
3158
- <button type="button" class="ngx-field__remove-btn" (click)="remove()" aria-label="Remove file">✕</button>
3159
- </div>
3160
- }
3161
- <label class="ngx-field__upload-btn" [class.ngx-field__upload-btn--loading]="uploading()">
3162
- @if (uploading()) {
3163
- <span>Uploading…</span>
3164
- } @else {
3165
- <span>{{ fileName() ? 'Replace file' : 'Choose file' }}</span>
3166
- <input
3167
- type="file"
3168
- class="ngx-field__file-input"
3169
- [id]="'field-' + field.id"
3170
- [disabled]="field.disabled || uploading()"
3171
- (change)="onFileSelect($any($event.target).files[0])"
3172
- />
3173
- }
3174
- </label>
3175
- </div>
3176
- @if (uploadError()) {
3177
- <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">{{ uploadError() }}</span>
3178
- }
3179
- }
3180
- </div>
3167
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: FileFieldComponent, isStandalone: true, selector: "ngx-file-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
3168
+ <div class="ngx-field ngx-field--file"
3169
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
3170
+ <label class="ngx-field__label" [attr.for]="'field-' + field.id">{{ label }}</label>
3171
+ @if (masked) {
3172
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
3173
+ } @else if (readonly) {
3174
+ @if (fileUrl()) {
3175
+ <a class="ngx-field__file-link" [href]="fileUrl()!" target="_blank" rel="noopener">
3176
+ 📎 {{ fileName() || 'Download file' }}
3177
+ </a>
3178
+ } @else {
3179
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">—</span>
3180
+ }
3181
+ } @else {
3182
+ <div class="ngx-field__file-wrap">
3183
+ @if (fileName()) {
3184
+ <div class="ngx-field__file-selected">
3185
+ <span class="ngx-field__file-name">📎 {{ fileName() }}</span>
3186
+ <button type="button" class="ngx-field__remove-btn" (click)="remove()" aria-label="Remove file">✕</button>
3187
+ </div>
3188
+ }
3189
+ <label class="ngx-field__upload-btn" [class.ngx-field__upload-btn--loading]="uploading()">
3190
+ @if (uploading()) {
3191
+ <span>Uploading…</span>
3192
+ } @else {
3193
+ <span>{{ fileName() ? 'Replace file' : 'Choose file' }}</span>
3194
+ <input
3195
+ type="file"
3196
+ class="ngx-field__file-input"
3197
+ [id]="'field-' + field.id"
3198
+ [disabled]="field.disabled || uploading()"
3199
+ (change)="onFileSelect($any($event.target).files[0])"
3200
+ />
3201
+ }
3202
+ </label>
3203
+ </div>
3204
+ @if (uploadError()) {
3205
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">{{ uploadError() }}</span>
3206
+ }
3207
+ }
3208
+ </div>
3181
3209
  `, isInline: true });
3182
3210
  }
3183
3211
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: FileFieldComponent, decorators: [{
@@ -3186,48 +3214,48 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
3186
3214
  selector: 'ngx-file-field',
3187
3215
  standalone: true,
3188
3216
  imports: [],
3189
- template: `
3190
- <div class="ngx-field ngx-field--file"
3191
- [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
3192
- <label class="ngx-field__label" [attr.for]="'field-' + field.id">{{ label }}</label>
3193
- @if (masked) {
3194
- <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
3195
- } @else if (readonly) {
3196
- @if (fileUrl()) {
3197
- <a class="ngx-field__file-link" [href]="fileUrl()!" target="_blank" rel="noopener">
3198
- 📎 {{ fileName() || 'Download file' }}
3199
- </a>
3200
- } @else {
3201
- <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">—</span>
3202
- }
3203
- } @else {
3204
- <div class="ngx-field__file-wrap">
3205
- @if (fileName()) {
3206
- <div class="ngx-field__file-selected">
3207
- <span class="ngx-field__file-name">📎 {{ fileName() }}</span>
3208
- <button type="button" class="ngx-field__remove-btn" (click)="remove()" aria-label="Remove file">✕</button>
3209
- </div>
3210
- }
3211
- <label class="ngx-field__upload-btn" [class.ngx-field__upload-btn--loading]="uploading()">
3212
- @if (uploading()) {
3213
- <span>Uploading…</span>
3214
- } @else {
3215
- <span>{{ fileName() ? 'Replace file' : 'Choose file' }}</span>
3216
- <input
3217
- type="file"
3218
- class="ngx-field__file-input"
3219
- [id]="'field-' + field.id"
3220
- [disabled]="field.disabled || uploading()"
3221
- (change)="onFileSelect($any($event.target).files[0])"
3222
- />
3223
- }
3224
- </label>
3225
- </div>
3226
- @if (uploadError()) {
3227
- <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">{{ uploadError() }}</span>
3228
- }
3229
- }
3230
- </div>
3217
+ template: `
3218
+ <div class="ngx-field ngx-field--file"
3219
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
3220
+ <label class="ngx-field__label" [attr.for]="'field-' + field.id">{{ label }}</label>
3221
+ @if (masked) {
3222
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
3223
+ } @else if (readonly) {
3224
+ @if (fileUrl()) {
3225
+ <a class="ngx-field__file-link" [href]="fileUrl()!" target="_blank" rel="noopener">
3226
+ 📎 {{ fileName() || 'Download file' }}
3227
+ </a>
3228
+ } @else {
3229
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">—</span>
3230
+ }
3231
+ } @else {
3232
+ <div class="ngx-field__file-wrap">
3233
+ @if (fileName()) {
3234
+ <div class="ngx-field__file-selected">
3235
+ <span class="ngx-field__file-name">📎 {{ fileName() }}</span>
3236
+ <button type="button" class="ngx-field__remove-btn" (click)="remove()" aria-label="Remove file">✕</button>
3237
+ </div>
3238
+ }
3239
+ <label class="ngx-field__upload-btn" [class.ngx-field__upload-btn--loading]="uploading()">
3240
+ @if (uploading()) {
3241
+ <span>Uploading…</span>
3242
+ } @else {
3243
+ <span>{{ fileName() ? 'Replace file' : 'Choose file' }}</span>
3244
+ <input
3245
+ type="file"
3246
+ class="ngx-field__file-input"
3247
+ [id]="'field-' + field.id"
3248
+ [disabled]="field.disabled || uploading()"
3249
+ (change)="onFileSelect($any($event.target).files[0])"
3250
+ />
3251
+ }
3252
+ </label>
3253
+ </div>
3254
+ @if (uploadError()) {
3255
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">{{ uploadError() }}</span>
3256
+ }
3257
+ }
3258
+ </div>
3231
3259
  `,
3232
3260
  }]
3233
3261
  }], propDecorators: { field: [{
@@ -3454,6 +3482,7 @@ class DynamicFormComponent {
3454
3482
  rulesEvaluation = inject(RulesEvaluationService);
3455
3483
  entityRefSelection = inject(EntityRefSelectionService);
3456
3484
  commonModulesRegistry = inject(COMMON_MODULES_REGISTRY, { optional: true });
3485
+ migrations = inject(RECORD_MIGRATIONS, { optional: true }) ?? [];
3457
3486
  Object = Object;
3458
3487
  // ─── Signals (local reactive state) ───────────────────────────────────────
3459
3488
  activeTab = signal('');
@@ -3536,11 +3565,62 @@ class DynamicFormComponent {
3536
3565
  const hiddenFields = this.ruleResult().hiddenFields;
3537
3566
  return rawFields.filter(field => evaluateFieldVisibility(field, currentValues) && !hiddenFields.includes(field.id));
3538
3567
  }
3568
+ /**
3569
+ * Entity-level permissions for the current roles.
3570
+ *
3571
+ * Cached rather than recomputed: this is read from the template several times per change
3572
+ * detection pass, and a fresh object each time is both wasted allocation and a new
3573
+ * identity for anything comparing by reference. Invalidated in `ngOnChanges` when
3574
+ * `config` or `userRoles` change, which are its only inputs.
3575
+ */
3539
3576
  get permissions() {
3540
- return this.rbacService.getPermissions(this.config, this.userRoles);
3577
+ return (this.permissionsCache ??= this.rbacService.getPermissions(this.config, this.userRoles));
3578
+ }
3579
+ permissionsCache = null;
3580
+ /**
3581
+ * Whether the current roles may see this record at all.
3582
+ *
3583
+ * `permissions.view` used to be computed and thrown away — a user whose roles failed it
3584
+ * still received the complete form with every value in the DOM. It is honoured now.
3585
+ *
3586
+ * As with masking, this is presentational: it stops the browser rendering data, it does
3587
+ * not stop the data reaching the browser. Authorize on the server.
3588
+ */
3589
+ get canView() {
3590
+ return this.permissions.canView;
3591
+ }
3592
+ /**
3593
+ * Whether the current roles may delete this record.
3594
+ *
3595
+ * The library ships no delete affordance, so this is surfaced for the consumer to gate
3596
+ * their own — exposed rather than dropped, because a permission the schema declares
3597
+ * should be answerable.
3598
+ */
3599
+ get canDelete() {
3600
+ return this.permissions.canDelete;
3541
3601
  }
3542
3602
  get canSubmit() {
3543
- return this.permissions.canEdit && !this.readonly;
3603
+ return this.canView && this.permissions.canEdit && !this.readonly;
3604
+ }
3605
+ /**
3606
+ * Errors raised by `validation` rules, keyed by the target they point at.
3607
+ *
3608
+ * A rule can declare the record invalid independently of Angular's validators, so form
3609
+ * validity alone is not the whole picture. `DynamicRecordFormComponent.saveSection()`
3610
+ * has always honoured these; `submit()` did not, which meant the same rule blocked one
3611
+ * save path and merely painted a banner on the other — and anything wiring
3612
+ * `(formSubmit)` to persistence wrote records the rules engine had already rejected.
3613
+ */
3614
+ get ruleValidationErrors() {
3615
+ return this.ruleResult().validationErrors;
3616
+ }
3617
+ /** True when a `validation` rule is currently blocking submission. */
3618
+ get hasRuleErrors() {
3619
+ return Object.keys(this.ruleValidationErrors).length > 0;
3620
+ }
3621
+ /** Single source of truth for both the submit guard and the button's disabled state. */
3622
+ get submitBlocked() {
3623
+ return this.form.invalid || this.hasRuleErrors;
3544
3624
  }
3545
3625
  ngOnInit() {
3546
3626
  this.selectionSub = this.entityRefSelection.selection$.subscribe(({ fieldId, option }) => {
@@ -3548,6 +3628,9 @@ class DynamicFormComponent {
3548
3628
  });
3549
3629
  }
3550
3630
  ngOnChanges(changes) {
3631
+ if (changes['config'] || changes['userRoles']) {
3632
+ this.permissionsCache = null;
3633
+ }
3551
3634
  if (changes['config'] && this.config) {
3552
3635
  // Configs arrive as plain JSON from storage or an API, where TypeScript cannot enforce
3553
3636
  // the option shape. Normalise here, at the library boundary, so everything downstream
@@ -3641,13 +3724,14 @@ class DynamicFormComponent {
3641
3724
  buildTabControls(this.config.tabs || [], group);
3642
3725
  this.form = this.fb.group(group);
3643
3726
  if (this.initialData) {
3644
- this.patchForm(this.initialData);
3727
+ this.patchForm(this.upgradeRecord(this.initialData));
3645
3728
  }
3646
3729
  const initialFlattened = this.flattenFormValues();
3647
3730
  this.formValues.set(initialFlattened);
3648
3731
  this.previousValues = { ...initialFlattened };
3649
3732
  this.sessionBaseline.set({ ...initialFlattened });
3650
3733
  this.unlockedFields.set(new Set());
3734
+ this.syncHiddenFieldState(initialFlattened);
3651
3735
  if (this.preview) {
3652
3736
  // Show what an array field looks like rather than an empty slot, then freeze.
3653
3737
  for (const field of this.allFields()) {
@@ -3668,6 +3752,7 @@ class DynamicFormComponent {
3668
3752
  const flattened = this.flattenFormValues();
3669
3753
  this.formValues.set(flattened);
3670
3754
  this.runPatchOnTrue(flattened);
3755
+ this.syncHiddenFieldState(flattened);
3671
3756
  this.previousValues = { ...flattened };
3672
3757
  this.formChange.emit(this.extractRecord());
3673
3758
  });
@@ -3716,10 +3801,31 @@ class DynamicFormComponent {
3716
3801
  group.patchValue(item);
3717
3802
  return group;
3718
3803
  }
3804
+ /**
3805
+ * Bring a record up to the config's `version` before it reaches the form.
3806
+ *
3807
+ * Done here because this is the one place a record enters the renderer, so a consumer
3808
+ * cannot forget it. Migration is a no-op unless the consumer registered steps *and* the
3809
+ * record is behind — an unstamped record is deliberately left alone (see `migrateRecord`).
3810
+ *
3811
+ * A missing or broken step throws from core. That is not swallowed: rendering a
3812
+ * half-understood record would write it back in a shape matching neither schema.
3813
+ */
3814
+ upgradeRecord(data) {
3815
+ if (!this.migrations.length || !this.config)
3816
+ return data;
3817
+ const result = migrateRecord(data, this.config, this.migrations);
3818
+ if (result.applied.length && isDevMode()) {
3819
+ console.info(`[ngx-dynamic-entity] Migrated a "${this.config.entity}" record from config version ` +
3820
+ `${result.from} to ${result.to} (steps: ${result.applied.join(', ')}).`);
3821
+ }
3822
+ return result.record;
3823
+ }
3719
3824
  patchForm(data) {
3720
3825
  if (!data || !this.form)
3721
3826
  return;
3722
3827
  const fieldsById = new Map(this.allFields().map(f => [f.id, f]));
3828
+ const patchedFieldIds = new Set();
3723
3829
  const walkTabs = (tabs) => {
3724
3830
  for (const tab of tabs) {
3725
3831
  const tabData = getTabData(tab.id, data, this.config);
@@ -3744,13 +3850,57 @@ class DynamicFormComponent {
3744
3850
  else {
3745
3851
  ctrl.patchValue(val, { emitEvent: false });
3746
3852
  }
3853
+ patchedFieldIds.add(field.id);
3747
3854
  }
3748
3855
  if (tab.children)
3749
3856
  walkTabs(tab.children);
3750
3857
  }
3751
3858
  };
3752
3859
  walkTabs(this.config.tabs || []);
3860
+ this.warnUnconsumedInitialData(data, fieldsById, patchedFieldIds);
3753
3861
  }
3862
+ /**
3863
+ * A record is nested by tab id (`{ tabId: { fieldId: value } }`) unless the tab sets
3864
+ * `flatData: true`. Handing a flat record to a nested tab finds nothing, so the fields
3865
+ * stay empty — with no error and no clue. That silent miss is the most expensive way to
3866
+ * lose an hour with this library, so name it in dev builds.
3867
+ *
3868
+ * Only top-level keys that match a known field id are reported: anything else is assumed
3869
+ * to be the consumer's own record metadata (ids, timestamps, `_configVersion`) and is not
3870
+ * our business.
3871
+ */
3872
+ warnUnconsumedInitialData(data, fieldsById, patchedFieldIds) {
3873
+ if (!isDevMode() || !this.config)
3874
+ return;
3875
+ const tabIds = new Set();
3876
+ const collectTabIds = (tabs) => {
3877
+ for (const tab of tabs) {
3878
+ tabIds.add(tab.id);
3879
+ if (tab.children)
3880
+ collectTabIds(tab.children);
3881
+ }
3882
+ };
3883
+ collectTabIds(this.config.tabs || []);
3884
+ const unconsumed = Object.keys(data).filter(key => !tabIds.has(key) && // a tab id at the root is the nested container, not a stray field
3885
+ data[key] !== undefined &&
3886
+ fieldsById.has(key) &&
3887
+ !patchedFieldIds.has(key) &&
3888
+ !this.warnedUnconsumedKeys.has(key));
3889
+ if (!unconsumed.length)
3890
+ return;
3891
+ for (const key of unconsumed)
3892
+ this.warnedUnconsumedKeys.add(key);
3893
+ const shown = unconsumed.slice(0, 5).join(', ');
3894
+ const more = unconsumed.length > 5 ? ` (+${unconsumed.length - 5} more)` : '';
3895
+ const plural = unconsumed.length === 1;
3896
+ console.warn(`[ngx-dynamic-entity] initialData has top-level ${plural ? 'key' : 'keys'} matching ` +
3897
+ `${plural ? 'a field that was' : 'fields that were'} not populated: ${shown}${more}. ` +
3898
+ `A record is nested by tab id ({ tabId: { fieldId: value } }) unless the tab sets ` +
3899
+ `flatData: true. Either nest the value under its tab id, or set flatData: true on ` +
3900
+ `the tab holding ${plural ? 'that field' : 'those fields'}.`);
3901
+ }
3902
+ /** Warn once per key per form instance — patchForm re-runs on every initialData change. */
3903
+ warnedUnconsumedKeys = new Set();
3754
3904
  /** Assemble full nested record from per-tab FormGroups, respecting flatData, refererField & arrays. */
3755
3905
  extractRecord() {
3756
3906
  if (!this.form || !this.config)
@@ -3849,6 +3999,57 @@ class DynamicFormComponent {
3849
3999
  return this.getControl(targetId, autoPatch.targetTab);
3850
4000
  }
3851
4001
  // ─── Helpers ──────────────────────────────────────────────────────────────
4002
+ /**
4003
+ * The fields the render filter operates on — a tab's own fields, at every tab depth.
4004
+ * Deliberately not `allFields()`: `group` and `array` children are shown or hidden with
4005
+ * their parent, and toggling a child's state independently would fight the parent's.
4006
+ */
4007
+ tabFields() {
4008
+ const out = [];
4009
+ const walk = (tabs) => {
4010
+ for (const tab of tabs ?? []) {
4011
+ out.push(...(tab.fields ?? []));
4012
+ walk(tab.children);
4013
+ }
4014
+ };
4015
+ walk(this.config?.tabs);
4016
+ return out;
4017
+ }
4018
+ /**
4019
+ * Keep a hidden field's control out of form validity.
4020
+ *
4021
+ * A field hidden by a rule or a `showWhen` condition is filtered out of the render, but
4022
+ * its validators stay attached — so a hidden *required* field holds `form.invalid` true
4023
+ * forever, permanently disabling Save with nothing on screen to explain why.
4024
+ *
4025
+ * Disabling is the fix rather than stripping validators: Angular excludes disabled
4026
+ * controls from validity, and the field's own validators survive intact for when it
4027
+ * comes back. Values are untouched — `flattenFormValues` and `extractRecord` both read
4028
+ * `control.value`, which a disabled control still carries, so rule evaluation and the
4029
+ * emitted record see exactly what they saw before. That also keeps this from feeding
4030
+ * back on itself: hiding a field cannot change the values the rules are evaluated over.
4031
+ *
4032
+ * The visibility predicate is the same one `fieldsForActiveTab` filters with, so what is
4033
+ * rendered and what counts toward validity cannot drift apart.
4034
+ */
4035
+ syncHiddenFieldState(values) {
4036
+ if (this.preview)
4037
+ return; // preview freezes the whole form on purpose
4038
+ const hiddenByRule = new Set(this.ruleResult().hiddenFields);
4039
+ for (const field of this.tabFields()) {
4040
+ const ctrl = this.getControl(field.id);
4041
+ if (!ctrl)
4042
+ continue;
4043
+ const hidden = hiddenByRule.has(field.id) || !evaluateFieldVisibility(field, values);
4044
+ if (hidden) {
4045
+ if (ctrl.enabled)
4046
+ ctrl.disable({ emitEvent: false });
4047
+ }
4048
+ else if (ctrl.disabled && !field.disabled) {
4049
+ ctrl.enable({ emitEvent: false });
4050
+ }
4051
+ }
4052
+ }
3852
4053
  /** Every field in the config, including `group`/`array` children. */
3853
4054
  allFields() {
3854
4055
  const out = [];
@@ -3918,7 +4119,7 @@ class DynamicFormComponent {
3918
4119
  return resolveLabel(tab.label, this.language);
3919
4120
  }
3920
4121
  async submit() {
3921
- if (!this.canSubmit || this.form.invalid) {
4122
+ if (!this.canSubmit || this.submitBlocked) {
3922
4123
  this.form.markAllAsTouched();
3923
4124
  return;
3924
4125
  }
@@ -3947,11 +4148,11 @@ class DynamicFormComponent {
3947
4148
  this.formReset.emit();
3948
4149
  }
3949
4150
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: DynamicFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3950
- 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: "<form [formGroup]=\"form\" (ngSubmit)=\"submit()\" class=\"ngx-form\" novalidate>\r\n\r\n <!-- Error message banner -->\r\n @if (error) {\r\n <div class=\"ngx-form__error-banner\" role=\"alert\" data-testid=\"form-error\">{{ error }}</div>\r\n }\r\n\r\n <!-- Rules Info Banners -->\r\n @for (infoKey of showInfoBanners ? Object.keys(ruleResult().infoBanners) : []; track infoKey) {\r\n <div class=\"ngx-form__info-banner\" role=\"status\" [attr.data-testid]=\"'info-banner-' + infoKey\">\r\n \u2139\uFE0F {{ ruleResult().infoBanners[infoKey] }}\r\n </div>\r\n }\r\n\r\n <!-- Rules Validation Errors & Warnings -->\r\n @for (errKey of Object.keys(ruleResult().validationErrors); track errKey) {\r\n <div class=\"ngx-form__error-banner\" role=\"alert\" [attr.data-testid]=\"'rule-error-' + errKey\">\r\n \u26A0\uFE0F {{ ruleResult().validationErrors[errKey] }}\r\n </div>\r\n }\r\n @for (warnKey of Object.keys(ruleResult().validationWarnings); track warnKey) {\r\n <div class=\"ngx-form__warning-banner\" role=\"status\" [attr.data-testid]=\"'rule-warning-' + warnKey\">\r\n \u26A1 {{ ruleResult().validationWarnings[warnKey] }}\r\n </div>\r\n }\r\n\r\n <!-- Deferred VALUE_CHANGED notice for criticalField edits (vs session baseline) -->\r\n @if (changedCriticalFields().length > 0) {\r\n <div class=\"ngx-form__warning-banner\" role=\"status\" data-testid=\"critical-change-banner\">\r\n \uD83D\uDD12 Critical field changed:\r\n @for (field of changedCriticalFields(); track field.id) {\r\n <span>{{ resolveFieldLabel(field) }}{{ $last ? '' : ', ' }}</span>\r\n }\r\n \u2014 this differs from the value at the start of this session.\r\n </div>\r\n }\r\n\r\n <!-- Primary Tab bar \u2014 only if visible tabs defined -->\r\n @if (visibleTabs.length > 0) {\r\n <div class=\"ngx-form__tabs\" role=\"tablist\" data-testid=\"tab-strip\">\r\n @for (tab of visibleTabs; track tab.id) {\r\n <button\r\n type=\"button\"\r\n class=\"ngx-form__tab\"\r\n [attr.data-testid]=\"'tab-' + tab.id\"\r\n [class.ngx-form__tab--active]=\"activeTab() === tab.id\"\r\n (click)=\"setActiveTab(tab.id)\"\r\n role=\"tab\"\r\n [attr.aria-selected]=\"activeTab() === tab.id\"\r\n >\r\n {{ resolveTabLabel(tab) }}\r\n </button>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Sub-tab bar \u2014 only if active tab has sub-tabs -->\r\n @if (visibleSubTabs.length > 0) {\r\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;\">\r\n @for (subTab of visibleSubTabs; track subTab.id) {\r\n <button\r\n type=\"button\"\r\n class=\"ngx-form__tab ngx-form__tab--sub\"\r\n [attr.data-testid]=\"'subtab-' + subTab.id\"\r\n [class.ngx-form__tab--active]=\"(activeSubTabConfig?.id) === subTab.id\"\r\n (click)=\"setActiveSubTab(subTab.id)\"\r\n role=\"tab\"\r\n [attr.aria-selected]=\"(activeSubTabConfig?.id) === subTab.id\"\r\n >\r\n {{ resolveTabLabel(subTab) }}\r\n </button>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Module tab component OR Fields for active tab -->\r\n @if (activeTabModuleComponent) {\r\n <div class=\"ngx-form__module-panel\" data-testid=\"module-panel\">\r\n <ng-container *ngComponentOutlet=\"activeTabModuleComponent; inputs: (activeSubTabConfig || activeTabConfig)?.moduleInputs\"></ng-container>\r\n </div>\r\n } @else {\r\n <!-- Fields for active tab (Responsive 12-column grid) -->\r\n <div class=\"ngx-form__panel\" data-testid=\"form-panel\">\r\n @for (field of fieldsForActiveTab; track field.id) {\r\n <div\r\n class=\"ngx-form__field-slot\"\r\n [style.gridColumn]=\"getFieldSpan(field)\"\r\n [attr.id]=\"'field-container-' + field.id\"\r\n >\r\n <ngx-dynamic-field\r\n [field]=\"field\"\r\n [control]=\"getControl(field.id, (activeSubTabConfig || activeTabConfig)?.id)!\"\r\n [config]=\"config\"\r\n [currentTabId]=\"(activeSubTabConfig || activeTabConfig)?.id\"\r\n [language]=\"language\"\r\n [readonly]=\"isFieldReadonly(field)\"\r\n [userRoles]=\"userRoles\"\r\n />\r\n @if (field.criticalField && !readonly) {\r\n <button\r\n type=\"button\"\r\n class=\"ngx-form__lock\"\r\n [attr.data-testid]=\"'lock-' + field.id\"\r\n [attr.aria-pressed]=\"!isFieldLocked(field)\"\r\n [attr.aria-label]=\"\r\n (isFieldLocked(field) ? 'Unlock ' : 'Lock ') + resolveFieldLabel(field)\r\n \"\r\n [title]=\"\r\n isFieldLocked(field)\r\n ? 'Critical field \u2014 click to unlock for editing'\r\n : 'Critical field \u2014 click to lock'\r\n \"\r\n (click)=\"toggleFieldLock(field)\"\r\n >\r\n {{ isFieldLocked(field) ? '\uD83D\uDD12' : '\uD83D\uDD13' }}\r\n </button>\r\n }\r\n </div>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Submit button \u2014 gated by RBAC and isSaving lock -->\r\n @if (!readonly && permissions.canEdit) {\r\n <div class=\"ngx-form__actions\" data-testid=\"form-actions\">\r\n <button\r\n type=\"button\"\r\n class=\"ngx-form__reset\"\r\n data-testid=\"form-reset\"\r\n (click)=\"reset()\"\r\n [disabled]=\"isSaving()\"\r\n >\r\n Reset\r\n </button>\r\n <button\r\n type=\"submit\"\r\n class=\"ngx-form__submit\"\r\n data-testid=\"form-submit\"\r\n [disabled]=\"form.invalid\"\r\n title=\"Shortcut: Ctrl+S\"\r\n >\r\n @if (isSaving() || loading) {\r\n Saving\u2026\r\n } @else {\r\n Save\r\n }\r\n </button>\r\n </div>\r\n }\r\n\r\n</form>\r\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"] }] });
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 <!-- Module tab component OR Fields for active tab -->\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.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\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"] }] });
3951
4152
  }
3952
4153
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: DynamicFormComponent, decorators: [{
3953
4154
  type: Component,
3954
- args: [{ selector: 'ngx-dynamic-form', standalone: true, imports: [ReactiveFormsModule, DynamicFieldComponent, NgComponentOutlet], providers: [EntityRefSelectionService], template: "<form [formGroup]=\"form\" (ngSubmit)=\"submit()\" class=\"ngx-form\" novalidate>\r\n\r\n <!-- Error message banner -->\r\n @if (error) {\r\n <div class=\"ngx-form__error-banner\" role=\"alert\" data-testid=\"form-error\">{{ error }}</div>\r\n }\r\n\r\n <!-- Rules Info Banners -->\r\n @for (infoKey of showInfoBanners ? Object.keys(ruleResult().infoBanners) : []; track infoKey) {\r\n <div class=\"ngx-form__info-banner\" role=\"status\" [attr.data-testid]=\"'info-banner-' + infoKey\">\r\n \u2139\uFE0F {{ ruleResult().infoBanners[infoKey] }}\r\n </div>\r\n }\r\n\r\n <!-- Rules Validation Errors & Warnings -->\r\n @for (errKey of Object.keys(ruleResult().validationErrors); track errKey) {\r\n <div class=\"ngx-form__error-banner\" role=\"alert\" [attr.data-testid]=\"'rule-error-' + errKey\">\r\n \u26A0\uFE0F {{ ruleResult().validationErrors[errKey] }}\r\n </div>\r\n }\r\n @for (warnKey of Object.keys(ruleResult().validationWarnings); track warnKey) {\r\n <div class=\"ngx-form__warning-banner\" role=\"status\" [attr.data-testid]=\"'rule-warning-' + warnKey\">\r\n \u26A1 {{ ruleResult().validationWarnings[warnKey] }}\r\n </div>\r\n }\r\n\r\n <!-- Deferred VALUE_CHANGED notice for criticalField edits (vs session baseline) -->\r\n @if (changedCriticalFields().length > 0) {\r\n <div class=\"ngx-form__warning-banner\" role=\"status\" data-testid=\"critical-change-banner\">\r\n \uD83D\uDD12 Critical field changed:\r\n @for (field of changedCriticalFields(); track field.id) {\r\n <span>{{ resolveFieldLabel(field) }}{{ $last ? '' : ', ' }}</span>\r\n }\r\n \u2014 this differs from the value at the start of this session.\r\n </div>\r\n }\r\n\r\n <!-- Primary Tab bar \u2014 only if visible tabs defined -->\r\n @if (visibleTabs.length > 0) {\r\n <div class=\"ngx-form__tabs\" role=\"tablist\" data-testid=\"tab-strip\">\r\n @for (tab of visibleTabs; track tab.id) {\r\n <button\r\n type=\"button\"\r\n class=\"ngx-form__tab\"\r\n [attr.data-testid]=\"'tab-' + tab.id\"\r\n [class.ngx-form__tab--active]=\"activeTab() === tab.id\"\r\n (click)=\"setActiveTab(tab.id)\"\r\n role=\"tab\"\r\n [attr.aria-selected]=\"activeTab() === tab.id\"\r\n >\r\n {{ resolveTabLabel(tab) }}\r\n </button>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Sub-tab bar \u2014 only if active tab has sub-tabs -->\r\n @if (visibleSubTabs.length > 0) {\r\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;\">\r\n @for (subTab of visibleSubTabs; track subTab.id) {\r\n <button\r\n type=\"button\"\r\n class=\"ngx-form__tab ngx-form__tab--sub\"\r\n [attr.data-testid]=\"'subtab-' + subTab.id\"\r\n [class.ngx-form__tab--active]=\"(activeSubTabConfig?.id) === subTab.id\"\r\n (click)=\"setActiveSubTab(subTab.id)\"\r\n role=\"tab\"\r\n [attr.aria-selected]=\"(activeSubTabConfig?.id) === subTab.id\"\r\n >\r\n {{ resolveTabLabel(subTab) }}\r\n </button>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Module tab component OR Fields for active tab -->\r\n @if (activeTabModuleComponent) {\r\n <div class=\"ngx-form__module-panel\" data-testid=\"module-panel\">\r\n <ng-container *ngComponentOutlet=\"activeTabModuleComponent; inputs: (activeSubTabConfig || activeTabConfig)?.moduleInputs\"></ng-container>\r\n </div>\r\n } @else {\r\n <!-- Fields for active tab (Responsive 12-column grid) -->\r\n <div class=\"ngx-form__panel\" data-testid=\"form-panel\">\r\n @for (field of fieldsForActiveTab; track field.id) {\r\n <div\r\n class=\"ngx-form__field-slot\"\r\n [style.gridColumn]=\"getFieldSpan(field)\"\r\n [attr.id]=\"'field-container-' + field.id\"\r\n >\r\n <ngx-dynamic-field\r\n [field]=\"field\"\r\n [control]=\"getControl(field.id, (activeSubTabConfig || activeTabConfig)?.id)!\"\r\n [config]=\"config\"\r\n [currentTabId]=\"(activeSubTabConfig || activeTabConfig)?.id\"\r\n [language]=\"language\"\r\n [readonly]=\"isFieldReadonly(field)\"\r\n [userRoles]=\"userRoles\"\r\n />\r\n @if (field.criticalField && !readonly) {\r\n <button\r\n type=\"button\"\r\n class=\"ngx-form__lock\"\r\n [attr.data-testid]=\"'lock-' + field.id\"\r\n [attr.aria-pressed]=\"!isFieldLocked(field)\"\r\n [attr.aria-label]=\"\r\n (isFieldLocked(field) ? 'Unlock ' : 'Lock ') + resolveFieldLabel(field)\r\n \"\r\n [title]=\"\r\n isFieldLocked(field)\r\n ? 'Critical field \u2014 click to unlock for editing'\r\n : 'Critical field \u2014 click to lock'\r\n \"\r\n (click)=\"toggleFieldLock(field)\"\r\n >\r\n {{ isFieldLocked(field) ? '\uD83D\uDD12' : '\uD83D\uDD13' }}\r\n </button>\r\n }\r\n </div>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Submit button \u2014 gated by RBAC and isSaving lock -->\r\n @if (!readonly && permissions.canEdit) {\r\n <div class=\"ngx-form__actions\" data-testid=\"form-actions\">\r\n <button\r\n type=\"button\"\r\n class=\"ngx-form__reset\"\r\n data-testid=\"form-reset\"\r\n (click)=\"reset()\"\r\n [disabled]=\"isSaving()\"\r\n >\r\n Reset\r\n </button>\r\n <button\r\n type=\"submit\"\r\n class=\"ngx-form__submit\"\r\n data-testid=\"form-submit\"\r\n [disabled]=\"form.invalid\"\r\n title=\"Shortcut: Ctrl+S\"\r\n >\r\n @if (isSaving() || loading) {\r\n Saving\u2026\r\n } @else {\r\n Save\r\n }\r\n </button>\r\n </div>\r\n }\r\n\r\n</form>\r\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"] }]
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 <!-- Module tab component OR Fields for active tab -->\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.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\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"] }]
3955
4156
  }], propDecorators: { config: [{
3956
4157
  type: Input
3957
4158
  }], rules: [{
@@ -3991,6 +4192,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
3991
4192
  args: ['window:keydown', ['$event']]
3992
4193
  }] } });
3993
4194
 
4195
+ /**
4196
+ * Distinguishes "no tab holds this field" from "the field holds undefined", so the tab walk
4197
+ * can keep searching in the first case and stop in the second.
4198
+ */
4199
+ const FIELD_NOT_FOUND = Symbol('field-not-found');
3994
4200
  /**
3995
4201
  * DynamicRecordFormComponent — comprehensive tabbed record view & edit component.
3996
4202
  * Supports cross-tab rule evaluation, profile header, summary drawer (showOnMinimize),
@@ -4340,14 +4546,34 @@ class DynamicRecordFormComponent {
4340
4546
  controlFor(fieldId) {
4341
4547
  return this.dynamicFormComp?.getControl(fieldId) ?? null;
4342
4548
  }
4343
- /** A field's current value, read from the record so it is stable during render. */
4549
+ /**
4550
+ * A field's current value, read from the record so it is stable during render.
4551
+ *
4552
+ * Resolved through `getTabData` so it honours exactly the nesting contract the form
4553
+ * patches with — `{ tabId: { fieldId } }`, or the record root when the tab sets
4554
+ * `flatData`. Reading the record flat instead is not a shortcut: it makes the summary
4555
+ * display values the form never loaded, which reads as "the data is there" while every
4556
+ * control behind it is empty.
4557
+ *
4558
+ * Walks sub-tabs as well: a summary, header, or profile field may sit on any tab at any
4559
+ * depth.
4560
+ */
4344
4561
  fieldValue(fieldId) {
4345
- for (const tab of this.config?.tabs ?? []) {
4346
- const data = getTabData(tab.id, this.currentData(), this.config);
4347
- if (data && fieldId in data)
4348
- return data[fieldId];
4349
- }
4350
- return undefined;
4562
+ const visit = (tabs = []) => {
4563
+ for (const tab of tabs) {
4564
+ const data = getTabData(tab.id, this.currentData(), this.config);
4565
+ if (data && fieldId in data)
4566
+ return data[fieldId];
4567
+ if (tab.children) {
4568
+ const found = visit(tab.children);
4569
+ if (found !== FIELD_NOT_FOUND)
4570
+ return found;
4571
+ }
4572
+ }
4573
+ return FIELD_NOT_FOUND;
4574
+ };
4575
+ const result = visit(this.config?.tabs);
4576
+ return result === FIELD_NOT_FOUND ? undefined : result;
4351
4577
  }
4352
4578
  isModified = computed(() => {
4353
4579
  const orig = this.originalBaseline();
@@ -4391,7 +4617,7 @@ class DynamicRecordFormComponent {
4391
4617
  }
4392
4618
  /** Summary values render through the shared core formatter, not a local stringifier. */
4393
4619
  formatFieldValue(field) {
4394
- return formatDisplayValue(field.type, field.options, this.currentData()[field.id], this.language);
4620
+ return formatDisplayValue(field.type, field.options, this.fieldValue(field.id), this.language);
4395
4621
  }
4396
4622
  jumpToField(fieldId) {
4397
4623
  // Find parent tab for field
@@ -4411,11 +4637,11 @@ class DynamicRecordFormComponent {
4411
4637
  }
4412
4638
  }
4413
4639
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: DynamicRecordFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4414
- 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\">\r\n <!-- Record Header -->\r\n <div class=\"ngx-record-editor__header\">\r\n <div class=\"ngx-record-editor__title-group\">\r\n @if (profileImageUrl) {\r\n <img\r\n class=\"ngx-record-editor__avatar\"\r\n data-testid=\"header-profile-image\"\r\n [src]=\"profileImageUrl\"\r\n [alt]=\"recordTitle\"\r\n />\r\n } @else {\r\n <div class=\"ngx-record-editor__avatar\" data-testid=\"header-avatar-initial\">\r\n {{ avatarLetter }}\r\n </div>\r\n }\r\n <div>\r\n <h2 class=\"ngx-record-editor__title\" data-testid=\"record-title\">\r\n {{ recordTitle }}\r\n </h2>\r\n <span class=\"ngx-record-editor__subtitle\">Entity: {{ config.entity }}</span>\r\n </div>\r\n </div>\r\n\r\n <!-- Record-level status switch, outside section editing -->\r\n @if (headerToggleField; as toggleField) {\r\n <label class=\"ngx-record-editor__header-toggle\">\r\n <span>{{ formatFieldLabel(toggleField) }}</span>\r\n <input\r\n type=\"checkbox\"\r\n role=\"switch\"\r\n data-testid=\"header-toggle\"\r\n [checked]=\"headerToggleValue\"\r\n [disabled]=\"recordReadOnly\"\r\n [attr.aria-checked]=\"headerToggleValue\"\r\n (change)=\"toggleHeaderStatus()\"\r\n />\r\n <span class=\"ngx-record-editor__header-toggle-text\">{{ headerToggleValue ? 'Yes' : 'No' }}</span>\r\n </label>\r\n }\r\n </div>\r\n\r\n <!-- Dismissible info banners (rules `info` action) -->\r\n @for (banner of infoBanners; track banner.fieldId) {\r\n <div\r\n class=\"ngx-record-editor__banner ngx-record-editor__banner--info\"\r\n role=\"status\"\r\n [attr.data-testid]=\"'info-banner-' + banner.fieldId\"\r\n >\r\n <span>\u2139\uFE0F {{ banner.message }}</span>\r\n <button\r\n type=\"button\"\r\n class=\"ngx-record-editor__banner-dismiss\"\r\n [attr.data-testid]=\"'dismiss-' + banner.fieldId\"\r\n [attr.aria-label]=\"'Dismiss: ' + banner.message\"\r\n (click)=\"dismissInfoBanner(banner.fieldId)\"\r\n >\u2715</button>\r\n </div>\r\n }\r\n\r\n <!-- Modification baseline banner -->\r\n @if (isModified()) {\r\n <div class=\"ngx-record-editor__banner ngx-record-editor__banner--warning\">\r\n \uD83D\uDD12 Record modified from session baseline \u2014 unsaved changes pending.\r\n </div>\r\n }\r\n\r\n <!-- Summary Panel (showOnMinimize fields with interactive jump) -->\r\n @if (summaryFields().length > 0) {\r\n <div class=\"ngx-record-editor__summary-panel\" data-testid=\"summary-panel\">\r\n @for (sf of summaryFields(); track sf.id) {\r\n <div\r\n class=\"ngx-record-editor__summary-item\"\r\n [attr.data-testid]=\"'summary-' + sf.id\"\r\n (click)=\"jumpToField(sf.id)\"\r\n [title]=\"'Click to jump to ' + formatFieldLabel(sf)\"\r\n >\r\n <span class=\"ngx-record-editor__summary-label\">{{ formatFieldLabel(sf) }}</span>\r\n <span class=\"ngx-record-editor__summary-val\">{{ formatFieldValue(sf) }}</span>\r\n </div>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Section edit bar: one tab is edited at a time, and validated on its own -->\r\n @if (viewMode && !recordReadOnly) {\r\n <div class=\"ngx-record-editor__section-bar\">\r\n @if (isEditingActiveTab) {\r\n <span class=\"ngx-record-editor__section-hint\">Editing this section</span>\r\n <button type=\"button\" class=\"ngx-form__reset\" data-testid=\"cancel-section\" (click)=\"cancelSection()\">\r\n Cancel\r\n </button>\r\n <button type=\"button\" class=\"ngx-form__submit\" data-testid=\"save-section\" (click)=\"saveSection()\">\r\n Save section\r\n </button>\r\n } @else {\r\n <button type=\"button\" class=\"ngx-form__reset\" data-testid=\"edit-section\" (click)=\"editSection()\">\r\n Edit section\r\n </button>\r\n }\r\n </div>\r\n }\r\n\r\n @if (Object.keys(sectionErrors()).length > 0) {\r\n <div class=\"ngx-record-editor__banner ngx-record-editor__banner--error\" role=\"alert\" data-testid=\"section-errors\">\r\n @for (key of Object.keys(sectionErrors()); track key) {\r\n <div>\u26A0\uFE0F {{ sectionErrors()[key] }}</div>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Array rows: listed here, edited in the drawer below -->\r\n @for (arrayField of arrayFieldsForActiveTab; track arrayField.id) {\r\n <div class=\"ngx-record-editor__rows\" [attr.data-testid]=\"'rows-' + arrayField.id\">\r\n <div class=\"ngx-record-editor__rows-head\">\r\n <span class=\"ngx-record-editor__summary-label\">{{ formatFieldLabel(arrayField) }}</span>\r\n @if (!sectionReadOnly) {\r\n <button\r\n type=\"button\"\r\n class=\"ngx-form__reset\"\r\n [attr.data-testid]=\"'add-row-' + arrayField.id\"\r\n (click)=\"openAddRow(arrayField)\"\r\n >+ Row</button>\r\n }\r\n </div>\r\n\r\n @for (row of rowsOf(arrayField); track $index) {\r\n <div class=\"ngx-record-editor__row\" [attr.data-testid]=\"'row-' + arrayField.id + '-' + $index\">\r\n <span class=\"ngx-record-editor__row-text\">{{ rowSummary(arrayField, row) }}</span>\r\n @if (!sectionReadOnly) {\r\n <button type=\"button\" class=\"ngx-record-editor__row-btn\"\r\n [attr.data-testid]=\"'edit-row-' + arrayField.id + '-' + $index\"\r\n (click)=\"openEditRow(arrayField, $index)\">Edit</button>\r\n <button type=\"button\" class=\"ngx-record-editor__row-btn\"\r\n [attr.data-testid]=\"'delete-row-' + arrayField.id + '-' + $index\"\r\n (click)=\"deleteRow(arrayField, $index)\">Delete</button>\r\n }\r\n </div>\r\n } @empty {\r\n <p class=\"ngx-record-editor__row-empty\">No rows yet.</p>\r\n }\r\n </div>\r\n }\r\n\r\n <!--\r\n Row drawer. Deliberately outside the tab panel: building a row's FormGroup inside the\r\n tab view races the tab's own initialisation (the reference hit this with FormArray).\r\n -->\r\n @if (inlineRowField(); as rowField) {\r\n <div class=\"ngx-record-editor__drawer\" data-testid=\"row-drawer\" [formGroup]=\"inlineRowForm!\">\r\n <div class=\"ngx-record-editor__rows-head\">\r\n <strong>{{ inlineRowIndex() === null ? 'Add' : 'Edit' }} {{ formatFieldLabel(rowField) }} row</strong>\r\n </div>\r\n\r\n <div class=\"ngx-form__panel\">\r\n @for (child of rowField.children ?? []; track child.id) {\r\n <ngx-dynamic-field\r\n [field]=\"child\"\r\n [control]=\"inlineRowForm!.get(child.id)!\"\r\n [config]=\"config\"\r\n [language]=\"language\"\r\n [readonly]=\"false\"\r\n [userRoles]=\"userRoles\"\r\n [style.gridColumn]=\"'span 12'\"\r\n />\r\n }\r\n </div>\r\n\r\n <div class=\"ngx-record-editor__section-bar\">\r\n <button type=\"button\" class=\"ngx-form__reset\" data-testid=\"cancel-row\" (click)=\"cancelRow()\">Cancel</button>\r\n <button type=\"button\" class=\"ngx-form__submit\" data-testid=\"save-row\" (click)=\"saveRow()\">Save row</button>\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- Main Dynamic Form -->\r\n <ngx-dynamic-form\r\n [config]=\"config\"\r\n [rules]=\"rules\"\r\n [initialData]=\"initialData\"\r\n [originalData]=\"originalBaseline()\"\r\n [userRoles]=\"userRoles\"\r\n [language]=\"language\"\r\n [readonly]=\"sectionReadOnly\"\r\n [readOnlyFields]=\"readOnlyFields\"\r\n [showInfoBanners]=\"false\"\r\n [loading]=\"loading\"\r\n [error]=\"error\"\r\n (formChange)=\"onFormChange($event)\"\r\n (formSubmit)=\"onFormSubmit($event)\"\r\n (formReset)=\"onFormReset()\"\r\n (activeTabChange)=\"onActiveTabChange($event)\"\r\n />\r\n</div>\r\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"] }] });
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"] }] });
4415
4641
  }
4416
4642
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: DynamicRecordFormComponent, decorators: [{
4417
4643
  type: Component,
4418
- args: [{ selector: 'ngx-dynamic-record-form', standalone: true, imports: [CommonModule, ReactiveFormsModule, DynamicFormComponent, DynamicFieldComponent], template: "<div class=\"ngx-record-editor\">\r\n <!-- Record Header -->\r\n <div class=\"ngx-record-editor__header\">\r\n <div class=\"ngx-record-editor__title-group\">\r\n @if (profileImageUrl) {\r\n <img\r\n class=\"ngx-record-editor__avatar\"\r\n data-testid=\"header-profile-image\"\r\n [src]=\"profileImageUrl\"\r\n [alt]=\"recordTitle\"\r\n />\r\n } @else {\r\n <div class=\"ngx-record-editor__avatar\" data-testid=\"header-avatar-initial\">\r\n {{ avatarLetter }}\r\n </div>\r\n }\r\n <div>\r\n <h2 class=\"ngx-record-editor__title\" data-testid=\"record-title\">\r\n {{ recordTitle }}\r\n </h2>\r\n <span class=\"ngx-record-editor__subtitle\">Entity: {{ config.entity }}</span>\r\n </div>\r\n </div>\r\n\r\n <!-- Record-level status switch, outside section editing -->\r\n @if (headerToggleField; as toggleField) {\r\n <label class=\"ngx-record-editor__header-toggle\">\r\n <span>{{ formatFieldLabel(toggleField) }}</span>\r\n <input\r\n type=\"checkbox\"\r\n role=\"switch\"\r\n data-testid=\"header-toggle\"\r\n [checked]=\"headerToggleValue\"\r\n [disabled]=\"recordReadOnly\"\r\n [attr.aria-checked]=\"headerToggleValue\"\r\n (change)=\"toggleHeaderStatus()\"\r\n />\r\n <span class=\"ngx-record-editor__header-toggle-text\">{{ headerToggleValue ? 'Yes' : 'No' }}</span>\r\n </label>\r\n }\r\n </div>\r\n\r\n <!-- Dismissible info banners (rules `info` action) -->\r\n @for (banner of infoBanners; track banner.fieldId) {\r\n <div\r\n class=\"ngx-record-editor__banner ngx-record-editor__banner--info\"\r\n role=\"status\"\r\n [attr.data-testid]=\"'info-banner-' + banner.fieldId\"\r\n >\r\n <span>\u2139\uFE0F {{ banner.message }}</span>\r\n <button\r\n type=\"button\"\r\n class=\"ngx-record-editor__banner-dismiss\"\r\n [attr.data-testid]=\"'dismiss-' + banner.fieldId\"\r\n [attr.aria-label]=\"'Dismiss: ' + banner.message\"\r\n (click)=\"dismissInfoBanner(banner.fieldId)\"\r\n >\u2715</button>\r\n </div>\r\n }\r\n\r\n <!-- Modification baseline banner -->\r\n @if (isModified()) {\r\n <div class=\"ngx-record-editor__banner ngx-record-editor__banner--warning\">\r\n \uD83D\uDD12 Record modified from session baseline \u2014 unsaved changes pending.\r\n </div>\r\n }\r\n\r\n <!-- Summary Panel (showOnMinimize fields with interactive jump) -->\r\n @if (summaryFields().length > 0) {\r\n <div class=\"ngx-record-editor__summary-panel\" data-testid=\"summary-panel\">\r\n @for (sf of summaryFields(); track sf.id) {\r\n <div\r\n class=\"ngx-record-editor__summary-item\"\r\n [attr.data-testid]=\"'summary-' + sf.id\"\r\n (click)=\"jumpToField(sf.id)\"\r\n [title]=\"'Click to jump to ' + formatFieldLabel(sf)\"\r\n >\r\n <span class=\"ngx-record-editor__summary-label\">{{ formatFieldLabel(sf) }}</span>\r\n <span class=\"ngx-record-editor__summary-val\">{{ formatFieldValue(sf) }}</span>\r\n </div>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Section edit bar: one tab is edited at a time, and validated on its own -->\r\n @if (viewMode && !recordReadOnly) {\r\n <div class=\"ngx-record-editor__section-bar\">\r\n @if (isEditingActiveTab) {\r\n <span class=\"ngx-record-editor__section-hint\">Editing this section</span>\r\n <button type=\"button\" class=\"ngx-form__reset\" data-testid=\"cancel-section\" (click)=\"cancelSection()\">\r\n Cancel\r\n </button>\r\n <button type=\"button\" class=\"ngx-form__submit\" data-testid=\"save-section\" (click)=\"saveSection()\">\r\n Save section\r\n </button>\r\n } @else {\r\n <button type=\"button\" class=\"ngx-form__reset\" data-testid=\"edit-section\" (click)=\"editSection()\">\r\n Edit section\r\n </button>\r\n }\r\n </div>\r\n }\r\n\r\n @if (Object.keys(sectionErrors()).length > 0) {\r\n <div class=\"ngx-record-editor__banner ngx-record-editor__banner--error\" role=\"alert\" data-testid=\"section-errors\">\r\n @for (key of Object.keys(sectionErrors()); track key) {\r\n <div>\u26A0\uFE0F {{ sectionErrors()[key] }}</div>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Array rows: listed here, edited in the drawer below -->\r\n @for (arrayField of arrayFieldsForActiveTab; track arrayField.id) {\r\n <div class=\"ngx-record-editor__rows\" [attr.data-testid]=\"'rows-' + arrayField.id\">\r\n <div class=\"ngx-record-editor__rows-head\">\r\n <span class=\"ngx-record-editor__summary-label\">{{ formatFieldLabel(arrayField) }}</span>\r\n @if (!sectionReadOnly) {\r\n <button\r\n type=\"button\"\r\n class=\"ngx-form__reset\"\r\n [attr.data-testid]=\"'add-row-' + arrayField.id\"\r\n (click)=\"openAddRow(arrayField)\"\r\n >+ Row</button>\r\n }\r\n </div>\r\n\r\n @for (row of rowsOf(arrayField); track $index) {\r\n <div class=\"ngx-record-editor__row\" [attr.data-testid]=\"'row-' + arrayField.id + '-' + $index\">\r\n <span class=\"ngx-record-editor__row-text\">{{ rowSummary(arrayField, row) }}</span>\r\n @if (!sectionReadOnly) {\r\n <button type=\"button\" class=\"ngx-record-editor__row-btn\"\r\n [attr.data-testid]=\"'edit-row-' + arrayField.id + '-' + $index\"\r\n (click)=\"openEditRow(arrayField, $index)\">Edit</button>\r\n <button type=\"button\" class=\"ngx-record-editor__row-btn\"\r\n [attr.data-testid]=\"'delete-row-' + arrayField.id + '-' + $index\"\r\n (click)=\"deleteRow(arrayField, $index)\">Delete</button>\r\n }\r\n </div>\r\n } @empty {\r\n <p class=\"ngx-record-editor__row-empty\">No rows yet.</p>\r\n }\r\n </div>\r\n }\r\n\r\n <!--\r\n Row drawer. Deliberately outside the tab panel: building a row's FormGroup inside the\r\n tab view races the tab's own initialisation (the reference hit this with FormArray).\r\n -->\r\n @if (inlineRowField(); as rowField) {\r\n <div class=\"ngx-record-editor__drawer\" data-testid=\"row-drawer\" [formGroup]=\"inlineRowForm!\">\r\n <div class=\"ngx-record-editor__rows-head\">\r\n <strong>{{ inlineRowIndex() === null ? 'Add' : 'Edit' }} {{ formatFieldLabel(rowField) }} row</strong>\r\n </div>\r\n\r\n <div class=\"ngx-form__panel\">\r\n @for (child of rowField.children ?? []; track child.id) {\r\n <ngx-dynamic-field\r\n [field]=\"child\"\r\n [control]=\"inlineRowForm!.get(child.id)!\"\r\n [config]=\"config\"\r\n [language]=\"language\"\r\n [readonly]=\"false\"\r\n [userRoles]=\"userRoles\"\r\n [style.gridColumn]=\"'span 12'\"\r\n />\r\n }\r\n </div>\r\n\r\n <div class=\"ngx-record-editor__section-bar\">\r\n <button type=\"button\" class=\"ngx-form__reset\" data-testid=\"cancel-row\" (click)=\"cancelRow()\">Cancel</button>\r\n <button type=\"button\" class=\"ngx-form__submit\" data-testid=\"save-row\" (click)=\"saveRow()\">Save row</button>\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- Main Dynamic Form -->\r\n <ngx-dynamic-form\r\n [config]=\"config\"\r\n [rules]=\"rules\"\r\n [initialData]=\"initialData\"\r\n [originalData]=\"originalBaseline()\"\r\n [userRoles]=\"userRoles\"\r\n [language]=\"language\"\r\n [readonly]=\"sectionReadOnly\"\r\n [readOnlyFields]=\"readOnlyFields\"\r\n [showInfoBanners]=\"false\"\r\n [loading]=\"loading\"\r\n [error]=\"error\"\r\n (formChange)=\"onFormChange($event)\"\r\n (formSubmit)=\"onFormSubmit($event)\"\r\n (formReset)=\"onFormReset()\"\r\n (activeTabChange)=\"onActiveTabChange($event)\"\r\n />\r\n</div>\r\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"] }]
4644
+ args: [{ selector: 'ngx-dynamic-record-form', standalone: true, imports: [CommonModule, ReactiveFormsModule, DynamicFormComponent, DynamicFieldComponent], 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"] }]
4419
4645
  }], propDecorators: { config: [{
4420
4646
  type: Input
4421
4647
  }], rules: [{
@@ -4457,5 +4683,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
4457
4683
  * Generated bundle index. Do not edit.
4458
4684
  */
4459
4685
 
4460
- export { COMMON_MODULES_REGISTRY, CONFIG_SOURCE, CascadeDataService, CheckboxFieldComponent, ConfigSourceService, DateFieldComponent, DropdownFieldComponent, DynamicFieldComponent, DynamicFormComponent, DynamicRecordFormComponent, ENTITY_REF_CACHE_STORE, ENTITY_REF_REGISTRY, EntityRefFieldComponent, EntityRefRegistryService, EntityRefSelectionService, EntityReferenceService, FIELD_TYPE_REGISTRY, FIELD_TYPE_SETS, FieldRegistryService, HOOK_REGISTRY, HookRegistryService, InMemoryEntityRefCacheStore, LOOKUP_REGISTRY, LookupRegistryService, MASKED_ROLES, MultiSelectFieldComponent, NumberFieldComponent, RbacService, RulesEvaluationService, SYSTEM_DEFAULT_CAN_EDIT, TextFieldComponent, TextareaFieldComponent, UPLOAD_HANDLER, VALIDATOR_REGISTRY, ValidatorRegistryService, builtInFieldTypes, provideBuiltInFieldTypes, provideFieldTypes, provideNgxDynamicEntity, refreshChoiceOptions };
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 };
4461
4687
  //# sourceMappingURL=ngx-dynamic-entity.mjs.map