ngx-dynamic-entity 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +129 -139
  2. package/esm2022/lib/field-types/array-field.component.mjs +112 -104
  3. package/esm2022/lib/field-types/boolean-field.component.mjs +62 -61
  4. package/esm2022/lib/field-types/checkbox-field.component.mjs +48 -47
  5. package/esm2022/lib/field-types/currency-field.component.mjs +54 -53
  6. package/esm2022/lib/field-types/date-field.component.mjs +46 -45
  7. package/esm2022/lib/field-types/dropdown-field.component.mjs +89 -87
  8. package/esm2022/lib/field-types/email-field.component.mjs +64 -63
  9. package/esm2022/lib/field-types/entity-ref-field.component.mjs +85 -71
  10. package/esm2022/lib/field-types/field-component.contract.mjs +2 -0
  11. package/esm2022/lib/field-types/file-field.component.mjs +88 -87
  12. package/esm2022/lib/field-types/group-field.component.mjs +50 -50
  13. package/esm2022/lib/field-types/image-field.component.mjs +92 -91
  14. package/esm2022/lib/field-types/month-year-field.component.mjs +80 -79
  15. package/esm2022/lib/field-types/multi-select-field.component.mjs +66 -65
  16. package/esm2022/lib/field-types/number-field.component.mjs +71 -75
  17. package/esm2022/lib/field-types/password-field.component.mjs +68 -67
  18. package/esm2022/lib/field-types/radio-field.component.mjs +66 -65
  19. package/esm2022/lib/field-types/text-field.component.mjs +81 -74
  20. package/esm2022/lib/field-types/textarea-field.component.mjs +48 -47
  21. package/esm2022/lib/form/dynamic-field/dynamic-field.component.mjs +53 -6
  22. package/esm2022/lib/form/dynamic-form.component.mjs +309 -20
  23. package/esm2022/lib/form/dynamic-record-form.component.mjs +36 -11
  24. package/esm2022/lib/providers/provide-field-types.mjs +1 -1
  25. package/esm2022/lib/providers/provide-ngx-dynamic-entity.mjs +8 -2
  26. package/esm2022/lib/services/cascade-data.service.mjs +2 -2
  27. package/esm2022/lib/services/entity-ref-cache.mjs +1 -1
  28. package/esm2022/lib/services/entity-ref-registry.service.mjs +2 -2
  29. package/esm2022/lib/services/entity-ref-selection.service.mjs +1 -1
  30. package/esm2022/lib/services/entity-reference.service.mjs +1 -1
  31. package/esm2022/lib/services/field-registry.service.mjs +1 -1
  32. package/esm2022/lib/services/file-upload.service.mjs +1 -1
  33. package/esm2022/lib/services/lookup-registry.service.mjs +2 -2
  34. package/esm2022/lib/services/rbac.service.mjs +1 -1
  35. package/esm2022/lib/services/validation-messages.service.mjs +56 -0
  36. package/esm2022/lib/services/validator-registry.service.mjs +35 -2
  37. package/esm2022/lib/tokens/injection-tokens.mjs +35 -6
  38. package/esm2022/public-api.mjs +23 -2
  39. package/fesm2022/ngx-dynamic-entity.mjs +1754 -1236
  40. package/fesm2022/ngx-dynamic-entity.mjs.map +1 -1
  41. package/lib/field-types/array-field.component.d.ts +7 -1
  42. package/lib/field-types/dropdown-field.component.d.ts +2 -1
  43. package/lib/field-types/entity-ref-field.component.d.ts +12 -2
  44. package/lib/field-types/field-component.contract.d.ts +49 -0
  45. package/lib/field-types/group-field.component.d.ts +1 -1
  46. package/lib/field-types/multi-select-field.component.d.ts +1 -1
  47. package/lib/field-types/number-field.component.d.ts +1 -0
  48. package/lib/field-types/radio-field.component.d.ts +1 -1
  49. package/lib/field-types/text-field.component.d.ts +10 -2
  50. package/lib/form/dynamic-field/dynamic-field.component.d.ts +39 -4
  51. package/lib/form/dynamic-form.component.d.ts +155 -8
  52. package/lib/form/dynamic-record-form.component.d.ts +13 -2
  53. package/lib/providers/provide-ngx-dynamic-entity.d.ts +25 -3
  54. package/lib/services/cascade-data.service.d.ts +1 -1
  55. package/lib/services/entity-ref-registry.service.d.ts +1 -1
  56. package/lib/services/lookup-registry.service.d.ts +1 -1
  57. package/lib/services/validation-messages.service.d.ts +43 -0
  58. package/lib/services/validator-registry.service.d.ts +13 -1
  59. package/lib/tokens/injection-tokens.d.ts +42 -7
  60. package/package.json +17 -18
  61. package/public-api.d.ts +12 -0
  62. package/styles.css +579 -0
@@ -1,8 +1,8 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, makeEnvironmentProviders, Input, Component, signal, inject, Injectable, DestroyRef, isDevMode, ViewContainerRef, ViewChild, forwardRef, Optional, Inject, EventEmitter, computed, HostListener, Output } from '@angular/core';
2
+ import { InjectionToken, makeEnvironmentProviders, inject, Injectable, signal, Input, ChangeDetectionStrategy, Component, DestroyRef, Injector, isDevMode, ViewContainerRef, ViewChild, forwardRef, ChangeDetectorRef, Optional, Inject, EventEmitter, computed, HostListener, ViewChildren, Output } from '@angular/core';
3
3
  import * as i1 from '@angular/forms';
4
4
  import { ReactiveFormsModule, Validators, FormBuilder, FormArray, FormGroup } from '@angular/forms';
5
- 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';
@@ -29,7 +29,7 @@ class InMemoryEntityRefCacheStore {
29
29
  }
30
30
  }
31
31
 
32
- /** Roles that see XXXXXXXXX for masked fields (ADR-003) */
32
+ /** Roles that see XXXXXXXXX for masked fields. Presentational only — enforce authz server-side. */
33
33
  const MASKED_ROLES = new InjectionToken('MASKED_ROLES');
34
34
  /**
35
35
  * Consumer **overrides**: fieldType string → Angular component class.
@@ -70,8 +70,41 @@ 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');
89
+ /**
90
+ * Overrides for the messages shown under an invalid field, keyed by Angular error key
91
+ * (`required`, `minlength`, `pattern`, …) plus a few field-specific keys.
92
+ *
93
+ * The built-in messages are English literals. Registering here is what makes a non-English
94
+ * form possible without re-implementing every field component; unlisted keys keep their
95
+ * default, so overriding one does not mean supplying them all.
96
+ */
97
+ const VALIDATION_MESSAGES = new InjectionToken('VALIDATION_MESSAGES');
73
98
  /** Registry: validator key → ValidatorFn */
74
99
  const VALIDATOR_REGISTRY = new InjectionToken('VALIDATOR_REGISTRY');
100
+ /**
101
+ * Registry: validator key → AsyncValidatorFn.
102
+ *
103
+ * Kept separate from VALIDATOR_REGISTRY because Angular applies the two differently: an async
104
+ * validator runs only once the synchronous ones pass, and puts the control into `pending`
105
+ * while it does. Name them from a schema with `validators: { customAsync: ['uniqueEmail'] }`.
106
+ */
107
+ const ASYNC_VALIDATOR_REGISTRY = new InjectionToken('ASYNC_VALIDATOR_REGISTRY');
75
108
  /** Registry: hook key → async (data, context) => data */
76
109
  const HOOK_REGISTRY = new InjectionToken('HOOK_REGISTRY');
77
110
  /**
@@ -109,10 +142,6 @@ const UPLOAD_HANDLER = new InjectionToken('UPLOAD_HANDLER');
109
142
  * { provide: ENTITY_REF_CACHE_STORE, useClass: SessionStorageRefCache }
110
143
  */
111
144
  const ENTITY_REF_CACHE_STORE = new InjectionToken('ENTITY_REF_CACHE_STORE', { providedIn: 'root', factory: () => new InMemoryEntityRefCacheStore() });
112
- /**
113
- * Consumer-provided predicate function to check whether user roles can edit system default tabs or fields.
114
- * Defaults to allowing edits if not provided.
115
- */
116
145
  /**
117
146
  * Consumer-provided predicate function to check whether user roles can edit system default tabs or fields.
118
147
  * Defaults to allowing edits if not provided.
@@ -140,15 +169,90 @@ const provideNgxDynamicEntity = (config = {}) => makeEnvironmentProviders([
140
169
  { provide: ENTITY_REF_REGISTRY, useValue: new Map(Object.entries(config.entityRefs ?? {})) },
141
170
  { provide: LOOKUP_REGISTRY, useValue: new Map(Object.entries(config.lookups ?? {})) },
142
171
  { provide: VALIDATOR_REGISTRY, useValue: new Map(Object.entries(config.validators ?? {})) },
172
+ {
173
+ provide: ASYNC_VALIDATOR_REGISTRY,
174
+ useValue: new Map(Object.entries(config.asyncValidators ?? {})),
175
+ },
143
176
  { provide: HOOK_REGISTRY, useValue: new Map(Object.entries(config.hooks ?? {})) },
177
+ { provide: RECORD_MIGRATIONS, useValue: config.migrations ?? [] },
178
+ { provide: VALIDATION_MESSAGES, useValue: config.validationMessages ?? {} },
144
179
  ]);
145
180
 
181
+ /** The built-in English messages. Every key here can be overridden individually. */
182
+ const DEFAULT_VALIDATION_MESSAGES = {
183
+ required: 'This field is required.',
184
+ requiredSelection: 'Please select an option.',
185
+ email: 'Please enter a valid email address.',
186
+ pattern: 'Invalid format.',
187
+ minlength: (_lang, err) => `Minimum ${err?.requiredLength} characters required.`,
188
+ maxlength: (_lang, err) => `Maximum ${err?.requiredLength} characters allowed.`,
189
+ min: (_lang, err) => `Value must be at least ${err?.min}.`,
190
+ max: (_lang, err) => `Value must not exceed ${err?.max}.`,
191
+ invalid: 'Invalid value.',
192
+ invalidNumber: 'Invalid number.',
193
+ invalidSelection: 'Invalid selection.',
194
+ };
195
+ class ValidationMessagesService {
196
+ overrides = inject(VALIDATION_MESSAGES, { optional: true }) ?? {};
197
+ /**
198
+ * The message for the first error present, in the order the caller lists.
199
+ *
200
+ * Order is the caller's because it is field-specific: a dropdown wants
201
+ * `requiredSelection` where a text input wants `required`, and the first match wins so the
202
+ * most specific message is the one shown.
203
+ */
204
+ resolve(errors, language, order, fallbackKey = 'invalid') {
205
+ if (!errors)
206
+ return '';
207
+ for (const entry of order) {
208
+ // A tuple maps an Angular error key to a different message key: a dropdown raises the
209
+ // standard `required` error but reads better as "Please select an option", and both
210
+ // messages stay independently overridable.
211
+ const [errorKey, messageKey] = typeof entry === 'string' ? [entry, entry] : entry;
212
+ // `errors[errorKey]` is usually an object of detail, but `required` is simply `true`.
213
+ if (errors[errorKey] === undefined)
214
+ continue;
215
+ return this.messageFor(messageKey, language, errors[errorKey]);
216
+ }
217
+ return this.messageFor(fallbackKey, language, null);
218
+ }
219
+ /** One message by key, honouring an override before the default. */
220
+ messageFor(key, language, error) {
221
+ const message = this.overrides[key] ?? DEFAULT_VALIDATION_MESSAGES[key];
222
+ if (message === undefined)
223
+ return '';
224
+ return typeof message === 'function' ? message(language, error) : message;
225
+ }
226
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ValidationMessagesService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
227
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ValidationMessagesService, providedIn: 'root' });
228
+ }
229
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ValidationMessagesService, decorators: [{
230
+ type: Injectable,
231
+ args: [{ providedIn: 'root' }]
232
+ }] });
233
+
146
234
  class TextFieldComponent {
235
+ messages = inject(ValidationMessagesService);
236
+ displaySub;
147
237
  field;
148
- control;
238
+ set control(value) {
239
+ this._control = value;
240
+ this.displaySub?.unsubscribe();
241
+ this.displayValue.set(value?.value ?? null);
242
+ this.displaySub = value?.valueChanges.subscribe(v => this.displayValue.set(v));
243
+ }
244
+ get control() {
245
+ return this._control;
246
+ }
247
+ _control;
149
248
  language = 'en';
150
249
  readonly = false;
151
250
  masked = false;
251
+ /** Readonly markup reads this signal so an external patch (autoPatch) is visible under OnPush. */
252
+ displayValue = signal(null);
253
+ ngOnDestroy() {
254
+ this.displaySub?.unsubscribe();
255
+ }
152
256
  get label() {
153
257
  return resolveLabel(this.field?.label, this.language);
154
258
  }
@@ -158,86 +262,76 @@ class TextFieldComponent {
158
262
  get errorMessage() {
159
263
  if (!this.control || !this.control.errors || !this.control.touched)
160
264
  return '';
161
- const errs = this.control.errors;
162
- if (errs['required'])
163
- return 'This field is required.';
164
- if (errs['email'])
165
- return 'Please enter a valid email address.';
166
- if (errs['minlength'])
167
- return `Minimum ${errs['minlength'].requiredLength} characters required.`;
168
- if (errs['maxlength'])
169
- return `Maximum ${errs['maxlength'].requiredLength} characters allowed.`;
170
- if (errs['pattern'])
171
- return 'Invalid format.';
172
- return 'Invalid value.';
265
+ return this.messages.resolve(this.control.errors, this.language, ['required', 'email', 'minlength', 'maxlength', 'pattern']);
173
266
  }
174
267
  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>
204
- `, 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"] }] });
268
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: TextFieldComponent, isStandalone: true, selector: "ngx-text-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
269
+ <div
270
+ class="ngx-field ngx-field--text"
271
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type"
272
+ [class.ngx-field--readonly]="readonly"
273
+ [class.ngx-field--masked]="masked"
274
+ [class.ngx-field--invalid]="control && control.invalid && control.touched"
275
+ >
276
+ <label class="ngx-field__label" [attr.for]="field.id">{{ label }}@if (field.validators?.required) { <span class="ngx-field__req">*</span> }</label>
277
+ @if (masked) {
278
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
279
+ } @else if (readonly) {
280
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ displayValue() ?? '—' }}</span>
281
+ } @else {
282
+ <input
283
+ [id]="field.id"
284
+ type="text"
285
+ class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
286
+ [formControl]="$any(control)"
287
+ [placeholder]="placeholder"
288
+ [attr.disabled]="field.disabled ? true : null"
289
+ [attr.aria-invalid]="control.invalid && control.touched"
290
+ [attr.aria-describedby]="errorMessage ? field.id + '-error' : null"
291
+ />
292
+ @if (errorMessage) {
293
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'" [id]="field.id + '-error'" role="alert">{{ errorMessage }}</span>
294
+ }
295
+ }
296
+ </div>
297
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
205
298
  }
206
299
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: TextFieldComponent, decorators: [{
207
300
  type: Component,
208
301
  args: [{
302
+ changeDetection: ChangeDetectionStrategy.OnPush,
209
303
  selector: 'ngx-text-field',
210
304
  standalone: true,
211
305
  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>
306
+ template: `
307
+ <div
308
+ class="ngx-field ngx-field--text"
309
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type"
310
+ [class.ngx-field--readonly]="readonly"
311
+ [class.ngx-field--masked]="masked"
312
+ [class.ngx-field--invalid]="control && control.invalid && control.touched"
313
+ >
314
+ <label class="ngx-field__label" [attr.for]="field.id">{{ label }}@if (field.validators?.required) { <span class="ngx-field__req">*</span> }</label>
315
+ @if (masked) {
316
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
317
+ } @else if (readonly) {
318
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ displayValue() ?? '—' }}</span>
319
+ } @else {
320
+ <input
321
+ [id]="field.id"
322
+ type="text"
323
+ class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
324
+ [formControl]="$any(control)"
325
+ [placeholder]="placeholder"
326
+ [attr.disabled]="field.disabled ? true : null"
327
+ [attr.aria-invalid]="control.invalid && control.touched"
328
+ [attr.aria-describedby]="errorMessage ? field.id + '-error' : null"
329
+ />
330
+ @if (errorMessage) {
331
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'" [id]="field.id + '-error'" role="alert">{{ errorMessage }}</span>
332
+ }
333
+ }
334
+ </div>
241
335
  `,
242
336
  }]
243
337
  }], propDecorators: { field: [{
@@ -265,58 +359,59 @@ class TextareaFieldComponent {
265
359
  return resolveLabel(this.field?.placeholder, this.language);
266
360
  }
267
361
  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>
290
- `, 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"] }] });
362
+ 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: `
363
+ <div class="ngx-field ngx-field--textarea"
364
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
365
+ <label class="ngx-field__label" [attr.for]="field.id">{{ label }}</label>
366
+ @if (masked) {
367
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
368
+ } @else if (readonly) {
369
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ control.value }}</span>
370
+ } @else {
371
+ <textarea
372
+ [id]="field.id"
373
+ class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
374
+ [formControl]="$any(control)"
375
+ [placeholder]="placeholder"
376
+ [attr.disabled]="field.disabled ? true : null"
377
+ rows="3"
378
+ ></textarea>
379
+ @if (control.invalid && control.touched) {
380
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
381
+ }
382
+ }
383
+ </div>
384
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
291
385
  }
292
386
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: TextareaFieldComponent, decorators: [{
293
387
  type: Component,
294
388
  args: [{
389
+ changeDetection: ChangeDetectionStrategy.OnPush,
295
390
  selector: 'ngx-textarea-field',
296
391
  standalone: true,
297
392
  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>
393
+ template: `
394
+ <div class="ngx-field ngx-field--textarea"
395
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
396
+ <label class="ngx-field__label" [attr.for]="field.id">{{ label }}</label>
397
+ @if (masked) {
398
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
399
+ } @else if (readonly) {
400
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ control.value }}</span>
401
+ } @else {
402
+ <textarea
403
+ [id]="field.id"
404
+ class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
405
+ [formControl]="$any(control)"
406
+ [placeholder]="placeholder"
407
+ [attr.disabled]="field.disabled ? true : null"
408
+ rows="3"
409
+ ></textarea>
410
+ @if (control.invalid && control.touched) {
411
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
412
+ }
413
+ }
414
+ </div>
320
415
  `,
321
416
  }]
322
417
  }], propDecorators: { field: [{
@@ -332,6 +427,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
332
427
  }] } });
333
428
 
334
429
  class NumberFieldComponent {
430
+ messages = inject(ValidationMessagesService);
335
431
  field;
336
432
  control;
337
433
  language = 'en';
@@ -346,88 +442,82 @@ class NumberFieldComponent {
346
442
  get errorMessage() {
347
443
  if (!this.control || !this.control.errors || !this.control.touched)
348
444
  return '';
349
- const errs = this.control.errors;
350
- if (errs['required'])
351
- return 'This field is required.';
352
- if (errs['min'])
353
- return `Value must be at least ${errs['min'].min}.`;
354
- if (errs['max'])
355
- return `Value must not exceed ${errs['max'].max}.`;
356
- return 'Invalid number.';
445
+ return this.messages.resolve(this.control.errors, this.language, ['required', 'min', 'max'], 'invalidNumber');
357
446
  }
358
447
  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>
391
- `, 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"] }] });
448
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: NumberFieldComponent, isStandalone: true, selector: "ngx-number-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
449
+ <div
450
+ class="ngx-field ngx-field--number"
451
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type"
452
+ [class.ngx-field--readonly]="readonly"
453
+ [class.ngx-field--masked]="masked"
454
+ [class.ngx-field--invalid]="control && control.invalid && control.touched"
455
+ >
456
+ <label class="ngx-field__label" [attr.for]="field.id">
457
+ {{ label }}
458
+ @if (field.validators?.required) { <span class="ngx-field__req">*</span> }
459
+ </label>
460
+ @if (masked) {
461
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
462
+ } @else if (readonly) {
463
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ control.value ?? '—' }}</span>
464
+ } @else {
465
+ <input
466
+ [id]="field.id"
467
+ type="number"
468
+ class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
469
+ [formControl]="$any(control)"
470
+ [placeholder]="placeholder"
471
+ [attr.disabled]="field.disabled ? true : null"
472
+ [attr.aria-invalid]="control.invalid && control.touched"
473
+ [attr.aria-describedby]="errorMessage ? field.id + '-error' : null"
474
+ />
475
+ @if (errorMessage) {
476
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'" [id]="field.id + '-error'" role="alert">{{ errorMessage }}</span>
477
+ }
478
+ }
479
+ </div>
480
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
392
481
  }
393
482
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: NumberFieldComponent, decorators: [{
394
483
  type: Component,
395
484
  args: [{
485
+ changeDetection: ChangeDetectionStrategy.OnPush,
396
486
  selector: 'ngx-number-field',
397
487
  standalone: true,
398
488
  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>
489
+ template: `
490
+ <div
491
+ class="ngx-field ngx-field--number"
492
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type"
493
+ [class.ngx-field--readonly]="readonly"
494
+ [class.ngx-field--masked]="masked"
495
+ [class.ngx-field--invalid]="control && control.invalid && control.touched"
496
+ >
497
+ <label class="ngx-field__label" [attr.for]="field.id">
498
+ {{ label }}
499
+ @if (field.validators?.required) { <span class="ngx-field__req">*</span> }
500
+ </label>
501
+ @if (masked) {
502
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
503
+ } @else if (readonly) {
504
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ control.value ?? '—' }}</span>
505
+ } @else {
506
+ <input
507
+ [id]="field.id"
508
+ type="number"
509
+ class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
510
+ [formControl]="$any(control)"
511
+ [placeholder]="placeholder"
512
+ [attr.disabled]="field.disabled ? true : null"
513
+ [attr.aria-invalid]="control.invalid && control.touched"
514
+ [attr.aria-describedby]="errorMessage ? field.id + '-error' : null"
515
+ />
516
+ @if (errorMessage) {
517
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'" [id]="field.id + '-error'" role="alert">{{ errorMessage }}</span>
518
+ }
519
+ }
520
+ </div>
431
521
  `,
432
522
  }]
433
523
  }], propDecorators: { field: [{
@@ -466,64 +556,65 @@ class CurrencyFieldComponent {
466
556
  }
467
557
  }
468
558
  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>
494
- `, 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"] }] });
559
+ 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: `
560
+ <div class="ngx-field ngx-field--currency"
561
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
562
+ <label class="ngx-field__label">{{ label }}</label>
563
+ @if (masked) {
564
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
565
+ } @else if (readonly) {
566
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ symbol }}{{ control.value }}</span>
567
+ } @else {
568
+ <div class="ngx-field__currency-wrap">
569
+ <span class="ngx-field__currency-symbol">{{ symbol }}</span>
570
+ <input
571
+ class="ngx-field__input ngx-field__input--currency" [attr.data-testid]="'field-' + field.id + '-input'"
572
+ type="number"
573
+ step="0.01"
574
+ [formControl]="$any(control)"
575
+ [placeholder]="placeholder || '0.00'"
576
+ [attr.disabled]="field.disabled ? true : null"
577
+ />
578
+ </div>
579
+ @if (control.invalid && control.touched) {
580
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
581
+ }
582
+ }
583
+ </div>
584
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
495
585
  }
496
586
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: CurrencyFieldComponent, decorators: [{
497
587
  type: Component,
498
588
  args: [{
589
+ changeDetection: ChangeDetectionStrategy.OnPush,
499
590
  selector: 'ngx-currency-field',
500
591
  standalone: true,
501
592
  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>
593
+ template: `
594
+ <div class="ngx-field ngx-field--currency"
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'">{{ symbol }}{{ control.value }}</span>
601
+ } @else {
602
+ <div class="ngx-field__currency-wrap">
603
+ <span class="ngx-field__currency-symbol">{{ symbol }}</span>
604
+ <input
605
+ class="ngx-field__input ngx-field__input--currency" [attr.data-testid]="'field-' + field.id + '-input'"
606
+ type="number"
607
+ step="0.01"
608
+ [formControl]="$any(control)"
609
+ [placeholder]="placeholder || '0.00'"
610
+ [attr.disabled]="field.disabled ? true : null"
611
+ />
612
+ </div>
613
+ @if (control.invalid && control.touched) {
614
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
615
+ }
616
+ }
617
+ </div>
527
618
  `,
528
619
  }]
529
620
  }], propDecorators: { field: [{
@@ -552,74 +643,75 @@ class EmailFieldComponent {
552
643
  return resolveLabel(this.field?.placeholder, this.language);
553
644
  }
554
645
  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>
585
- `, 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"] }] });
646
+ 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: `
647
+ <div class="ngx-field ngx-field--email"
648
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
649
+ <label class="ngx-field__label">{{ label }}</label>
650
+ @if (masked) {
651
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
652
+ } @else if (readonly) {
653
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">
654
+ <a [href]="'mailto:' + control.value" class="ngx-field__email-link">{{ control.value || '—' }}</a>
655
+ </span>
656
+ } @else {
657
+ <input
658
+ class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
659
+ type="email"
660
+ autocomplete="email"
661
+ [formControl]="$any(control)"
662
+ [placeholder]="placeholder || 'you@example.com'"
663
+ [attr.disabled]="field.disabled ? true : null"
664
+ />
665
+ @if (control.invalid && control.touched) {
666
+ @if (control.errors?.['required']) {
667
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">Email is required</span>
668
+ } @else if (control.errors?.['email']) {
669
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">Enter a valid email address</span>
670
+ } @else {
671
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
672
+ }
673
+ }
674
+ }
675
+ </div>
676
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
586
677
  }
587
678
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: EmailFieldComponent, decorators: [{
588
679
  type: Component,
589
680
  args: [{
681
+ changeDetection: ChangeDetectionStrategy.OnPush,
590
682
  selector: 'ngx-email-field',
591
683
  standalone: true,
592
684
  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>
685
+ template: `
686
+ <div class="ngx-field ngx-field--email"
687
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
688
+ <label class="ngx-field__label">{{ label }}</label>
689
+ @if (masked) {
690
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
691
+ } @else if (readonly) {
692
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">
693
+ <a [href]="'mailto:' + control.value" class="ngx-field__email-link">{{ control.value || '—' }}</a>
694
+ </span>
695
+ } @else {
696
+ <input
697
+ class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
698
+ type="email"
699
+ autocomplete="email"
700
+ [formControl]="$any(control)"
701
+ [placeholder]="placeholder || 'you@example.com'"
702
+ [attr.disabled]="field.disabled ? true : null"
703
+ />
704
+ @if (control.invalid && control.touched) {
705
+ @if (control.errors?.['required']) {
706
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">Email is required</span>
707
+ } @else if (control.errors?.['email']) {
708
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">Enter a valid email address</span>
709
+ } @else {
710
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
711
+ }
712
+ }
713
+ }
714
+ </div>
623
715
  `,
624
716
  }]
625
717
  }], propDecorators: { field: [{
@@ -652,78 +744,79 @@ class PasswordFieldComponent {
652
744
  this.visible.update(v => !v);
653
745
  }
654
746
  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>
687
- `, 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"] }] });
747
+ 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: `
748
+ <div class="ngx-field ngx-field--password"
749
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
750
+ <label class="ngx-field__label">{{ label }}</label>
751
+ @if (masked) {
752
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
753
+ } @else if (readonly) {
754
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">••••••••</span>
755
+ } @else {
756
+ <div class="ngx-field__password-wrap">
757
+ <input
758
+ class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
759
+ [type]="visible() ? 'text' : 'password'"
760
+ autocomplete="current-password"
761
+ [formControl]="$any(control)"
762
+ [placeholder]="placeholder || '••••••••'"
763
+ [attr.disabled]="field.disabled ? true : null"
764
+ />
765
+ <button
766
+ type="button"
767
+ class="ngx-field__eye-btn"
768
+ (click)="toggleVisibility()"
769
+ [attr.aria-label]="visible() ? 'Hide password' : 'Show password'"
770
+ >
771
+ {{ visible() ? '🙈' : '👁' }}
772
+ </button>
773
+ </div>
774
+ @if (control.invalid && control.touched) {
775
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
776
+ }
777
+ }
778
+ </div>
779
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
688
780
  }
689
781
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: PasswordFieldComponent, decorators: [{
690
782
  type: Component,
691
783
  args: [{
784
+ changeDetection: ChangeDetectionStrategy.OnPush,
692
785
  selector: 'ngx-password-field',
693
786
  standalone: true,
694
787
  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>
788
+ template: `
789
+ <div class="ngx-field ngx-field--password"
790
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
791
+ <label class="ngx-field__label">{{ label }}</label>
792
+ @if (masked) {
793
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
794
+ } @else if (readonly) {
795
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">••••••••</span>
796
+ } @else {
797
+ <div class="ngx-field__password-wrap">
798
+ <input
799
+ class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
800
+ [type]="visible() ? 'text' : 'password'"
801
+ autocomplete="current-password"
802
+ [formControl]="$any(control)"
803
+ [placeholder]="placeholder || '••••••••'"
804
+ [attr.disabled]="field.disabled ? true : null"
805
+ />
806
+ <button
807
+ type="button"
808
+ class="ngx-field__eye-btn"
809
+ (click)="toggleVisibility()"
810
+ [attr.aria-label]="visible() ? 'Hide password' : 'Show password'"
811
+ >
812
+ {{ visible() ? '🙈' : '👁' }}
813
+ </button>
814
+ </div>
815
+ @if (control.invalid && control.touched) {
816
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
817
+ }
818
+ }
819
+ </div>
727
820
  `,
728
821
  }]
729
822
  }], propDecorators: { field: [{
@@ -748,58 +841,59 @@ class CheckboxFieldComponent {
748
841
  return resolveLabel(this.field?.label, this.language);
749
842
  }
750
843
  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>
773
- `, 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"] }] });
844
+ 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: `
845
+ <div class="ngx-field ngx-field--checkbox"
846
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
847
+ @if (masked) {
848
+ <label class="ngx-field__label">{{ label }}</label>
849
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
850
+ } @else if (readonly) {
851
+ <label class="ngx-field__label">{{ label }}</label>
852
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ control.value ? 'Yes' : 'No' }}</span>
853
+ } @else {
854
+ <label class="ngx-field__label" [attr.for]="field.id">
855
+ <input
856
+ [id]="field.id"
857
+ class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
858
+ type="checkbox"
859
+ [formControl]="$any(control)"
860
+ [attr.disabled]="field.disabled ? true : null"
861
+ />
862
+ {{ label }}
863
+ </label>
864
+ }
865
+ </div>
866
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
774
867
  }
775
868
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: CheckboxFieldComponent, decorators: [{
776
869
  type: Component,
777
870
  args: [{
871
+ changeDetection: ChangeDetectionStrategy.OnPush,
778
872
  selector: 'ngx-checkbox-field',
779
873
  standalone: true,
780
874
  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>
875
+ template: `
876
+ <div class="ngx-field ngx-field--checkbox"
877
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
878
+ @if (masked) {
879
+ <label class="ngx-field__label">{{ label }}</label>
880
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
881
+ } @else if (readonly) {
882
+ <label class="ngx-field__label">{{ label }}</label>
883
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ control.value ? 'Yes' : 'No' }}</span>
884
+ } @else {
885
+ <label class="ngx-field__label" [attr.for]="field.id">
886
+ <input
887
+ [id]="field.id"
888
+ class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
889
+ type="checkbox"
890
+ [formControl]="$any(control)"
891
+ [attr.disabled]="field.disabled ? true : null"
892
+ />
893
+ {{ label }}
894
+ </label>
895
+ }
896
+ </div>
803
897
  `,
804
898
  }]
805
899
  }], propDecorators: { field: [{
@@ -829,72 +923,73 @@ class BooleanFieldComponent {
829
923
  return resolveLabel(this.field?.label, this.language);
830
924
  }
831
925
  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>
861
- `, 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"] }] });
926
+ 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: `
927
+ <div class="ngx-field ngx-field--boolean"
928
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
929
+ @if (masked) {
930
+ <label class="ngx-field__label">{{ label }}</label>
931
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
932
+ } @else if (readonly) {
933
+ <label class="ngx-field__label">{{ label }}</label>
934
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ control.value ? 'Yes' : 'No' }}</span>
935
+ } @else {
936
+ <div class="ngx-field__toggle-wrap">
937
+ <label class="ngx-field__toggle-label">{{ label }}</label>
938
+ <label class="ngx-field__toggle-switch" [class.ngx-field__toggle-switch--checked]="control.value">
939
+ <input
940
+ class="ngx-field__toggle-input"
941
+ type="checkbox"
942
+ role="switch"
943
+ [formControl]="$any(control)"
944
+ [attr.disabled]="field.disabled ? true : null"
945
+ [attr.aria-checked]="control.value"
946
+ />
947
+ <span class="ngx-field__toggle-track">
948
+ <span class="ngx-field__toggle-thumb"></span>
949
+ </span>
950
+ <span class="ngx-field__toggle-text">{{ control.value ? 'Yes' : 'No' }}</span>
951
+ </label>
952
+ </div>
953
+ }
954
+ </div>
955
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
862
956
  }
863
957
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: BooleanFieldComponent, decorators: [{
864
958
  type: Component,
865
959
  args: [{
960
+ changeDetection: ChangeDetectionStrategy.OnPush,
866
961
  selector: 'ngx-boolean-field',
867
962
  standalone: true,
868
963
  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>
964
+ template: `
965
+ <div class="ngx-field ngx-field--boolean"
966
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
967
+ @if (masked) {
968
+ <label class="ngx-field__label">{{ label }}</label>
969
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
970
+ } @else if (readonly) {
971
+ <label class="ngx-field__label">{{ label }}</label>
972
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ control.value ? 'Yes' : 'No' }}</span>
973
+ } @else {
974
+ <div class="ngx-field__toggle-wrap">
975
+ <label class="ngx-field__toggle-label">{{ label }}</label>
976
+ <label class="ngx-field__toggle-switch" [class.ngx-field__toggle-switch--checked]="control.value">
977
+ <input
978
+ class="ngx-field__toggle-input"
979
+ type="checkbox"
980
+ role="switch"
981
+ [formControl]="$any(control)"
982
+ [attr.disabled]="field.disabled ? true : null"
983
+ [attr.aria-checked]="control.value"
984
+ />
985
+ <span class="ngx-field__toggle-track">
986
+ <span class="ngx-field__toggle-thumb"></span>
987
+ </span>
988
+ <span class="ngx-field__toggle-text">{{ control.value ? 'Yes' : 'No' }}</span>
989
+ </label>
990
+ </div>
991
+ }
992
+ </div>
898
993
  `,
899
994
  }]
900
995
  }], propDecorators: { field: [{
@@ -930,56 +1025,57 @@ class DateFieldComponent {
930
1025
  }
931
1026
  }
932
1027
  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>
954
- `, 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"] }] });
1028
+ 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: `
1029
+ <div class="ngx-field ngx-field--date"
1030
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
1031
+ <label class="ngx-field__label" [attr.for]="field.id">{{ label }}</label>
1032
+ @if (masked) {
1033
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
1034
+ } @else if (readonly) {
1035
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ formatDate(control.value) }}</span>
1036
+ } @else {
1037
+ <input
1038
+ [id]="field.id"
1039
+ class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
1040
+ type="date"
1041
+ [formControl]="$any(control)"
1042
+ [attr.disabled]="field.disabled ? true : null"
1043
+ />
1044
+ @if (control.invalid && control.touched) {
1045
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
1046
+ }
1047
+ }
1048
+ </div>
1049
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
955
1050
  }
956
1051
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: DateFieldComponent, decorators: [{
957
1052
  type: Component,
958
1053
  args: [{
1054
+ changeDetection: ChangeDetectionStrategy.OnPush,
959
1055
  selector: 'ngx-date-field',
960
1056
  standalone: true,
961
1057
  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>
1058
+ template: `
1059
+ <div class="ngx-field ngx-field--date"
1060
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
1061
+ <label class="ngx-field__label" [attr.for]="field.id">{{ label }}</label>
1062
+ @if (masked) {
1063
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
1064
+ } @else if (readonly) {
1065
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ formatDate(control.value) }}</span>
1066
+ } @else {
1067
+ <input
1068
+ [id]="field.id"
1069
+ class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
1070
+ type="date"
1071
+ [formControl]="$any(control)"
1072
+ [attr.disabled]="field.disabled ? true : null"
1073
+ />
1074
+ @if (control.invalid && control.touched) {
1075
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
1076
+ }
1077
+ }
1078
+ </div>
983
1079
  `,
984
1080
  }]
985
1081
  }], propDecorators: { field: [{
@@ -1042,90 +1138,91 @@ class MonthYearFieldComponent {
1042
1138
  return monthName && year ? `${monthName} ${year}` : String(value);
1043
1139
  }
1044
1140
  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>
1083
- `, 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"] }] });
1141
+ 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: `
1142
+ <div class="ngx-field ngx-field--month-year"
1143
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
1144
+ <label class="ngx-field__label">{{ label }}</label>
1145
+ @if (masked) {
1146
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
1147
+ } @else if (readonly) {
1148
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ formatValue(control.value) }}</span>
1149
+ } @else {
1150
+ <div class="ngx-field__month-year-wrap">
1151
+ <select
1152
+ class="ngx-field__input ngx-field__input--month" [attr.data-testid]="'field-' + field.id + '-month'"
1153
+ [value]="selectedMonth"
1154
+ (change)="onMonthChange($any($event.target).value)"
1155
+ [attr.disabled]="field.disabled ? true : null"
1156
+ >
1157
+ <option value="">Month</option>
1158
+ @for (m of months; track m.value) {
1159
+ <option [value]="m.value">{{ m.label }}</option>
1160
+ }
1161
+ </select>
1162
+ <select
1163
+ class="ngx-field__input ngx-field__input--year" [attr.data-testid]="'field-' + field.id + '-year'"
1164
+ [value]="selectedYear"
1165
+ (change)="onYearChange($any($event.target).value)"
1166
+ [attr.disabled]="field.disabled ? true : null"
1167
+ >
1168
+ <option value="">Year</option>
1169
+ @for (y of years; track y) {
1170
+ <option [value]="y">{{ y }}</option>
1171
+ }
1172
+ </select>
1173
+ </div>
1174
+ @if (control.invalid && control.touched) {
1175
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
1176
+ }
1177
+ }
1178
+ </div>
1179
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1084
1180
  }
1085
1181
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: MonthYearFieldComponent, decorators: [{
1086
1182
  type: Component,
1087
1183
  args: [{
1184
+ changeDetection: ChangeDetectionStrategy.OnPush,
1088
1185
  selector: 'ngx-month-year-field',
1089
1186
  standalone: true,
1090
1187
  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>
1188
+ template: `
1189
+ <div class="ngx-field ngx-field--month-year"
1190
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
1191
+ <label class="ngx-field__label">{{ label }}</label>
1192
+ @if (masked) {
1193
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
1194
+ } @else if (readonly) {
1195
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ formatValue(control.value) }}</span>
1196
+ } @else {
1197
+ <div class="ngx-field__month-year-wrap">
1198
+ <select
1199
+ class="ngx-field__input ngx-field__input--month" [attr.data-testid]="'field-' + field.id + '-month'"
1200
+ [value]="selectedMonth"
1201
+ (change)="onMonthChange($any($event.target).value)"
1202
+ [attr.disabled]="field.disabled ? true : null"
1203
+ >
1204
+ <option value="">Month</option>
1205
+ @for (m of months; track m.value) {
1206
+ <option [value]="m.value">{{ m.label }}</option>
1207
+ }
1208
+ </select>
1209
+ <select
1210
+ class="ngx-field__input ngx-field__input--year" [attr.data-testid]="'field-' + field.id + '-year'"
1211
+ [value]="selectedYear"
1212
+ (change)="onYearChange($any($event.target).value)"
1213
+ [attr.disabled]="field.disabled ? true : null"
1214
+ >
1215
+ <option value="">Year</option>
1216
+ @for (y of years; track y) {
1217
+ <option [value]="y">{{ y }}</option>
1218
+ }
1219
+ </select>
1220
+ </div>
1221
+ @if (control.invalid && control.touched) {
1222
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
1223
+ }
1224
+ }
1225
+ </div>
1129
1226
  `,
1130
1227
  }]
1131
1228
  }], propDecorators: { field: [{
@@ -1355,7 +1452,7 @@ const hostLastConfig = new WeakMap();
1355
1452
  * Point a choice component's `options` signal at the right source (parity plan §6.3).
1356
1453
  *
1357
1454
  * Called from the `field` / `language` input setters of `dropdown`, `radio` and `multiSelect` —
1358
- * the wiring is theirs, the decision is the service's, which is what keeps the ADR-008
1455
+ * the wiring is theirs, the decision is the service's, which is what keeps the field
1359
1456
  * five-input contract intact: options are never pushed in as a sixth input.
1360
1457
  *
1361
1458
  * Setters rather than `ngOnChanges` on purpose. `ngOnChanges` only fires for inputs Angular
@@ -1384,6 +1481,7 @@ function refreshChoiceOptions(host, lookups) {
1384
1481
 
1385
1482
  class DropdownFieldComponent {
1386
1483
  lookups = inject(LookupRegistryService);
1484
+ messages = inject(ValidationMessagesService);
1387
1485
  _field;
1388
1486
  _language = 'en';
1389
1487
  /** Setter-based so options resolve however the input is set — see `refreshChoiceOptions`. */
@@ -1442,100 +1540,100 @@ class DropdownFieldComponent {
1442
1540
  get errorMessage() {
1443
1541
  if (!this.control || !this.control.errors || !this.control.touched)
1444
1542
  return '';
1445
- const errs = this.control.errors;
1446
- if (errs['required'])
1447
- return 'Please select an option.';
1448
- return 'Invalid selection.';
1543
+ // `requiredSelection` rather than `required`: "Please select an option" reads better on a
1544
+ // dropdown than "This field is required", and both stay independently overridable.
1545
+ return this.messages.resolve(this.control.errors, this.language, [['required', 'requiredSelection']], 'invalidSelection');
1449
1546
  }
1450
1547
  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>
1491
- `, 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"] }] });
1548
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: DropdownFieldComponent, isStandalone: true, selector: "ngx-dropdown-field", inputs: { field: "field", language: "language", control: "control", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
1549
+ <div
1550
+ class="ngx-field ngx-field--dropdown"
1551
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type"
1552
+ [class.ngx-field--readonly]="readonly"
1553
+ [class.ngx-field--masked]="masked"
1554
+ [class.ngx-field--invalid]="control && control.invalid && control.touched"
1555
+ >
1556
+ <label class="ngx-field__label" [attr.for]="field.id">
1557
+ {{ label }}
1558
+ @if (field.validators?.required) { <span class="ngx-field__req">*</span> }
1559
+ </label>
1560
+ @if (masked) {
1561
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
1562
+ } @else if (readonly) {
1563
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ getLabel(control.value) }}</span>
1564
+ } @else {
1565
+ <select
1566
+ [id]="field.id"
1567
+ class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
1568
+ [formControl]="$any(control)"
1569
+ [compareWith]="compareFn"
1570
+ [attr.disabled]="field.disabled ? true : null"
1571
+ [attr.aria-invalid]="control.invalid && control.touched"
1572
+ [attr.aria-describedby]="errorMessage ? field.id + '-error' : null"
1573
+ >
1574
+ <option [value]="''">{{ placeholder || 'Select...' }}</option>
1575
+ @for (option of options(); track getOptLabel(option)) {
1576
+ @if (isObjectVal(option)) {
1577
+ <option [ngValue]="getOptStoredVal(option)">{{ getOptLabel(option) }}</option>
1578
+ } @else {
1579
+ <option [value]="getOptStoredVal(option)">{{ getOptLabel(option) }}</option>
1580
+ }
1581
+ }
1582
+ </select>
1583
+ @if (errorMessage) {
1584
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'" [id]="field.id + '-error'" role="alert">{{ errorMessage }}</span>
1585
+ }
1586
+ }
1587
+ </div>
1588
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1492
1589
  }
1493
1590
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: DropdownFieldComponent, decorators: [{
1494
1591
  type: Component,
1495
1592
  args: [{
1593
+ changeDetection: ChangeDetectionStrategy.OnPush,
1496
1594
  selector: 'ngx-dropdown-field',
1497
1595
  standalone: true,
1498
1596
  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>
1597
+ template: `
1598
+ <div
1599
+ class="ngx-field ngx-field--dropdown"
1600
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type"
1601
+ [class.ngx-field--readonly]="readonly"
1602
+ [class.ngx-field--masked]="masked"
1603
+ [class.ngx-field--invalid]="control && control.invalid && control.touched"
1604
+ >
1605
+ <label class="ngx-field__label" [attr.for]="field.id">
1606
+ {{ label }}
1607
+ @if (field.validators?.required) { <span class="ngx-field__req">*</span> }
1608
+ </label>
1609
+ @if (masked) {
1610
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
1611
+ } @else if (readonly) {
1612
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ getLabel(control.value) }}</span>
1613
+ } @else {
1614
+ <select
1615
+ [id]="field.id"
1616
+ class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
1617
+ [formControl]="$any(control)"
1618
+ [compareWith]="compareFn"
1619
+ [attr.disabled]="field.disabled ? true : null"
1620
+ [attr.aria-invalid]="control.invalid && control.touched"
1621
+ [attr.aria-describedby]="errorMessage ? field.id + '-error' : null"
1622
+ >
1623
+ <option [value]="''">{{ placeholder || 'Select...' }}</option>
1624
+ @for (option of options(); track getOptLabel(option)) {
1625
+ @if (isObjectVal(option)) {
1626
+ <option [ngValue]="getOptStoredVal(option)">{{ getOptLabel(option) }}</option>
1627
+ } @else {
1628
+ <option [value]="getOptStoredVal(option)">{{ getOptLabel(option) }}</option>
1629
+ }
1630
+ }
1631
+ </select>
1632
+ @if (errorMessage) {
1633
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'" [id]="field.id + '-error'" role="alert">{{ errorMessage }}</span>
1634
+ }
1635
+ }
1636
+ </div>
1539
1637
  `,
1540
1638
  }]
1541
1639
  }], propDecorators: { field: [{
@@ -1612,76 +1710,77 @@ class RadioFieldComponent {
1612
1710
  return typeof value === 'object' ? resolveLabel(value, this.language) : (value ?? '—');
1613
1711
  }
1614
1712
  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>
1646
- `, 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"] }] });
1713
+ 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: `
1714
+ <div class="ngx-field ngx-field--radio"
1715
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
1716
+ <fieldset class="ngx-field__fieldset">
1717
+ <legend class="ngx-field__label">{{ label }}</legend>
1718
+ @if (masked) {
1719
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
1720
+ } @else if (readonly) {
1721
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ getSelectedLabel() }}</span>
1722
+ } @else {
1723
+ <div class="ngx-field__radio-group">
1724
+ @for (option of options(); track getOptLabel(option)) {
1725
+ <label class="ngx-field__radio-option" [attr.for]="getRadioId(option)">
1726
+ <input
1727
+ [id]="getRadioId(option)"
1728
+ type="radio"
1729
+ class="ngx-field__radio-input"
1730
+ [formControl]="$any(control)"
1731
+ [value]="getOptStoredVal(option)"
1732
+ [attr.disabled]="field.disabled ? true : null"
1733
+ />
1734
+ <span class="ngx-field__radio-label">{{ getOptLabel(option) }}</span>
1735
+ </label>
1736
+ }
1737
+ </div>
1738
+ @if (control.invalid && control.touched) {
1739
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
1740
+ }
1741
+ }
1742
+ </fieldset>
1743
+ </div>
1744
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.RadioControlValueAccessor, selector: "input[type=radio][formControlName],input[type=radio][formControl],input[type=radio][ngModel]", inputs: ["name", "formControlName", "value"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1647
1745
  }
1648
1746
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: RadioFieldComponent, decorators: [{
1649
1747
  type: Component,
1650
1748
  args: [{
1749
+ changeDetection: ChangeDetectionStrategy.OnPush,
1651
1750
  selector: 'ngx-radio-field',
1652
1751
  standalone: true,
1653
1752
  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>
1753
+ template: `
1754
+ <div class="ngx-field ngx-field--radio"
1755
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
1756
+ <fieldset class="ngx-field__fieldset">
1757
+ <legend class="ngx-field__label">{{ label }}</legend>
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'">{{ getSelectedLabel() }}</span>
1762
+ } @else {
1763
+ <div class="ngx-field__radio-group">
1764
+ @for (option of options(); track getOptLabel(option)) {
1765
+ <label class="ngx-field__radio-option" [attr.for]="getRadioId(option)">
1766
+ <input
1767
+ [id]="getRadioId(option)"
1768
+ type="radio"
1769
+ class="ngx-field__radio-input"
1770
+ [formControl]="$any(control)"
1771
+ [value]="getOptStoredVal(option)"
1772
+ [attr.disabled]="field.disabled ? true : null"
1773
+ />
1774
+ <span class="ngx-field__radio-label">{{ getOptLabel(option) }}</span>
1775
+ </label>
1776
+ }
1777
+ </div>
1778
+ @if (control.invalid && control.touched) {
1779
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
1780
+ }
1781
+ }
1782
+ </fieldset>
1783
+ </div>
1685
1784
  `,
1686
1785
  }]
1687
1786
  }], propDecorators: { field: [{
@@ -1751,76 +1850,77 @@ class MultiSelectFieldComponent {
1751
1850
  .join(', ');
1752
1851
  }
1753
1852
  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>
1785
- `, 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"] }] });
1853
+ 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: `
1854
+ <div class="ngx-field ngx-field--multiSelect"
1855
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
1856
+ <label class="ngx-field__label">{{ label }}</label>
1857
+ @if (masked) {
1858
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
1859
+ } @else if (readonly) {
1860
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ getLabels(control.value) }}</span>
1861
+ } @else {
1862
+ <select
1863
+ [id]="field.id"
1864
+ class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
1865
+ [formControl]="$any(control)"
1866
+ [compareWith]="compareFn"
1867
+ [attr.disabled]="field.disabled ? true : null"
1868
+ multiple
1869
+ size="4"
1870
+ >
1871
+ @for (option of options(); track getOptLabel(option)) {
1872
+ @if (isObjectVal(option)) {
1873
+ <option [ngValue]="getOptStoredVal(option)">{{ getOptLabel(option) }}</option>
1874
+ } @else {
1875
+ <option [value]="getOptStoredVal(option)">{{ getOptLabel(option) }}</option>
1876
+ }
1877
+ }
1878
+ </select>
1879
+ @if (control.invalid && control.touched) {
1880
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
1881
+ }
1882
+ }
1883
+ </div>
1884
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.SelectMultipleControlValueAccessor, selector: "select[multiple][formControlName],select[multiple][formControl],select[multiple][ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1786
1885
  }
1787
1886
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: MultiSelectFieldComponent, decorators: [{
1788
1887
  type: Component,
1789
1888
  args: [{
1889
+ changeDetection: ChangeDetectionStrategy.OnPush,
1790
1890
  selector: 'ngx-multi-select-field',
1791
1891
  standalone: true,
1792
1892
  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>
1893
+ template: `
1894
+ <div class="ngx-field ngx-field--multiSelect"
1895
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
1896
+ <label class="ngx-field__label">{{ label }}</label>
1897
+ @if (masked) {
1898
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
1899
+ } @else if (readonly) {
1900
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ getLabels(control.value) }}</span>
1901
+ } @else {
1902
+ <select
1903
+ [id]="field.id"
1904
+ class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
1905
+ [formControl]="$any(control)"
1906
+ [compareWith]="compareFn"
1907
+ [attr.disabled]="field.disabled ? true : null"
1908
+ multiple
1909
+ size="4"
1910
+ >
1911
+ @for (option of options(); track getOptLabel(option)) {
1912
+ @if (isObjectVal(option)) {
1913
+ <option [ngValue]="getOptStoredVal(option)">{{ getOptLabel(option) }}</option>
1914
+ } @else {
1915
+ <option [value]="getOptStoredVal(option)">{{ getOptLabel(option) }}</option>
1916
+ }
1917
+ }
1918
+ </select>
1919
+ @if (control.invalid && control.touched) {
1920
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
1921
+ }
1922
+ }
1923
+ </div>
1824
1924
  `,
1825
1925
  }]
1826
1926
  }], propDecorators: { field: [{
@@ -1837,7 +1937,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
1837
1937
 
1838
1938
  /**
1839
1939
  * EntityRefRegistryService — resolves entity-ref option loader functions.
1840
- * ADR-006: Entity-ref options come ONLY from this registry.
1940
+ * Entity-ref options come ONLY from this registry.
1841
1941
  * Never pass loaders via @Input() on field components.
1842
1942
  *
1843
1943
  * Consumers register loaders via provideNgxDynamicEntity({ entityRefs: { clients: () => [...] } }).
@@ -2029,7 +2129,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
2029
2129
  /**
2030
2130
  * CascadeDataService — loads entity-reference options and applies parent→child cascades.
2031
2131
  *
2032
- * Loaders come from the registry only (ADR-006). This service owns the three things a
2132
+ * Loaders come from the registry only. This service owns the three things a
2033
2133
  * field component should not: resolving the loader, coercing whatever it returned
2034
2134
  * (array | Promise | Observable) into a Promise, and filtering by the parent's value.
2035
2135
  */
@@ -2209,6 +2309,9 @@ class EntityRefFieldComponent {
2209
2309
  return;
2210
2310
  void this.reload();
2211
2311
  this.watchParent();
2312
+ this.control?.valueChanges
2313
+ .pipe(takeUntilDestroyed(this.destroyRef))
2314
+ .subscribe(value => this.publishSelection(String(value ?? '')));
2212
2315
  }
2213
2316
  /** Reload options against the parent's current value. */
2214
2317
  async reload() {
@@ -2226,8 +2329,20 @@ class EntityRefFieldComponent {
2226
2329
  this.loading.set(false);
2227
2330
  }
2228
2331
  }
2229
- onSelectionChange() {
2230
- const selected = this.options().find(o => String(o.value) === String(this.control.value));
2332
+ /**
2333
+ * Publish the selected option so autoPatch can copy from its record.
2334
+ *
2335
+ * Driven from `valueChanges` rather than the select's `change` event: that event is the
2336
+ * same one the `formControl` directive listens to, so reading the control inside a
2337
+ * template `(change)` handler can see the previous value. `valueChanges` fires after the
2338
+ * model has updated — including Playwright `selectOption`, which updates the control
2339
+ * through the value accessor.
2340
+ */
2341
+ onSelectionChange(value) {
2342
+ this.publishSelection(value ?? String(this.control?.value ?? ''));
2343
+ }
2344
+ publishSelection(current) {
2345
+ const selected = this.options().find(o => String(o.value) === current);
2231
2346
  this.selectionBus.emit(this.field.id, selected ?? null);
2232
2347
  }
2233
2348
  getLabel(value) {
@@ -2259,80 +2374,79 @@ class EntityRefFieldComponent {
2259
2374
  });
2260
2375
  }
2261
2376
  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>
2295
- `, 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"] }] });
2377
+ 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: `
2378
+ <div class="ngx-field ngx-field--entity-ref"
2379
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
2380
+ <label class="ngx-field__label" [attr.for]="'field-' + field.id">{{ label }}</label>
2381
+ @if (masked) {
2382
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
2383
+ } @else if (readonly) {
2384
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ getLabel(control.value) }}</span>
2385
+ } @else {
2386
+ @if (loading()) {
2387
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-loading'" role="status">Loading…</span>
2388
+ } @else {
2389
+ <select
2390
+ class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
2391
+ [id]="'field-' + field.id"
2392
+ [formControl]="$any(control)"
2393
+ [attr.disabled]="field.disabled ? true : null"
2394
+ >
2395
+ <option value="">{{ placeholder || 'Select...' }}</option>
2396
+ @for (option of options(); track option.value) {
2397
+ <option [value]="option.value">{{ option.label }}</option>
2398
+ }
2399
+ </select>
2400
+ @if (awaitingParent()) {
2401
+ <span class="ngx-field__hint" [attr.data-testid]="'field-' + field.id + '-hint'">Select {{ parentFieldId }} first.</span>
2402
+ }
2403
+ }
2404
+ @if (control.invalid && control.touched) {
2405
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
2406
+ }
2407
+ }
2408
+ </div>
2409
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2296
2410
  }
2297
2411
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: EntityRefFieldComponent, decorators: [{
2298
2412
  type: Component,
2299
2413
  args: [{
2414
+ changeDetection: ChangeDetectionStrategy.OnPush,
2300
2415
  selector: 'ngx-entity-ref-field',
2301
2416
  standalone: true,
2302
2417
  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>
2418
+ template: `
2419
+ <div class="ngx-field ngx-field--entity-ref"
2420
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
2421
+ <label class="ngx-field__label" [attr.for]="'field-' + field.id">{{ label }}</label>
2422
+ @if (masked) {
2423
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
2424
+ } @else if (readonly) {
2425
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ getLabel(control.value) }}</span>
2426
+ } @else {
2427
+ @if (loading()) {
2428
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-loading'" role="status">Loading…</span>
2429
+ } @else {
2430
+ <select
2431
+ class="ngx-field__input" [attr.data-testid]="'field-' + field.id + '-input'"
2432
+ [id]="'field-' + field.id"
2433
+ [formControl]="$any(control)"
2434
+ [attr.disabled]="field.disabled ? true : null"
2435
+ >
2436
+ <option value="">{{ placeholder || 'Select...' }}</option>
2437
+ @for (option of options(); track option.value) {
2438
+ <option [value]="option.value">{{ option.label }}</option>
2439
+ }
2440
+ </select>
2441
+ @if (awaitingParent()) {
2442
+ <span class="ngx-field__hint" [attr.data-testid]="'field-' + field.id + '-hint'">Select {{ parentFieldId }} first.</span>
2443
+ }
2444
+ }
2445
+ @if (control.invalid && control.touched) {
2446
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">This field has an error</span>
2447
+ }
2448
+ }
2449
+ </div>
2336
2450
  `,
2337
2451
  }]
2338
2452
  }], propDecorators: { field: [{
@@ -2443,7 +2557,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
2443
2557
 
2444
2558
  /**
2445
2559
  * DynamicFieldComponent — mounts the correct field component for a given NestedFieldConfig.
2446
- * Uses ViewContainerRef.createComponent() and passes inputs via setInput() (ADR-008).
2560
+ * Uses ViewContainerRef.createComponent() and passes inputs via setInput(); see
2561
+ * DynamicFieldComponentContract for the five inputs every field component receives.
2447
2562
  * Never add field-type-specific logic here — this component must remain generic.
2448
2563
  *
2449
2564
  * The 5 inputs (field, control, language, readonly, masked) are passed to every mounted
@@ -2460,6 +2575,12 @@ class DynamicFieldComponent {
2460
2575
  fieldHost;
2461
2576
  fieldRegistry = inject(FieldRegistryService);
2462
2577
  rbacService = inject(RbacService);
2578
+ /**
2579
+ * Passed into `createComponent` so a hosted field sees this host's injector — including
2580
+ * `DynamicFormComponent`'s scoped `EntityRefSelectionService`. Without it, a standalone
2581
+ * field resolves the root bus and `autoPatch` never runs.
2582
+ */
2583
+ injector = inject(Injector);
2463
2584
  componentRef = null;
2464
2585
  ngOnChanges(_changes) {
2465
2586
  this.mountField();
@@ -2481,9 +2602,12 @@ class DynamicFieldComponent {
2481
2602
  return;
2482
2603
  }
2483
2604
  this.fieldHost.clear();
2484
- this.componentRef = this.fieldHost.createComponent(ComponentClass);
2605
+ this.componentRef = this.fieldHost.createComponent(ComponentClass, {
2606
+ injector: this.injector,
2607
+ });
2485
2608
  this.setInputs(this.componentRef, masked);
2486
- this.componentRef.changeDetectorRef.markForCheck();
2609
+ this.watchControl();
2610
+ this.componentRef.changeDetectorRef.detectChanges();
2487
2611
  }
2488
2612
  /**
2489
2613
  * Field types are opt-in (see `provideFieldTypes` / `provideBuiltInFieldTypes`), so an
@@ -2499,7 +2623,44 @@ class DynamicFieldComponent {
2499
2623
  }
2500
2624
  static warnedTypes = new Set();
2501
2625
  /**
2502
- * Pass all 5 contract inputs via ComponentRef.setInput() — uniform for all types (ADR-008).
2626
+ * Re-check the hosted field component.
2627
+ *
2628
+ * The field components are OnPush, so they re-render on an input change, an event from
2629
+ * their own template, or a signal they read. `form.markAllAsTouched()` is none of those —
2630
+ * it flips `touched` from outside, and every field's error message depends on it. Without
2631
+ * this, a blocked submit would mark the form touched and no error would appear.
2632
+ */
2633
+ refresh() {
2634
+ this.componentRef?.changeDetectorRef.detectChanges();
2635
+ }
2636
+ /**
2637
+ * Re-check the hosted component whenever its control changes from outside.
2638
+ *
2639
+ * The field components are OnPush, so they re-render on an input change or an event from
2640
+ * their own template. A control mutated externally is neither — and that is not an edge
2641
+ * case: patchForm, reset, autoPatch and patchOnTrue all do exactly that. Without this, a
2642
+ * field patched from a record would keep rendering its previous value.
2643
+ *
2644
+ * `detectChanges()` rather than `markForCheck()`: a selection that arrives from a native
2645
+ * `change` event (or Playwright) may not be followed by another Angular tick, so a dirty
2646
+ * flag would never be flushed. Running CD on the hosted component now is the copy showing up.
2647
+ *
2648
+ * Done here rather than in each of the eighteen field components: the host owns the
2649
+ * component reference, so one subscription covers every type including custom ones.
2650
+ */
2651
+ watchControl() {
2652
+ this.controlSub?.unsubscribe();
2653
+ if (!this.control)
2654
+ return;
2655
+ this.controlSub = this.control.valueChanges.subscribe(() => this.refresh());
2656
+ }
2657
+ controlSub;
2658
+ ngOnDestroy() {
2659
+ this.controlSub?.unsubscribe();
2660
+ }
2661
+ /**
2662
+ * Pass all 5 contract inputs via ComponentRef.setInput() — uniform for all types.
2663
+ * The public shape is DynamicFieldComponentContract.
2503
2664
  * Must use setInput(), not property assignment: the field components declare inputs with
2504
2665
  * definite assignment and no initializer, and the lib compiles with
2505
2666
  * useDefineForClassFields:false, so `'field' in instance` is false until first set.
@@ -2540,7 +2701,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
2540
2701
  args: ['fieldHost', { read: ViewContainerRef, static: true }]
2541
2702
  }] } });
2542
2703
 
2543
- /** ADR-008: GroupFieldComponent — renders nested fieldset container for group.children */
2704
+ /** Renders a nested fieldset container for a group field's children. */
2544
2705
  class GroupFieldComponent {
2545
2706
  field;
2546
2707
  control;
@@ -2557,56 +2718,56 @@ class GroupFieldComponent {
2557
2718
  return group?.get(childId);
2558
2719
  }
2559
2720
  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>
2583
- `, 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"] }] });
2721
+ 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: `
2722
+ <fieldset class="ngx-field ngx-field--group"
2723
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
2724
+ <legend class="ngx-field__legend">{{ label }}</legend>
2725
+ @if (masked) {
2726
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
2727
+ } @else {
2728
+ <div class="ngx-field__group-children">
2729
+ @for (child of field.children ?? []; track child.id) {
2730
+ @if (child.visibility !== false) {
2731
+ <ngx-dynamic-field
2732
+ [field]="child"
2733
+ [control]="getChildControl(child.id)"
2734
+ [config]="config"
2735
+ [language]="language"
2736
+ [readonly]="readonly || !!child.readonly"
2737
+ [userRoles]="userRoles"
2738
+ />
2739
+ }
2740
+ }
2741
+ </div>
2742
+ }
2743
+ </fieldset>
2744
+ `, isInline: true, styles: [".ngx-field--group{border:1px solid #e2e8f0;border-radius:8px;padding:16px;margin-bottom:16px;background:#fafafa}.ngx-field__legend{font-weight:600;font-size:14px;padding:0 8px;color:#334155}.ngx-field__group-children{display:flex;flex-direction:column;gap:12px}\n"], dependencies: [{ kind: "ngmodule", type: i0.forwardRef(() => ReactiveFormsModule) }, { kind: "component", type: i0.forwardRef(() => DynamicFieldComponent), selector: "ngx-dynamic-field", inputs: ["field", "control", "config", "currentTabId", "language", "readonly", "userRoles"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2584
2745
  }
2585
2746
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: GroupFieldComponent, decorators: [{
2586
2747
  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>
2748
+ args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'ngx-group-field', standalone: true, imports: [ReactiveFormsModule, forwardRef(() => DynamicFieldComponent)], template: `
2749
+ <fieldset class="ngx-field ngx-field--group"
2750
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
2751
+ <legend class="ngx-field__legend">{{ label }}</legend>
2752
+ @if (masked) {
2753
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
2754
+ } @else {
2755
+ <div class="ngx-field__group-children">
2756
+ @for (child of field.children ?? []; track child.id) {
2757
+ @if (child.visibility !== false) {
2758
+ <ngx-dynamic-field
2759
+ [field]="child"
2760
+ [control]="getChildControl(child.id)"
2761
+ [config]="config"
2762
+ [language]="language"
2763
+ [readonly]="readonly || !!child.readonly"
2764
+ [userRoles]="userRoles"
2765
+ />
2766
+ }
2767
+ }
2768
+ </div>
2769
+ }
2770
+ </fieldset>
2610
2771
  `, 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
2772
  }], propDecorators: { field: [{
2612
2773
  type: Input
@@ -2629,6 +2790,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
2629
2790
  */
2630
2791
  class ValidatorRegistryService {
2631
2792
  consumerRegistry = inject(VALIDATOR_REGISTRY, { optional: true }) ?? new Map();
2793
+ asyncRegistry = inject(ASYNC_VALIDATOR_REGISTRY, { optional: true }) ?? new Map();
2632
2794
  /**
2633
2795
  * Resolve a validator key string to a ValidatorFn.
2634
2796
  * Supports parameterized built-ins like 'min:0', 'maxLength:255'.
@@ -2676,8 +2838,40 @@ class ValidatorRegistryService {
2676
2838
  fnList.push(Validators.maxLength(config.maxLength));
2677
2839
  if (config.pattern)
2678
2840
  fnList.push(Validators.pattern(config.pattern));
2841
+ if (config.email)
2842
+ fnList.push(Validators.email);
2843
+ // Named validators from the consumer registry. This branch previously hardcoded the
2844
+ // built-ins and consulted the registry only for the `string[]` form of this config, so
2845
+ // a validator the consumer had registered could not be named from a typed schema
2846
+ // without casting it to `any`.
2847
+ for (const key of config.custom ?? []) {
2848
+ const fn = this.resolve(key);
2849
+ if (fn)
2850
+ fnList.push(fn);
2851
+ }
2679
2852
  return fnList;
2680
2853
  }
2854
+ /**
2855
+ * Resolve the async validators a field names, in `validators.customAsync`.
2856
+ *
2857
+ * Async validators are deliberately a separate registry and a separate config key: Angular
2858
+ * attaches them through `setAsyncValidators`, runs them only once the synchronous ones
2859
+ * pass, and holds the control in `pending` until they settle. Mixing them into the
2860
+ * synchronous list would silently never run them.
2861
+ *
2862
+ * An unknown name is skipped rather than throwing, matching the synchronous behaviour.
2863
+ */
2864
+ resolveAsyncFromConfig(config) {
2865
+ if (!config || Array.isArray(config))
2866
+ return [];
2867
+ const out = [];
2868
+ for (const key of config.customAsync ?? []) {
2869
+ const fn = this.asyncRegistry.get(key);
2870
+ if (fn)
2871
+ out.push(fn);
2872
+ }
2873
+ return out;
2874
+ }
2681
2875
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ValidatorRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
2682
2876
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ValidatorRegistryService, providedIn: 'root' });
2683
2877
  }
@@ -2686,8 +2880,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
2686
2880
  args: [{ providedIn: 'root' }]
2687
2881
  }] });
2688
2882
 
2689
- /** ADR-008: ArrayFieldComponent — renders repeating rows for FormArray with + Add Item and Remove */
2883
+ /** Renders repeating rows for a FormArray, with Add Item and Remove. */
2690
2884
  class ArrayFieldComponent {
2885
+ /**
2886
+ * addItem/removeItem are public and may be called from outside this component's template,
2887
+ * which under OnPush would not re-render. The host also watches the control, but this
2888
+ * keeps the component correct on its own.
2889
+ */
2890
+ cdr = inject(ChangeDetectorRef);
2691
2891
  field;
2692
2892
  control;
2693
2893
  config;
@@ -2714,115 +2914,117 @@ class ArrayFieldComponent {
2714
2914
  }
2715
2915
  const group = this.fb.group(controls);
2716
2916
  this.formArray.push(group);
2917
+ this.cdr.markForCheck();
2717
2918
  }
2718
2919
  removeItem(index) {
2719
2920
  this.formArray.removeAt(index);
2921
+ this.cdr.markForCheck();
2720
2922
  }
2721
2923
  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>
2772
- `, 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"] }] });
2924
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: ArrayFieldComponent, isStandalone: true, selector: "ngx-array-field", inputs: { field: "field", control: "control", config: "config", language: "language", readonly: "readonly", masked: "masked", userRoles: "userRoles" }, ngImport: i0, template: `
2925
+ <div class="ngx-field ngx-field--array"
2926
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
2927
+ <div class="ngx-field__array-header">
2928
+ <label class="ngx-field__label">{{ label }}</label>
2929
+ @if (!readonly && !masked) {
2930
+ <button type="button" class="ngx-field__array-add-btn"
2931
+ [attr.data-testid]="'field-' + field.id + '-add'" (click)="addItem()">
2932
+ + Add Item
2933
+ </button>
2934
+ }
2935
+ </div>
2936
+
2937
+ @if (masked) {
2938
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
2939
+ } @else {
2940
+ <div class="ngx-field__array-list">
2941
+ @for (itemGroup of formArray.controls; track idx; let idx = $index) {
2942
+ <div class="ngx-field__array-item" [attr.data-testid]="'field-' + field.id + '-row'">
2943
+ <div class="ngx-field__array-item-header">
2944
+ <span class="ngx-field__array-item-title">Item #{{ idx + 1 }}</span>
2945
+ @if (!readonly) {
2946
+ <button type="button" class="ngx-field__array-remove-btn"
2947
+ [attr.data-testid]="'field-' + field.id + '-remove-' + idx" (click)="removeItem(idx)">
2948
+ Remove
2949
+ </button>
2950
+ }
2951
+ </div>
2952
+ <div class="ngx-field__array-item-fields">
2953
+ @for (child of field.children ?? []; track child.id) {
2954
+ @if (child.visibility !== false) {
2955
+ <ngx-dynamic-field
2956
+ [field]="child"
2957
+ [control]="getItemChildControl(itemGroup, child.id)"
2958
+ [config]="config"
2959
+ [language]="language"
2960
+ [readonly]="readonly || !!child.readonly"
2961
+ [userRoles]="userRoles"
2962
+ />
2963
+ }
2964
+ }
2965
+ </div>
2966
+ </div>
2967
+ }
2968
+ @if (formArray.controls.length === 0) {
2969
+ <div class="ngx-field__array-empty" [attr.data-testid]="'field-' + field.id + '-empty'">No items added yet.</div>
2970
+ }
2971
+ </div>
2972
+ }
2973
+ </div>
2974
+ `, isInline: true, styles: [".ngx-field--array{border:1px solid #e2e8f0;border-radius:8px;padding:16px;margin-bottom:16px;background:#f8fafc}.ngx-field__array-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}.ngx-field__array-add-btn{background:#2563eb;color:#fff;border:none;padding:6px 12px;border-radius:4px;font-weight:500;cursor:pointer}.ngx-field__array-add-btn:hover{background:#1d4ed8}.ngx-field__array-list{display:flex;flex-direction:column;gap:12px}.ngx-field__array-item{background:#fff;border:1px solid #cbd5e1;border-radius:6px;padding:12px}.ngx-field__array-item-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;border-bottom:1px solid #f1f5f9;padding-bottom:6px}.ngx-field__array-item-title{font-weight:600;font-size:13px;color:#475569}.ngx-field__array-remove-btn{background:#ef4444;color:#fff;border:none;padding:4px 8px;border-radius:4px;font-size:12px;cursor:pointer}.ngx-field__array-empty{color:#94a3b8;font-style:italic;font-size:13px}\n"], dependencies: [{ kind: "ngmodule", type: i0.forwardRef(() => ReactiveFormsModule) }, { kind: "component", type: i0.forwardRef(() => DynamicFieldComponent), selector: "ngx-dynamic-field", inputs: ["field", "control", "config", "currentTabId", "language", "readonly", "userRoles"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2773
2975
  }
2774
2976
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ArrayFieldComponent, decorators: [{
2775
2977
  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>
2978
+ args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'ngx-array-field', standalone: true, imports: [ReactiveFormsModule, forwardRef(() => DynamicFieldComponent)], template: `
2979
+ <div class="ngx-field ngx-field--array"
2980
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
2981
+ <div class="ngx-field__array-header">
2982
+ <label class="ngx-field__label">{{ label }}</label>
2983
+ @if (!readonly && !masked) {
2984
+ <button type="button" class="ngx-field__array-add-btn"
2985
+ [attr.data-testid]="'field-' + field.id + '-add'" (click)="addItem()">
2986
+ + Add Item
2987
+ </button>
2988
+ }
2989
+ </div>
2990
+
2991
+ @if (masked) {
2992
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
2993
+ } @else {
2994
+ <div class="ngx-field__array-list">
2995
+ @for (itemGroup of formArray.controls; track idx; let idx = $index) {
2996
+ <div class="ngx-field__array-item" [attr.data-testid]="'field-' + field.id + '-row'">
2997
+ <div class="ngx-field__array-item-header">
2998
+ <span class="ngx-field__array-item-title">Item #{{ idx + 1 }}</span>
2999
+ @if (!readonly) {
3000
+ <button type="button" class="ngx-field__array-remove-btn"
3001
+ [attr.data-testid]="'field-' + field.id + '-remove-' + idx" (click)="removeItem(idx)">
3002
+ Remove
3003
+ </button>
3004
+ }
3005
+ </div>
3006
+ <div class="ngx-field__array-item-fields">
3007
+ @for (child of field.children ?? []; track child.id) {
3008
+ @if (child.visibility !== false) {
3009
+ <ngx-dynamic-field
3010
+ [field]="child"
3011
+ [control]="getItemChildControl(itemGroup, child.id)"
3012
+ [config]="config"
3013
+ [language]="language"
3014
+ [readonly]="readonly || !!child.readonly"
3015
+ [userRoles]="userRoles"
3016
+ />
3017
+ }
3018
+ }
3019
+ </div>
3020
+ </div>
3021
+ }
3022
+ @if (formArray.controls.length === 0) {
3023
+ <div class="ngx-field__array-empty" [attr.data-testid]="'field-' + field.id + '-empty'">No items added yet.</div>
3024
+ }
3025
+ </div>
3026
+ }
3027
+ </div>
2826
3028
  `, 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
3029
  }], propDecorators: { field: [{
2828
3030
  type: Input
@@ -2980,102 +3182,103 @@ class ImageFieldComponent {
2980
3182
  this.uploads.revokePreviewUrl(this.previewCache);
2981
3183
  }
2982
3184
  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>
3027
- `, isInline: true });
3185
+ 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: `
3186
+ <div class="ngx-field ngx-field--image"
3187
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
3188
+ <label class="ngx-field__label" [attr.for]="'field-' + field.id">{{ label }}</label>
3189
+ @if (masked) {
3190
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
3191
+ } @else {
3192
+ <div class="ngx-field__image-wrap">
3193
+ @if (previewUrl()) {
3194
+ <img class="ngx-field__image-preview" [src]="previewUrl()" [alt]="label" />
3195
+ } @else {
3196
+ <div class="ngx-field__image-placeholder">
3197
+ <span>📷</span>
3198
+ <span>No image</span>
3199
+ </div>
3200
+ }
3201
+ @if (!readonly) {
3202
+ <div class="ngx-field__image-actions">
3203
+ <label class="ngx-field__upload-btn" [class.ngx-field__upload-btn--loading]="uploading()">
3204
+ @if (uploading()) {
3205
+ <span>Uploading…</span>
3206
+ } @else {
3207
+ <span>{{ previewUrl() ? 'Change' : 'Upload' }}</span>
3208
+ <input
3209
+ type="file"
3210
+ class="ngx-field__file-input"
3211
+ [id]="'field-' + field.id"
3212
+ accept="image/*"
3213
+ [disabled]="field.disabled || uploading()"
3214
+ (change)="onFileSelect($any($event.target).files[0])"
3215
+ />
3216
+ }
3217
+ </label>
3218
+ @if (previewUrl()) {
3219
+ <button type="button" class="ngx-field__remove-btn" (click)="remove()">Remove</button>
3220
+ }
3221
+ </div>
3222
+ }
3223
+ </div>
3224
+ @if (uploadError()) {
3225
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">{{ uploadError() }}</span>
3226
+ }
3227
+ }
3228
+ </div>
3229
+ `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
3028
3230
  }
3029
3231
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ImageFieldComponent, decorators: [{
3030
3232
  type: Component,
3031
3233
  args: [{
3234
+ changeDetection: ChangeDetectionStrategy.OnPush,
3032
3235
  selector: 'ngx-image-field',
3033
3236
  standalone: true,
3034
3237
  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>
3238
+ template: `
3239
+ <div class="ngx-field ngx-field--image"
3240
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
3241
+ <label class="ngx-field__label" [attr.for]="'field-' + field.id">{{ label }}</label>
3242
+ @if (masked) {
3243
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
3244
+ } @else {
3245
+ <div class="ngx-field__image-wrap">
3246
+ @if (previewUrl()) {
3247
+ <img class="ngx-field__image-preview" [src]="previewUrl()" [alt]="label" />
3248
+ } @else {
3249
+ <div class="ngx-field__image-placeholder">
3250
+ <span>📷</span>
3251
+ <span>No image</span>
3252
+ </div>
3253
+ }
3254
+ @if (!readonly) {
3255
+ <div class="ngx-field__image-actions">
3256
+ <label class="ngx-field__upload-btn" [class.ngx-field__upload-btn--loading]="uploading()">
3257
+ @if (uploading()) {
3258
+ <span>Uploading…</span>
3259
+ } @else {
3260
+ <span>{{ previewUrl() ? 'Change' : 'Upload' }}</span>
3261
+ <input
3262
+ type="file"
3263
+ class="ngx-field__file-input"
3264
+ [id]="'field-' + field.id"
3265
+ accept="image/*"
3266
+ [disabled]="field.disabled || uploading()"
3267
+ (change)="onFileSelect($any($event.target).files[0])"
3268
+ />
3269
+ }
3270
+ </label>
3271
+ @if (previewUrl()) {
3272
+ <button type="button" class="ngx-field__remove-btn" (click)="remove()">Remove</button>
3273
+ }
3274
+ </div>
3275
+ }
3276
+ </div>
3277
+ @if (uploadError()) {
3278
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">{{ uploadError() }}</span>
3279
+ }
3280
+ }
3281
+ </div>
3079
3282
  `,
3080
3283
  }]
3081
3284
  }], propDecorators: { field: [{
@@ -3136,98 +3339,99 @@ class FileFieldComponent {
3136
3339
  this.control.markAsTouched();
3137
3340
  }
3138
3341
  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>
3181
- `, isInline: true });
3342
+ 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: `
3343
+ <div class="ngx-field ngx-field--file"
3344
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
3345
+ <label class="ngx-field__label" [attr.for]="'field-' + field.id">{{ label }}</label>
3346
+ @if (masked) {
3347
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
3348
+ } @else if (readonly) {
3349
+ @if (fileUrl()) {
3350
+ <a class="ngx-field__file-link" [href]="fileUrl()!" target="_blank" rel="noopener">
3351
+ 📎 {{ fileName() || 'Download file' }}
3352
+ </a>
3353
+ } @else {
3354
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">—</span>
3355
+ }
3356
+ } @else {
3357
+ <div class="ngx-field__file-wrap">
3358
+ @if (fileName()) {
3359
+ <div class="ngx-field__file-selected">
3360
+ <span class="ngx-field__file-name">📎 {{ fileName() }}</span>
3361
+ <button type="button" class="ngx-field__remove-btn" (click)="remove()" aria-label="Remove file">✕</button>
3362
+ </div>
3363
+ }
3364
+ <label class="ngx-field__upload-btn" [class.ngx-field__upload-btn--loading]="uploading()">
3365
+ @if (uploading()) {
3366
+ <span>Uploading…</span>
3367
+ } @else {
3368
+ <span>{{ fileName() ? 'Replace file' : 'Choose file' }}</span>
3369
+ <input
3370
+ type="file"
3371
+ class="ngx-field__file-input"
3372
+ [id]="'field-' + field.id"
3373
+ [disabled]="field.disabled || uploading()"
3374
+ (change)="onFileSelect($any($event.target).files[0])"
3375
+ />
3376
+ }
3377
+ </label>
3378
+ </div>
3379
+ @if (uploadError()) {
3380
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">{{ uploadError() }}</span>
3381
+ }
3382
+ }
3383
+ </div>
3384
+ `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
3182
3385
  }
3183
3386
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: FileFieldComponent, decorators: [{
3184
3387
  type: Component,
3185
3388
  args: [{
3389
+ changeDetection: ChangeDetectionStrategy.OnPush,
3186
3390
  selector: 'ngx-file-field',
3187
3391
  standalone: true,
3188
3392
  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>
3393
+ template: `
3394
+ <div class="ngx-field ngx-field--file"
3395
+ [attr.data-testid]="'field-' + field.id" [attr.data-field-type]="field.type" [class.ngx-field--readonly]="readonly" [class.ngx-field--masked]="masked">
3396
+ <label class="ngx-field__label" [attr.for]="'field-' + field.id">{{ label }}</label>
3397
+ @if (masked) {
3398
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">XXXXXXXXX</span>
3399
+ } @else if (readonly) {
3400
+ @if (fileUrl()) {
3401
+ <a class="ngx-field__file-link" [href]="fileUrl()!" target="_blank" rel="noopener">
3402
+ 📎 {{ fileName() || 'Download file' }}
3403
+ </a>
3404
+ } @else {
3405
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">—</span>
3406
+ }
3407
+ } @else {
3408
+ <div class="ngx-field__file-wrap">
3409
+ @if (fileName()) {
3410
+ <div class="ngx-field__file-selected">
3411
+ <span class="ngx-field__file-name">📎 {{ fileName() }}</span>
3412
+ <button type="button" class="ngx-field__remove-btn" (click)="remove()" aria-label="Remove file">✕</button>
3413
+ </div>
3414
+ }
3415
+ <label class="ngx-field__upload-btn" [class.ngx-field__upload-btn--loading]="uploading()">
3416
+ @if (uploading()) {
3417
+ <span>Uploading…</span>
3418
+ } @else {
3419
+ <span>{{ fileName() ? 'Replace file' : 'Choose file' }}</span>
3420
+ <input
3421
+ type="file"
3422
+ class="ngx-field__file-input"
3423
+ [id]="'field-' + field.id"
3424
+ [disabled]="field.disabled || uploading()"
3425
+ (change)="onFileSelect($any($event.target).files[0])"
3426
+ />
3427
+ }
3428
+ </label>
3429
+ </div>
3430
+ @if (uploadError()) {
3431
+ <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'">{{ uploadError() }}</span>
3432
+ }
3433
+ }
3434
+ </div>
3231
3435
  `,
3232
3436
  }]
3233
3437
  }], propDecorators: { field: [{
@@ -3412,7 +3616,26 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
3412
3616
  */
3413
3617
  class DynamicFormComponent {
3414
3618
  // ─── Inputs ───────────────────────────────────────────────────────────────
3415
- config;
3619
+ /**
3620
+ * The schema to render.
3621
+ *
3622
+ * An accessor pair rather than a plain field: `ngOnChanges` normalises the option shape,
3623
+ * and that result is kept here instead of being written back over the input. Reassigning
3624
+ * an `@Input` works, but it mutates what the parent handed us — and a parent using OnPush
3625
+ * or signals would never observe the replacement anyway.
3626
+ *
3627
+ * Reads of `this.config` get the normalised copy once it exists, so every internal caller
3628
+ * is unaffected.
3629
+ */
3630
+ set config(value) {
3631
+ this.rawConfig = value;
3632
+ this.normalizedConfig = undefined; // re-normalised in ngOnChanges
3633
+ }
3634
+ get config() {
3635
+ return this.normalizedConfig ?? this.rawConfig;
3636
+ }
3637
+ rawConfig;
3638
+ normalizedConfig;
3416
3639
  rules;
3417
3640
  initialData;
3418
3641
  /** Session-original values for `VALUE_CHANGED` rules. Captured from the first build when omitted. */
@@ -3446,6 +3669,11 @@ class DynamicFormComponent {
3446
3669
  formReset = new EventEmitter();
3447
3670
  /** The visible tab changed. Lets a host track it without reaching into this component. */
3448
3671
  activeTabChange = new EventEmitter();
3672
+ /**
3673
+ * Emitted when a `beforeSave` hook aborted the save — by returning false or throwing.
3674
+ * `formSubmit` does not fire in that case.
3675
+ */
3676
+ saveRejected = new EventEmitter();
3449
3677
  // ─── Services ─────────────────────────────────────────────────────────────
3450
3678
  fb = inject(FormBuilder);
3451
3679
  validatorRegistry = inject(ValidatorRegistryService);
@@ -3454,6 +3682,7 @@ class DynamicFormComponent {
3454
3682
  rulesEvaluation = inject(RulesEvaluationService);
3455
3683
  entityRefSelection = inject(EntityRefSelectionService);
3456
3684
  commonModulesRegistry = inject(COMMON_MODULES_REGISTRY, { optional: true });
3685
+ migrations = inject(RECORD_MIGRATIONS, { optional: true }) ?? [];
3457
3686
  Object = Object;
3458
3687
  // ─── Signals (local reactive state) ───────────────────────────────────────
3459
3688
  activeTab = signal('');
@@ -3464,6 +3693,10 @@ class DynamicFormComponent {
3464
3693
  sessionBaseline = signal({});
3465
3694
  /** Critical fields the user has explicitly unlocked for editing. */
3466
3695
  unlockedFields = signal(new Set());
3696
+ /** The active tab's field panel, focused after a tab switch. */
3697
+ formPanel;
3698
+ /** The hosted field components, so an external control-state change can reach them. */
3699
+ fieldHosts;
3467
3700
  // ─── Form ─────────────────────────────────────────────────────────────────
3468
3701
  form;
3469
3702
  valueSub;
@@ -3536,11 +3769,72 @@ class DynamicFormComponent {
3536
3769
  const hiddenFields = this.ruleResult().hiddenFields;
3537
3770
  return rawFields.filter(field => evaluateFieldVisibility(field, currentValues) && !hiddenFields.includes(field.id));
3538
3771
  }
3772
+ /**
3773
+ * Entity-level permissions for the current roles.
3774
+ *
3775
+ * Cached rather than recomputed: this is read from the template several times per change
3776
+ * detection pass, and a fresh object each time is both wasted allocation and a new
3777
+ * identity for anything comparing by reference. Invalidated in `ngOnChanges` when
3778
+ * `config` or `userRoles` change, which are its only inputs.
3779
+ */
3539
3780
  get permissions() {
3540
- return this.rbacService.getPermissions(this.config, this.userRoles);
3781
+ return (this.permissionsCache ??= this.rbacService.getPermissions(this.config, this.userRoles));
3782
+ }
3783
+ permissionsCache = null;
3784
+ /**
3785
+ * Whether the current roles may see this record at all.
3786
+ *
3787
+ * `permissions.view` used to be computed and thrown away — a user whose roles failed it
3788
+ * still received the complete form with every value in the DOM. It is honoured now.
3789
+ *
3790
+ * As with masking, this is presentational: it stops the browser rendering data, it does
3791
+ * not stop the data reaching the browser. Authorize on the server.
3792
+ */
3793
+ get canView() {
3794
+ return this.permissions.canView;
3795
+ }
3796
+ /**
3797
+ * Whether the current roles may delete this record.
3798
+ *
3799
+ * The library ships no delete affordance, so this is surfaced for the consumer to gate
3800
+ * their own — exposed rather than dropped, because a permission the schema declares
3801
+ * should be answerable.
3802
+ */
3803
+ get canDelete() {
3804
+ return this.permissions.canDelete;
3541
3805
  }
3542
3806
  get canSubmit() {
3543
- return this.permissions.canEdit && !this.readonly;
3807
+ return this.canView && this.permissions.canEdit && !this.readonly;
3808
+ }
3809
+ /**
3810
+ * Errors raised by `validation` rules, keyed by the target they point at.
3811
+ *
3812
+ * A rule can declare the record invalid independently of Angular's validators, so form
3813
+ * validity alone is not the whole picture. `DynamicRecordFormComponent.saveSection()`
3814
+ * has always honoured these; `submit()` did not, which meant the same rule blocked one
3815
+ * save path and merely painted a banner on the other — and anything wiring
3816
+ * `(formSubmit)` to persistence wrote records the rules engine had already rejected.
3817
+ */
3818
+ get ruleValidationErrors() {
3819
+ return this.ruleResult().validationErrors;
3820
+ }
3821
+ /** True when a `validation` rule is currently blocking submission. */
3822
+ get hasRuleErrors() {
3823
+ return Object.keys(this.ruleValidationErrors).length > 0;
3824
+ }
3825
+ /**
3826
+ * True while an async validator is still running.
3827
+ *
3828
+ * Angular reports a control with an outstanding async check as `pending`, and `invalid` is
3829
+ * false until it settles — so without this a form could be submitted in the gap before a
3830
+ * uniqueness check came back.
3831
+ */
3832
+ get isValidating() {
3833
+ return this.form.pending;
3834
+ }
3835
+ /** Single source of truth for both the submit guard and the button's disabled state. */
3836
+ get submitBlocked() {
3837
+ return this.form.invalid || this.form.pending || this.hasRuleErrors;
3544
3838
  }
3545
3839
  ngOnInit() {
3546
3840
  this.selectionSub = this.entityRefSelection.selection$.subscribe(({ fieldId, option }) => {
@@ -3548,12 +3842,18 @@ class DynamicFormComponent {
3548
3842
  });
3549
3843
  }
3550
3844
  ngOnChanges(changes) {
3551
- if (changes['config'] && this.config) {
3845
+ if (changes['config'] || changes['userRoles']) {
3846
+ this.permissionsCache = null;
3847
+ }
3848
+ if (changes['config'] && this.rawConfig) {
3552
3849
  // Configs arrive as plain JSON from storage or an API, where TypeScript cannot enforce
3553
3850
  // the option shape. Normalise here, at the library boundary, so everything downstream
3554
3851
  // can rely on an option being a LocalizedText. Returns the same object when it already
3555
3852
  // is, so a well-formed config costs nothing.
3556
- this.config = normalizeConfigOptions(this.config);
3853
+ // Kept in a field of our own rather than written back to the @Input. Reassigning an
3854
+ // input works, but it surprises a parent that holds the same object — and a parent
3855
+ // using OnPush or signals would not see the replacement anyway.
3856
+ this.normalizedConfig = normalizeConfigOptions(this.config);
3557
3857
  }
3558
3858
  if (changes['config'] || changes['initialData']) {
3559
3859
  this.buildForm();
@@ -3567,9 +3867,12 @@ class DynamicFormComponent {
3567
3867
  this.selectionSub?.unsubscribe();
3568
3868
  }
3569
3869
  setActiveTab(tabId) {
3570
- if (this.activeTab() !== tabId)
3870
+ const changed = this.activeTab() !== tabId;
3871
+ if (changed)
3571
3872
  this.activeTabChange.emit(tabId);
3572
3873
  this.activeTab.set(tabId);
3874
+ if (changed)
3875
+ this.focusActivePanel();
3573
3876
  const parent = findTab(this.tabs, tabId);
3574
3877
  if (parent?.children?.length) {
3575
3878
  this.activeSubTab.set(parent.children[0].id);
@@ -3579,7 +3882,10 @@ class DynamicFormComponent {
3579
3882
  }
3580
3883
  }
3581
3884
  setActiveSubTab(subTabId) {
3885
+ const changed = this.activeSubTab() !== subTabId;
3582
3886
  this.activeSubTab.set(subTabId);
3887
+ if (changed)
3888
+ this.focusActivePanel();
3583
3889
  }
3584
3890
  getFieldSpan(field) {
3585
3891
  const span = field.colSpan ?? 12;
@@ -3641,13 +3947,14 @@ class DynamicFormComponent {
3641
3947
  buildTabControls(this.config.tabs || [], group);
3642
3948
  this.form = this.fb.group(group);
3643
3949
  if (this.initialData) {
3644
- this.patchForm(this.initialData);
3950
+ this.patchForm(this.upgradeRecord(this.initialData));
3645
3951
  }
3646
3952
  const initialFlattened = this.flattenFormValues();
3647
3953
  this.formValues.set(initialFlattened);
3648
3954
  this.previousValues = { ...initialFlattened };
3649
3955
  this.sessionBaseline.set({ ...initialFlattened });
3650
3956
  this.unlockedFields.set(new Set());
3957
+ this.syncHiddenFieldState(initialFlattened);
3651
3958
  if (this.preview) {
3652
3959
  // Show what an array field looks like rather than an empty slot, then freeze.
3653
3960
  for (const field of this.allFields()) {
@@ -3668,6 +3975,7 @@ class DynamicFormComponent {
3668
3975
  const flattened = this.flattenFormValues();
3669
3976
  this.formValues.set(flattened);
3670
3977
  this.runPatchOnTrue(flattened);
3978
+ this.syncHiddenFieldState(flattened);
3671
3979
  this.previousValues = { ...flattened };
3672
3980
  this.formChange.emit(this.extractRecord());
3673
3981
  });
@@ -3685,7 +3993,8 @@ class DynamicFormComponent {
3685
3993
  }
3686
3994
  else {
3687
3995
  const validators = this.validatorRegistry.resolveFromConfig(field.validators);
3688
- group[field.id] = this.fb.control({ value: field.defaultValue ?? null, disabled: field.disabled ?? false }, validators);
3996
+ const asyncValidators = this.validatorRegistry.resolveAsyncFromConfig(field.validators);
3997
+ group[field.id] = this.fb.control({ value: field.defaultValue ?? null, disabled: field.disabled ?? false }, validators, asyncValidators);
3689
3998
  }
3690
3999
  }
3691
4000
  /** One row of an `array` field: a FormGroup when the field declares columns, else a bare control. */
@@ -3716,10 +4025,41 @@ class DynamicFormComponent {
3716
4025
  group.patchValue(item);
3717
4026
  return group;
3718
4027
  }
4028
+ /**
4029
+ * Mark every control touched, then re-check the field components.
4030
+ *
4031
+ * They are OnPush and `touched` is flipped from outside their own template, so the second
4032
+ * half is what makes the error messages actually appear.
4033
+ */
4034
+ markAllTouched() {
4035
+ this.form.markAllAsTouched();
4036
+ this.fieldHosts?.forEach(host => host.refresh());
4037
+ }
4038
+ /**
4039
+ * Bring a record up to the config's `version` before it reaches the form.
4040
+ *
4041
+ * Done here because this is the one place a record enters the renderer, so a consumer
4042
+ * cannot forget it. Migration is a no-op unless the consumer registered steps *and* the
4043
+ * record is behind — an unstamped record is deliberately left alone (see `migrateRecord`).
4044
+ *
4045
+ * A missing or broken step throws from core. That is not swallowed: rendering a
4046
+ * half-understood record would write it back in a shape matching neither schema.
4047
+ */
4048
+ upgradeRecord(data) {
4049
+ if (!this.migrations.length || !this.config)
4050
+ return data;
4051
+ const result = migrateRecord(data, this.config, this.migrations);
4052
+ if (result.applied.length && isDevMode()) {
4053
+ console.info(`[ngx-dynamic-entity] Migrated a "${this.config.entity}" record from config version ` +
4054
+ `${result.from} to ${result.to} (steps: ${result.applied.join(', ')}).`);
4055
+ }
4056
+ return result.record;
4057
+ }
3719
4058
  patchForm(data) {
3720
4059
  if (!data || !this.form)
3721
4060
  return;
3722
4061
  const fieldsById = new Map(this.allFields().map(f => [f.id, f]));
4062
+ const patchedFieldIds = new Set();
3723
4063
  const walkTabs = (tabs) => {
3724
4064
  for (const tab of tabs) {
3725
4065
  const tabData = getTabData(tab.id, data, this.config);
@@ -3744,13 +4084,57 @@ class DynamicFormComponent {
3744
4084
  else {
3745
4085
  ctrl.patchValue(val, { emitEvent: false });
3746
4086
  }
4087
+ patchedFieldIds.add(field.id);
3747
4088
  }
3748
4089
  if (tab.children)
3749
4090
  walkTabs(tab.children);
3750
4091
  }
3751
4092
  };
3752
4093
  walkTabs(this.config.tabs || []);
4094
+ this.warnUnconsumedInitialData(data, fieldsById, patchedFieldIds);
3753
4095
  }
4096
+ /**
4097
+ * A record is nested by tab id (`{ tabId: { fieldId: value } }`) unless the tab sets
4098
+ * `flatData: true`. Handing a flat record to a nested tab finds nothing, so the fields
4099
+ * stay empty — with no error and no clue. That silent miss is the most expensive way to
4100
+ * lose an hour with this library, so name it in dev builds.
4101
+ *
4102
+ * Only top-level keys that match a known field id are reported: anything else is assumed
4103
+ * to be the consumer's own record metadata (ids, timestamps, `_configVersion`) and is not
4104
+ * our business.
4105
+ */
4106
+ warnUnconsumedInitialData(data, fieldsById, patchedFieldIds) {
4107
+ if (!isDevMode() || !this.config)
4108
+ return;
4109
+ const tabIds = new Set();
4110
+ const collectTabIds = (tabs) => {
4111
+ for (const tab of tabs) {
4112
+ tabIds.add(tab.id);
4113
+ if (tab.children)
4114
+ collectTabIds(tab.children);
4115
+ }
4116
+ };
4117
+ collectTabIds(this.config.tabs || []);
4118
+ const unconsumed = Object.keys(data).filter(key => !tabIds.has(key) && // a tab id at the root is the nested container, not a stray field
4119
+ data[key] !== undefined &&
4120
+ fieldsById.has(key) &&
4121
+ !patchedFieldIds.has(key) &&
4122
+ !this.warnedUnconsumedKeys.has(key));
4123
+ if (!unconsumed.length)
4124
+ return;
4125
+ for (const key of unconsumed)
4126
+ this.warnedUnconsumedKeys.add(key);
4127
+ const shown = unconsumed.slice(0, 5).join(', ');
4128
+ const more = unconsumed.length > 5 ? ` (+${unconsumed.length - 5} more)` : '';
4129
+ const plural = unconsumed.length === 1;
4130
+ console.warn(`[ngx-dynamic-entity] initialData has top-level ${plural ? 'key' : 'keys'} matching ` +
4131
+ `${plural ? 'a field that was' : 'fields that were'} not populated: ${shown}${more}. ` +
4132
+ `A record is nested by tab id ({ tabId: { fieldId: value } }) unless the tab sets ` +
4133
+ `flatData: true. Either nest the value under its tab id, or set flatData: true on ` +
4134
+ `the tab holding ${plural ? 'that field' : 'those fields'}.`);
4135
+ }
4136
+ /** Warn once per key per form instance — patchForm re-runs on every initialData change. */
4137
+ warnedUnconsumedKeys = new Set();
3754
4138
  /** Assemble full nested record from per-tab FormGroups, respecting flatData, refererField & arrays. */
3755
4139
  extractRecord() {
3756
4140
  if (!this.form || !this.config)
@@ -3820,6 +4204,10 @@ class DynamicFormComponent {
3820
4204
  const control = this.resolveTargetControl(autoPatch, targetId);
3821
4205
  control?.patchValue(value);
3822
4206
  }
4207
+ // Readonly targets interpolate `control.value` under OnPush. `detectChanges` on the
4208
+ // hosted component flushes that write immediately — `markForCheck` waits for a later
4209
+ // tick that a Playwright selection never schedules.
4210
+ this.fieldHosts?.forEach(host => host.refresh());
3823
4211
  }
3824
4212
  /**
3825
4213
  * `patchOnTrue` — when a boolean/checkbox field transitions to `true`,
@@ -3849,6 +4237,57 @@ class DynamicFormComponent {
3849
4237
  return this.getControl(targetId, autoPatch.targetTab);
3850
4238
  }
3851
4239
  // ─── Helpers ──────────────────────────────────────────────────────────────
4240
+ /**
4241
+ * The fields the render filter operates on — a tab's own fields, at every tab depth.
4242
+ * Deliberately not `allFields()`: `group` and `array` children are shown or hidden with
4243
+ * their parent, and toggling a child's state independently would fight the parent's.
4244
+ */
4245
+ tabFields() {
4246
+ const out = [];
4247
+ const walk = (tabs) => {
4248
+ for (const tab of tabs ?? []) {
4249
+ out.push(...(tab.fields ?? []));
4250
+ walk(tab.children);
4251
+ }
4252
+ };
4253
+ walk(this.config?.tabs);
4254
+ return out;
4255
+ }
4256
+ /**
4257
+ * Keep a hidden field's control out of form validity.
4258
+ *
4259
+ * A field hidden by a rule or a `showWhen` condition is filtered out of the render, but
4260
+ * its validators stay attached — so a hidden *required* field holds `form.invalid` true
4261
+ * forever, permanently disabling Save with nothing on screen to explain why.
4262
+ *
4263
+ * Disabling is the fix rather than stripping validators: Angular excludes disabled
4264
+ * controls from validity, and the field's own validators survive intact for when it
4265
+ * comes back. Values are untouched — `flattenFormValues` and `extractRecord` both read
4266
+ * `control.value`, which a disabled control still carries, so rule evaluation and the
4267
+ * emitted record see exactly what they saw before. That also keeps this from feeding
4268
+ * back on itself: hiding a field cannot change the values the rules are evaluated over.
4269
+ *
4270
+ * The visibility predicate is the same one `fieldsForActiveTab` filters with, so what is
4271
+ * rendered and what counts toward validity cannot drift apart.
4272
+ */
4273
+ syncHiddenFieldState(values) {
4274
+ if (this.preview)
4275
+ return; // preview freezes the whole form on purpose
4276
+ const hiddenByRule = new Set(this.ruleResult().hiddenFields);
4277
+ for (const field of this.tabFields()) {
4278
+ const ctrl = this.getControl(field.id);
4279
+ if (!ctrl)
4280
+ continue;
4281
+ const hidden = hiddenByRule.has(field.id) || !evaluateFieldVisibility(field, values);
4282
+ if (hidden) {
4283
+ if (ctrl.enabled)
4284
+ ctrl.disable({ emitEvent: false });
4285
+ }
4286
+ else if (ctrl.disabled && !field.disabled) {
4287
+ ctrl.enable({ emitEvent: false });
4288
+ }
4289
+ }
4290
+ }
3852
4291
  /** Every field in the config, including `group`/`array` children. */
3853
4292
  allFields() {
3854
4293
  const out = [];
@@ -3914,24 +4353,70 @@ class DynamicFormComponent {
3914
4353
  };
3915
4354
  return findInGroup(this.form);
3916
4355
  }
4356
+ /**
4357
+ * Move focus into the newly shown panel.
4358
+ *
4359
+ * Activating a tab replaces everything below it while focus stays on the tab button, so a
4360
+ * screen-reader or keyboard user is given no indication that the content changed and must
4361
+ * tab back through the strip to reach it. Deferred a frame because the panel's contents
4362
+ * are rendered by the change detection this call is part of.
4363
+ */
4364
+ focusActivePanel() {
4365
+ if (typeof requestAnimationFrame !== 'function')
4366
+ return;
4367
+ requestAnimationFrame(() => this.formPanel?.nativeElement?.focus?.());
4368
+ }
4369
+ /** Accessible name for the field panel — it is the tabpanel for the active tab. */
4370
+ get activeTabLabel() {
4371
+ const tab = this.activeSubTabConfig ?? this.activeTabConfig;
4372
+ return tab ? this.resolveTabLabel(tab) : '';
4373
+ }
4374
+ /**
4375
+ * Why a referenced field is flagged.
4376
+ *
4377
+ * `hasDrift` was written by the builder and read by nothing at runtime, so a field whose
4378
+ * source definition had changed since it was linked looked entirely normal to the person
4379
+ * filling the form in.
4380
+ */
4381
+ driftHint(field) {
4382
+ const source = field.referencedEntityKey
4383
+ ? `"${field.referencedEntityKey}"`
4384
+ : 'its source entity';
4385
+ return `This field is linked to ${source} and its definition there has changed since it was linked.`;
4386
+ }
3917
4387
  resolveTabLabel(tab) {
3918
4388
  return resolveLabel(tab.label, this.language);
3919
4389
  }
3920
4390
  async submit() {
3921
- if (!this.canSubmit || this.form.invalid) {
3922
- this.form.markAllAsTouched();
4391
+ if (!this.canSubmit || this.submitBlocked) {
4392
+ this.markAllTouched();
3923
4393
  return;
3924
4394
  }
3925
4395
  const rawData = this.extractRecord();
3926
4396
  let processedData = rawData;
3927
- const hookKey = `${this.config?.entity}:beforeSave`;
3928
- if (this.hookRegistry.has(hookKey)) {
3929
- processedData = await this.hookRegistry.run(hookKey, processedData);
3930
- }
3931
4397
  this.isSaving.set(true);
3932
4398
  try {
4399
+ // beforeSave can now abort the save: returning false, or throwing, stops it. Previously
4400
+ // its return value simply replaced the payload and the submit proceeded regardless, so
4401
+ // a hook that wanted to veto — a server-side check, a confirmation — had no way to.
4402
+ const hookKey = `${this.config?.entity}:beforeSave`;
4403
+ if (this.hookRegistry.has(hookKey)) {
4404
+ const result = await this.hookRegistry.run(hookKey, processedData);
4405
+ if (result === false) {
4406
+ this.saveRejected.emit({ reason: 'beforeSave returned false' });
4407
+ return;
4408
+ }
4409
+ // Anything else is the payload to submit; `undefined` means "unchanged".
4410
+ if (result !== undefined && result !== true)
4411
+ processedData = result;
4412
+ }
3933
4413
  this.formSubmit.emit(processedData);
3934
4414
  }
4415
+ catch (err) {
4416
+ // A hook that throws aborts the save and reports why, rather than the error escaping
4417
+ // into an unhandled rejection with the form left looking as though it saved.
4418
+ this.saveRejected.emit({ reason: err instanceof Error ? err.message : String(err), error: err });
4419
+ }
3935
4420
  finally {
3936
4421
  this.isSaving.set(false);
3937
4422
  }
@@ -3947,11 +4432,11 @@ class DynamicFormComponent {
3947
4432
  this.formReset.emit();
3948
4433
  }
3949
4434
  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"] }] });
4435
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: DynamicFormComponent, isStandalone: true, selector: "ngx-dynamic-form", inputs: { config: "config", rules: "rules", initialData: "initialData", originalData: "originalData", userRoles: "userRoles", language: "language", readonly: "readonly", readOnlyFields: "readOnlyFields", showInfoBanners: "showInfoBanners", loading: "loading", error: "error", changeDebounceMs: "changeDebounceMs", preview: "preview" }, outputs: { formSubmit: "formSubmit", formChange: "formChange", formReset: "formReset", activeTabChange: "activeTabChange", saveRejected: "saveRejected" }, host: { listeners: { "window:keydown": "handleKeyboardEvent($event)" } }, providers: [EntityRefSelectionService], viewQueries: [{ propertyName: "formPanel", first: true, predicate: ["formPanel"], descendants: true }, { propertyName: "fieldHosts", predicate: DynamicFieldComponent, descendants: true }], usesOnChanges: true, ngImport: i0, template: "@if (canView) {\n <form [formGroup]=\"form\" (ngSubmit)=\"submit()\" class=\"ngx-form\" novalidate>\n\n <!-- Error message banner -->\n @if (error) {\n <div class=\"ngx-form__error-banner\" role=\"alert\" data-testid=\"form-error\">{{ error }}</div>\n }\n\n <!-- Rules Info Banners -->\n @for (infoKey of showInfoBanners ? Object.keys(ruleResult().infoBanners) : []; track infoKey) {\n <div class=\"ngx-form__info-banner\" role=\"status\" [attr.data-testid]=\"'info-banner-' + infoKey\">\n \u2139\uFE0F {{ ruleResult().infoBanners[infoKey] }}\n </div>\n }\n\n <!-- Rules Validation Errors & Warnings -->\n @for (errKey of Object.keys(ruleResult().validationErrors); track errKey) {\n <div class=\"ngx-form__error-banner\" role=\"alert\" [attr.data-testid]=\"'rule-error-' + errKey\">\n \u26A0\uFE0F {{ ruleResult().validationErrors[errKey] }}\n </div>\n }\n @for (warnKey of Object.keys(ruleResult().validationWarnings); track warnKey) {\n <div class=\"ngx-form__warning-banner\" role=\"status\" [attr.data-testid]=\"'rule-warning-' + warnKey\">\n \u26A1 {{ ruleResult().validationWarnings[warnKey] }}\n </div>\n }\n\n <!-- Deferred VALUE_CHANGED notice for criticalField edits (vs session baseline) -->\n @if (changedCriticalFields().length > 0) {\n <div class=\"ngx-form__warning-banner\" role=\"status\" data-testid=\"critical-change-banner\">\n \uD83D\uDD12 Critical field changed:\n @for (field of changedCriticalFields(); track field.id) {\n <span>{{ resolveFieldLabel(field) }}{{ $last ? '' : ', ' }}</span>\n }\n \u2014 this differs from the value at the start of this session.\n </div>\n }\n\n <!-- Primary Tab bar \u2014 only if visible tabs defined -->\n @if (visibleTabs.length > 0) {\n <div class=\"ngx-form__tabs\" role=\"tablist\" data-testid=\"tab-strip\">\n @for (tab of visibleTabs; track tab.id) {\n <button\n type=\"button\"\n class=\"ngx-form__tab\"\n [attr.data-testid]=\"'tab-' + tab.id\"\n [class.ngx-form__tab--active]=\"activeTab() === tab.id\"\n (click)=\"setActiveTab(tab.id)\"\n role=\"tab\"\n [attr.aria-selected]=\"activeTab() === tab.id\"\n >\n {{ resolveTabLabel(tab) }}\n </button>\n }\n </div>\n }\n\n <!-- Sub-tab bar \u2014 only if active tab has sub-tabs -->\n @if (visibleSubTabs.length > 0) {\n <div class=\"ngx-form__tabs ngx-form__tabs--sub\" role=\"tablist\" data-testid=\"subtab-strip\" style=\"margin-top: -12px; margin-bottom: 12px; font-size: 13px;\">\n @for (subTab of visibleSubTabs; track subTab.id) {\n <button\n type=\"button\"\n class=\"ngx-form__tab ngx-form__tab--sub\"\n [attr.data-testid]=\"'subtab-' + subTab.id\"\n [class.ngx-form__tab--active]=\"(activeSubTabConfig?.id) === subTab.id\"\n (click)=\"setActiveSubTab(subTab.id)\"\n role=\"tab\"\n [attr.aria-selected]=\"(activeSubTabConfig?.id) === subTab.id\"\n >\n {{ resolveTabLabel(subTab) }}\n </button>\n }\n </div>\n }\n\n <!--\n One tabpanel per tab, wrapping both branches. Focus moves here on a tab switch, and a\n module tab is still a tab \u2014 putting the panel semantics on only the fields branch left\n a module tab with nowhere for focus to land.\n -->\n <div\n role=\"tabpanel\"\n tabindex=\"-1\"\n #formPanel\n class=\"ngx-form__tabpanel\"\n [attr.aria-label]=\"activeTabLabel\"\n >\n @if (activeTabModuleComponent) {\n <div class=\"ngx-form__module-panel\" data-testid=\"module-panel\">\n <ng-container *ngComponentOutlet=\"activeTabModuleComponent; inputs: (activeSubTabConfig || activeTabConfig)?.moduleInputs\"></ng-container>\n </div>\n } @else {\n <!-- Fields for active tab (Responsive 12-column grid) -->\n <div class=\"ngx-form__panel\" data-testid=\"form-panel\">\n @for (field of fieldsForActiveTab; track field.id) {\n <div\n class=\"ngx-form__field-slot\"\n [style.gridColumn]=\"getFieldSpan(field)\"\n [attr.id]=\"'field-container-' + field.id\"\n >\n <ngx-dynamic-field\n [field]=\"field\"\n [control]=\"getControl(field.id, (activeSubTabConfig || activeTabConfig)?.id)!\"\n [config]=\"config\"\n [currentTabId]=\"(activeSubTabConfig || activeTabConfig)?.id\"\n [language]=\"language\"\n [readonly]=\"isFieldReadonly(field)\"\n [userRoles]=\"userRoles\"\n />\n @if (field.hasDrift) {\n <!--\n Drift was previously visible only in the builder, so a referenced field whose\n source had changed looked completely normal to the person filling the form in.\n role=\"status\" rather than \"alert\": it is information, not an error.\n -->\n <span\n class=\"ngx-field__drift\"\n role=\"status\"\n [attr.data-testid]=\"'field-' + field.id + '-drift'\"\n [title]=\"driftHint(field)\"\n >\u26A0 {{ driftHint(field) }}</span>\n }\n @if (field.criticalField && !readonly) {\n <button\n type=\"button\"\n class=\"ngx-form__lock\"\n [attr.data-testid]=\"'lock-' + field.id\"\n [attr.aria-pressed]=\"!isFieldLocked(field)\"\n [attr.aria-label]=\"\n (isFieldLocked(field) ? 'Unlock ' : 'Lock ') + resolveFieldLabel(field)\n \"\n [title]=\"\n isFieldLocked(field)\n ? 'Critical field \u2014 click to unlock for editing'\n : 'Critical field \u2014 click to lock'\n \"\n (click)=\"toggleFieldLock(field)\"\n >\n {{ isFieldLocked(field) ? '\uD83D\uDD12' : '\uD83D\uDD13' }}\n </button>\n }\n </div>\n }\n </div>\n }\n </div>\n\n <!-- Submit button \u2014 gated by RBAC and isSaving lock -->\n @if (!readonly && permissions.canEdit) {\n <div class=\"ngx-form__actions\" data-testid=\"form-actions\">\n <button\n type=\"button\"\n class=\"ngx-form__reset\"\n data-testid=\"form-reset\"\n (click)=\"reset()\"\n [disabled]=\"isSaving()\"\n >\n Reset\n </button>\n <button\n type=\"submit\"\n class=\"ngx-form__submit\"\n data-testid=\"form-submit\"\n [disabled]=\"submitBlocked\"\n title=\"Shortcut: Ctrl+S\"\n >\n @if (isSaving() || loading) {\n Saving\u2026\n } @else {\n Save\n }\n </button>\n </div>\n }\n\n </form>\n} @else {\n <!-- permissions.view denied: render nothing of the record.\n Presentational only \u2014 the server must not send data this user may not see. -->\n <div class=\"ngx-form__denied\" role=\"status\" data-testid=\"form-access-denied\">\n You do not have permission to view this record.\n </div>\n}\n", styles: [".ngx-form__panel{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:16px 20px;align-items:start}.ngx-form__field-slot{display:flex;align-items:flex-start;gap:6px;min-width:0}.ngx-form__field-slot ngx-dynamic-field{flex:1;min-width:0}.ngx-form__lock{flex:0 0 auto;margin-top:22px;background:none;border:1px solid transparent;border-radius:6px;cursor:pointer;font-size:14px;line-height:1;padding:4px 6px}.ngx-form__lock:hover{border-color:#d1d5db;background:#f9fafb}@media (max-width: 768px){.ngx-form__panel{grid-template-columns:1fr}.ngx-form__field-slot{grid-column:span 12!important}}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: DynamicFieldComponent, selector: "ngx-dynamic-field", inputs: ["field", "control", "config", "currentTabId", "language", "readonly", "userRoles"] }, { kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletContent", "ngComponentOutletNgModule", "ngComponentOutletNgModuleFactory"] }] });
3951
4436
  }
3952
4437
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: DynamicFormComponent, decorators: [{
3953
4438
  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"] }]
4439
+ args: [{ selector: 'ngx-dynamic-form', standalone: true, imports: [ReactiveFormsModule, DynamicFieldComponent, NgComponentOutlet], providers: [EntityRefSelectionService], template: "@if (canView) {\n <form [formGroup]=\"form\" (ngSubmit)=\"submit()\" class=\"ngx-form\" novalidate>\n\n <!-- Error message banner -->\n @if (error) {\n <div class=\"ngx-form__error-banner\" role=\"alert\" data-testid=\"form-error\">{{ error }}</div>\n }\n\n <!-- Rules Info Banners -->\n @for (infoKey of showInfoBanners ? Object.keys(ruleResult().infoBanners) : []; track infoKey) {\n <div class=\"ngx-form__info-banner\" role=\"status\" [attr.data-testid]=\"'info-banner-' + infoKey\">\n \u2139\uFE0F {{ ruleResult().infoBanners[infoKey] }}\n </div>\n }\n\n <!-- Rules Validation Errors & Warnings -->\n @for (errKey of Object.keys(ruleResult().validationErrors); track errKey) {\n <div class=\"ngx-form__error-banner\" role=\"alert\" [attr.data-testid]=\"'rule-error-' + errKey\">\n \u26A0\uFE0F {{ ruleResult().validationErrors[errKey] }}\n </div>\n }\n @for (warnKey of Object.keys(ruleResult().validationWarnings); track warnKey) {\n <div class=\"ngx-form__warning-banner\" role=\"status\" [attr.data-testid]=\"'rule-warning-' + warnKey\">\n \u26A1 {{ ruleResult().validationWarnings[warnKey] }}\n </div>\n }\n\n <!-- Deferred VALUE_CHANGED notice for criticalField edits (vs session baseline) -->\n @if (changedCriticalFields().length > 0) {\n <div class=\"ngx-form__warning-banner\" role=\"status\" data-testid=\"critical-change-banner\">\n \uD83D\uDD12 Critical field changed:\n @for (field of changedCriticalFields(); track field.id) {\n <span>{{ resolveFieldLabel(field) }}{{ $last ? '' : ', ' }}</span>\n }\n \u2014 this differs from the value at the start of this session.\n </div>\n }\n\n <!-- Primary Tab bar \u2014 only if visible tabs defined -->\n @if (visibleTabs.length > 0) {\n <div class=\"ngx-form__tabs\" role=\"tablist\" data-testid=\"tab-strip\">\n @for (tab of visibleTabs; track tab.id) {\n <button\n type=\"button\"\n class=\"ngx-form__tab\"\n [attr.data-testid]=\"'tab-' + tab.id\"\n [class.ngx-form__tab--active]=\"activeTab() === tab.id\"\n (click)=\"setActiveTab(tab.id)\"\n role=\"tab\"\n [attr.aria-selected]=\"activeTab() === tab.id\"\n >\n {{ resolveTabLabel(tab) }}\n </button>\n }\n </div>\n }\n\n <!-- Sub-tab bar \u2014 only if active tab has sub-tabs -->\n @if (visibleSubTabs.length > 0) {\n <div class=\"ngx-form__tabs ngx-form__tabs--sub\" role=\"tablist\" data-testid=\"subtab-strip\" style=\"margin-top: -12px; margin-bottom: 12px; font-size: 13px;\">\n @for (subTab of visibleSubTabs; track subTab.id) {\n <button\n type=\"button\"\n class=\"ngx-form__tab ngx-form__tab--sub\"\n [attr.data-testid]=\"'subtab-' + subTab.id\"\n [class.ngx-form__tab--active]=\"(activeSubTabConfig?.id) === subTab.id\"\n (click)=\"setActiveSubTab(subTab.id)\"\n role=\"tab\"\n [attr.aria-selected]=\"(activeSubTabConfig?.id) === subTab.id\"\n >\n {{ resolveTabLabel(subTab) }}\n </button>\n }\n </div>\n }\n\n <!--\n One tabpanel per tab, wrapping both branches. Focus moves here on a tab switch, and a\n module tab is still a tab \u2014 putting the panel semantics on only the fields branch left\n a module tab with nowhere for focus to land.\n -->\n <div\n role=\"tabpanel\"\n tabindex=\"-1\"\n #formPanel\n class=\"ngx-form__tabpanel\"\n [attr.aria-label]=\"activeTabLabel\"\n >\n @if (activeTabModuleComponent) {\n <div class=\"ngx-form__module-panel\" data-testid=\"module-panel\">\n <ng-container *ngComponentOutlet=\"activeTabModuleComponent; inputs: (activeSubTabConfig || activeTabConfig)?.moduleInputs\"></ng-container>\n </div>\n } @else {\n <!-- Fields for active tab (Responsive 12-column grid) -->\n <div class=\"ngx-form__panel\" data-testid=\"form-panel\">\n @for (field of fieldsForActiveTab; track field.id) {\n <div\n class=\"ngx-form__field-slot\"\n [style.gridColumn]=\"getFieldSpan(field)\"\n [attr.id]=\"'field-container-' + field.id\"\n >\n <ngx-dynamic-field\n [field]=\"field\"\n [control]=\"getControl(field.id, (activeSubTabConfig || activeTabConfig)?.id)!\"\n [config]=\"config\"\n [currentTabId]=\"(activeSubTabConfig || activeTabConfig)?.id\"\n [language]=\"language\"\n [readonly]=\"isFieldReadonly(field)\"\n [userRoles]=\"userRoles\"\n />\n @if (field.hasDrift) {\n <!--\n Drift was previously visible only in the builder, so a referenced field whose\n source had changed looked completely normal to the person filling the form in.\n role=\"status\" rather than \"alert\": it is information, not an error.\n -->\n <span\n class=\"ngx-field__drift\"\n role=\"status\"\n [attr.data-testid]=\"'field-' + field.id + '-drift'\"\n [title]=\"driftHint(field)\"\n >\u26A0 {{ driftHint(field) }}</span>\n }\n @if (field.criticalField && !readonly) {\n <button\n type=\"button\"\n class=\"ngx-form__lock\"\n [attr.data-testid]=\"'lock-' + field.id\"\n [attr.aria-pressed]=\"!isFieldLocked(field)\"\n [attr.aria-label]=\"\n (isFieldLocked(field) ? 'Unlock ' : 'Lock ') + resolveFieldLabel(field)\n \"\n [title]=\"\n isFieldLocked(field)\n ? 'Critical field \u2014 click to unlock for editing'\n : 'Critical field \u2014 click to lock'\n \"\n (click)=\"toggleFieldLock(field)\"\n >\n {{ isFieldLocked(field) ? '\uD83D\uDD12' : '\uD83D\uDD13' }}\n </button>\n }\n </div>\n }\n </div>\n }\n </div>\n\n <!-- Submit button \u2014 gated by RBAC and isSaving lock -->\n @if (!readonly && permissions.canEdit) {\n <div class=\"ngx-form__actions\" data-testid=\"form-actions\">\n <button\n type=\"button\"\n class=\"ngx-form__reset\"\n data-testid=\"form-reset\"\n (click)=\"reset()\"\n [disabled]=\"isSaving()\"\n >\n Reset\n </button>\n <button\n type=\"submit\"\n class=\"ngx-form__submit\"\n data-testid=\"form-submit\"\n [disabled]=\"submitBlocked\"\n title=\"Shortcut: Ctrl+S\"\n >\n @if (isSaving() || loading) {\n Saving\u2026\n } @else {\n Save\n }\n </button>\n </div>\n }\n\n </form>\n} @else {\n <!-- permissions.view denied: render nothing of the record.\n Presentational only \u2014 the server must not send data this user may not see. -->\n <div class=\"ngx-form__denied\" role=\"status\" data-testid=\"form-access-denied\">\n You do not have permission to view this record.\n </div>\n}\n", styles: [".ngx-form__panel{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:16px 20px;align-items:start}.ngx-form__field-slot{display:flex;align-items:flex-start;gap:6px;min-width:0}.ngx-form__field-slot ngx-dynamic-field{flex:1;min-width:0}.ngx-form__lock{flex:0 0 auto;margin-top:22px;background:none;border:1px solid transparent;border-radius:6px;cursor:pointer;font-size:14px;line-height:1;padding:4px 6px}.ngx-form__lock:hover{border-color:#d1d5db;background:#f9fafb}@media (max-width: 768px){.ngx-form__panel{grid-template-columns:1fr}.ngx-form__field-slot{grid-column:span 12!important}}\n"] }]
3955
4440
  }], propDecorators: { config: [{
3956
4441
  type: Input
3957
4442
  }], rules: [{
@@ -3986,11 +4471,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
3986
4471
  type: Output
3987
4472
  }], activeTabChange: [{
3988
4473
  type: Output
4474
+ }], saveRejected: [{
4475
+ type: Output
4476
+ }], formPanel: [{
4477
+ type: ViewChild,
4478
+ args: ['formPanel']
4479
+ }], fieldHosts: [{
4480
+ type: ViewChildren,
4481
+ args: [DynamicFieldComponent]
3989
4482
  }], handleKeyboardEvent: [{
3990
4483
  type: HostListener,
3991
4484
  args: ['window:keydown', ['$event']]
3992
4485
  }] } });
3993
4486
 
4487
+ /**
4488
+ * Distinguishes "no tab holds this field" from "the field holds undefined", so the tab walk
4489
+ * can keep searching in the first case and stop in the second.
4490
+ */
4491
+ const FIELD_NOT_FOUND = Symbol('field-not-found');
3994
4492
  /**
3995
4493
  * DynamicRecordFormComponent — comprehensive tabbed record view & edit component.
3996
4494
  * Supports cross-tab rule evaluation, profile header, summary drawer (showOnMinimize),
@@ -4340,14 +4838,34 @@ class DynamicRecordFormComponent {
4340
4838
  controlFor(fieldId) {
4341
4839
  return this.dynamicFormComp?.getControl(fieldId) ?? null;
4342
4840
  }
4343
- /** A field's current value, read from the record so it is stable during render. */
4841
+ /**
4842
+ * A field's current value, read from the record so it is stable during render.
4843
+ *
4844
+ * Resolved through `getTabData` so it honours exactly the nesting contract the form
4845
+ * patches with — `{ tabId: { fieldId } }`, or the record root when the tab sets
4846
+ * `flatData`. Reading the record flat instead is not a shortcut: it makes the summary
4847
+ * display values the form never loaded, which reads as "the data is there" while every
4848
+ * control behind it is empty.
4849
+ *
4850
+ * Walks sub-tabs as well: a summary, header, or profile field may sit on any tab at any
4851
+ * depth.
4852
+ */
4344
4853
  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;
4854
+ const visit = (tabs = []) => {
4855
+ for (const tab of tabs) {
4856
+ const data = getTabData(tab.id, this.currentData(), this.config);
4857
+ if (data && fieldId in data)
4858
+ return data[fieldId];
4859
+ if (tab.children) {
4860
+ const found = visit(tab.children);
4861
+ if (found !== FIELD_NOT_FOUND)
4862
+ return found;
4863
+ }
4864
+ }
4865
+ return FIELD_NOT_FOUND;
4866
+ };
4867
+ const result = visit(this.config?.tabs);
4868
+ return result === FIELD_NOT_FOUND ? undefined : result;
4351
4869
  }
4352
4870
  isModified = computed(() => {
4353
4871
  const orig = this.originalBaseline();
@@ -4391,7 +4909,7 @@ class DynamicRecordFormComponent {
4391
4909
  }
4392
4910
  /** Summary values render through the shared core formatter, not a local stringifier. */
4393
4911
  formatFieldValue(field) {
4394
- return formatDisplayValue(field.type, field.options, this.currentData()[field.id], this.language);
4912
+ return formatDisplayValue(field.type, field.options, this.fieldValue(field.id), this.language);
4395
4913
  }
4396
4914
  jumpToField(fieldId) {
4397
4915
  // Find parent tab for field
@@ -4411,11 +4929,11 @@ class DynamicRecordFormComponent {
4411
4929
  }
4412
4930
  }
4413
4931
  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"] }] });
4932
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: DynamicRecordFormComponent, isStandalone: true, selector: "ngx-dynamic-record-form", inputs: { config: "config", rules: "rules", initialData: "initialData", userRoles: "userRoles", language: "language", readonly: "readonly", loading: "loading", error: "error", isReadOnly: "isReadOnly", readOnlyFields: "readOnlyFields", viewMode: "viewMode" }, outputs: { formSubmit: "formSubmit", formChange: "formChange", formReset: "formReset", sectionSave: "sectionSave" }, viewQueries: [{ propertyName: "dynamicFormComp", first: true, predicate: DynamicFormComponent, descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"ngx-record-editor\">\n <!-- Record Header -->\n <div class=\"ngx-record-editor__header\">\n <div class=\"ngx-record-editor__title-group\">\n @if (profileImageUrl) {\n <img\n class=\"ngx-record-editor__avatar\"\n data-testid=\"header-profile-image\"\n [src]=\"profileImageUrl\"\n [alt]=\"recordTitle\"\n />\n } @else {\n <div class=\"ngx-record-editor__avatar\" data-testid=\"header-avatar-initial\">\n {{ avatarLetter }}\n </div>\n }\n <div>\n <h2 class=\"ngx-record-editor__title\" data-testid=\"record-title\">\n {{ recordTitle }}\n </h2>\n <span class=\"ngx-record-editor__subtitle\">Entity: {{ config.entity }}</span>\n </div>\n </div>\n\n <!-- Record-level status switch, outside section editing -->\n @if (headerToggleField; as toggleField) {\n <label class=\"ngx-record-editor__header-toggle\">\n <span>{{ formatFieldLabel(toggleField) }}</span>\n <input\n type=\"checkbox\"\n role=\"switch\"\n data-testid=\"header-toggle\"\n [checked]=\"headerToggleValue\"\n [disabled]=\"recordReadOnly\"\n [attr.aria-checked]=\"headerToggleValue\"\n (change)=\"toggleHeaderStatus()\"\n />\n <span class=\"ngx-record-editor__header-toggle-text\">{{ headerToggleValue ? 'Yes' : 'No' }}</span>\n </label>\n }\n </div>\n\n <!-- Dismissible info banners (rules `info` action) -->\n @for (banner of infoBanners; track banner.fieldId) {\n <div\n class=\"ngx-record-editor__banner ngx-record-editor__banner--info\"\n role=\"status\"\n [attr.data-testid]=\"'info-banner-' + banner.fieldId\"\n >\n <span>\u2139\uFE0F {{ banner.message }}</span>\n <button\n type=\"button\"\n class=\"ngx-record-editor__banner-dismiss\"\n [attr.data-testid]=\"'dismiss-' + banner.fieldId\"\n [attr.aria-label]=\"'Dismiss: ' + banner.message\"\n (click)=\"dismissInfoBanner(banner.fieldId)\"\n >\u2715</button>\n </div>\n }\n\n <!-- Modification baseline banner -->\n @if (isModified()) {\n <div class=\"ngx-record-editor__banner ngx-record-editor__banner--warning\">\n \uD83D\uDD12 Record modified from session baseline \u2014 unsaved changes pending.\n </div>\n }\n\n <!-- Summary Panel (showOnMinimize fields with interactive jump) -->\n @if (summaryFields().length > 0) {\n <div class=\"ngx-record-editor__summary-panel\" data-testid=\"summary-panel\">\n @for (sf of summaryFields(); track sf.id) {\n <div\n class=\"ngx-record-editor__summary-item\"\n [attr.data-testid]=\"'summary-' + sf.id\"\n (click)=\"jumpToField(sf.id)\"\n [title]=\"'Click to jump to ' + formatFieldLabel(sf)\"\n >\n <span class=\"ngx-record-editor__summary-label\">{{ formatFieldLabel(sf) }}</span>\n <span class=\"ngx-record-editor__summary-val\">{{ formatFieldValue(sf) }}</span>\n </div>\n }\n </div>\n }\n\n <!-- Section edit bar: one tab is edited at a time, and validated on its own -->\n @if (viewMode && !recordReadOnly) {\n <div class=\"ngx-record-editor__section-bar\">\n @if (isEditingActiveTab) {\n <span class=\"ngx-record-editor__section-hint\">Editing this section</span>\n <button type=\"button\" class=\"ngx-form__reset\" data-testid=\"cancel-section\" (click)=\"cancelSection()\">\n Cancel\n </button>\n <button type=\"button\" class=\"ngx-form__submit\" data-testid=\"save-section\" (click)=\"saveSection()\">\n Save section\n </button>\n } @else {\n <button type=\"button\" class=\"ngx-form__reset\" data-testid=\"edit-section\" (click)=\"editSection()\">\n Edit section\n </button>\n }\n </div>\n }\n\n @if (Object.keys(sectionErrors()).length > 0) {\n <div class=\"ngx-record-editor__banner ngx-record-editor__banner--error\" role=\"alert\" data-testid=\"section-errors\">\n @for (key of Object.keys(sectionErrors()); track key) {\n <div>\u26A0\uFE0F {{ sectionErrors()[key] }}</div>\n }\n </div>\n }\n\n <!-- Array rows: listed here, edited in the drawer below -->\n @for (arrayField of arrayFieldsForActiveTab; track arrayField.id) {\n <div class=\"ngx-record-editor__rows\" [attr.data-testid]=\"'rows-' + arrayField.id\">\n <div class=\"ngx-record-editor__rows-head\">\n <span class=\"ngx-record-editor__summary-label\">{{ formatFieldLabel(arrayField) }}</span>\n @if (!sectionReadOnly) {\n <button\n type=\"button\"\n class=\"ngx-form__reset\"\n [attr.data-testid]=\"'add-row-' + arrayField.id\"\n (click)=\"openAddRow(arrayField)\"\n >+ Row</button>\n }\n </div>\n\n @for (row of rowsOf(arrayField); track $index) {\n <div class=\"ngx-record-editor__row\" [attr.data-testid]=\"'row-' + arrayField.id + '-' + $index\">\n <span class=\"ngx-record-editor__row-text\">{{ rowSummary(arrayField, row) }}</span>\n @if (!sectionReadOnly) {\n <button type=\"button\" class=\"ngx-record-editor__row-btn\"\n [attr.data-testid]=\"'edit-row-' + arrayField.id + '-' + $index\"\n (click)=\"openEditRow(arrayField, $index)\">Edit</button>\n <button type=\"button\" class=\"ngx-record-editor__row-btn\"\n [attr.data-testid]=\"'delete-row-' + arrayField.id + '-' + $index\"\n (click)=\"deleteRow(arrayField, $index)\">Delete</button>\n }\n </div>\n } @empty {\n <p class=\"ngx-record-editor__row-empty\">No rows yet.</p>\n }\n </div>\n }\n\n <!--\n Row drawer. Deliberately outside the tab panel: building a row's FormGroup inside the\n tab view races the tab's own initialisation (the reference hit this with FormArray).\n -->\n @if (inlineRowField(); as rowField) {\n <div class=\"ngx-record-editor__drawer\" data-testid=\"row-drawer\" [formGroup]=\"inlineRowForm!\">\n <div class=\"ngx-record-editor__rows-head\">\n <strong>{{ inlineRowIndex() === null ? 'Add' : 'Edit' }} {{ formatFieldLabel(rowField) }} row</strong>\n </div>\n\n <div class=\"ngx-form__panel\">\n @for (child of rowField.children ?? []; track child.id) {\n <ngx-dynamic-field\n [field]=\"child\"\n [control]=\"inlineRowForm!.get(child.id)!\"\n [config]=\"config\"\n [language]=\"language\"\n [readonly]=\"false\"\n [userRoles]=\"userRoles\"\n [style.gridColumn]=\"'span 12'\"\n />\n }\n </div>\n\n <div class=\"ngx-record-editor__section-bar\">\n <button type=\"button\" class=\"ngx-form__reset\" data-testid=\"cancel-row\" (click)=\"cancelRow()\">Cancel</button>\n <button type=\"button\" class=\"ngx-form__submit\" data-testid=\"save-row\" (click)=\"saveRow()\">Save row</button>\n </div>\n </div>\n }\n\n <!-- Main Dynamic Form -->\n <ngx-dynamic-form\n [config]=\"config\"\n [rules]=\"rules\"\n [initialData]=\"initialData\"\n [originalData]=\"originalBaseline()\"\n [userRoles]=\"userRoles\"\n [language]=\"language\"\n [readonly]=\"sectionReadOnly\"\n [readOnlyFields]=\"readOnlyFields\"\n [showInfoBanners]=\"false\"\n [loading]=\"loading\"\n [error]=\"error\"\n (formChange)=\"onFormChange($event)\"\n (formSubmit)=\"onFormSubmit($event)\"\n (formReset)=\"onFormReset()\"\n (activeTabChange)=\"onActiveTabChange($event)\"\n />\n</div>\n", styles: [".ngx-record-editor{display:flex;flex-direction:column;gap:16px;background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:20px;box-shadow:0 4px 6px -1px #0000000d}.ngx-record-editor__header{display:flex;align-items:center;justify-content:space-between;padding-bottom:16px;border-bottom:1px solid #f3f4f6}.ngx-record-editor__title-group{display:flex;align-items:center;gap:12px}.ngx-record-editor__avatar{width:48px;height:48px;border-radius:50%;object-fit:cover;background:#e0e7ff;color:#4f46e5;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:18px}.ngx-record-editor__title{font-size:20px;font-weight:700;color:#111827;margin:0}.ngx-record-editor__subtitle{font-size:13px;color:#6b7280}.ngx-record-editor__header-toggle{display:flex;align-items:center;gap:8px;font-size:13px;color:#374151}.ngx-record-editor__header-toggle-text{font-weight:600}.ngx-record-editor__banner{padding:10px 14px;border-radius:8px;font-size:13px;font-weight:500}.ngx-record-editor__banner--info{background:#eff6ff;border:1px solid #dbeafe;color:#1d4ed8;display:flex;align-items:center;justify-content:space-between;gap:12px}.ngx-record-editor__banner-dismiss{background:none;border:none;cursor:pointer;color:inherit;font-size:14px;line-height:1;padding:2px 6px;border-radius:4px}.ngx-record-editor__banner-dismiss:hover{background:#0000000f}.ngx-record-editor__banner--error{background:#fef2f2;border:1px solid #fecaca;color:#b91c1c}.ngx-record-editor__section-bar{display:flex;align-items:center;justify-content:flex-end;gap:8px}.ngx-record-editor__section-hint{margin-right:auto;font-size:12px;color:#6b7280}.ngx-record-editor__banner--warning{background:#fffbeb;border:1px solid #fef3c7;color:#b45309}.ngx-record-editor__rows{border:1px solid #f3f4f6;border-radius:8px;padding:12px 14px;display:flex;flex-direction:column;gap:6px}.ngx-record-editor__rows-head{display:flex;align-items:center;justify-content:space-between;gap:8px}.ngx-record-editor__row{display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:6px;background:#f9fafb}.ngx-record-editor__row-text{flex:1;font-size:13px;overflow-wrap:anywhere}.ngx-record-editor__row-btn{background:none;border:1px solid #e5e7eb;border-radius:6px;cursor:pointer;font-size:12px;padding:2px 8px}.ngx-record-editor__row-empty{font-size:12px;color:#6b7280;margin:0}.ngx-record-editor__drawer{border:1px solid #dbeafe;background:#f8fafc;border-radius:8px;padding:14px;display:flex;flex-direction:column;gap:12px}.ngx-record-editor__summary-panel{background:#f9fafb;border:1px solid #f3f4f6;border-radius:8px;padding:12px 16px;display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px}.ngx-record-editor__summary-item{display:flex;flex-direction:column;cursor:pointer;padding:6px 10px;border-radius:6px;transition:background .15s}.ngx-record-editor__summary-item:hover{background:#eff6ff}.ngx-record-editor__summary-label{font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:#6b7280;font-weight:600}.ngx-record-editor__summary-val{font-size:14px;font-weight:600;color:#1f2937}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: DynamicFormComponent, selector: "ngx-dynamic-form", inputs: ["config", "rules", "initialData", "originalData", "userRoles", "language", "readonly", "readOnlyFields", "showInfoBanners", "loading", "error", "changeDebounceMs", "preview"], outputs: ["formSubmit", "formChange", "formReset", "activeTabChange", "saveRejected"] }, { kind: "component", type: DynamicFieldComponent, selector: "ngx-dynamic-field", inputs: ["field", "control", "config", "currentTabId", "language", "readonly", "userRoles"] }] });
4415
4933
  }
4416
4934
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: DynamicRecordFormComponent, decorators: [{
4417
4935
  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"] }]
4936
+ 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
4937
  }], propDecorators: { config: [{
4420
4938
  type: Input
4421
4939
  }], rules: [{
@@ -4457,5 +4975,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
4457
4975
  * Generated bundle index. Do not edit.
4458
4976
  */
4459
4977
 
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 };
4978
+ export { ASYNC_VALIDATOR_REGISTRY, ArrayFieldComponent, BooleanFieldComponent, COMMON_MODULES_REGISTRY, CONFIG_SOURCE, CascadeDataService, CheckboxFieldComponent, ConfigSourceService, CurrencyFieldComponent, DEFAULT_VALIDATION_MESSAGES, DateFieldComponent, DropdownFieldComponent, DynamicFieldComponent, DynamicFormComponent, DynamicRecordFormComponent, ENTITY_REF_CACHE_STORE, ENTITY_REF_REGISTRY, EmailFieldComponent, EntityRefFieldComponent, EntityRefRegistryService, EntityRefSelectionService, EntityReferenceService, FIELD_TYPE_REGISTRY, FIELD_TYPE_SETS, FieldRegistryService, FileFieldComponent, GroupFieldComponent, HOOK_REGISTRY, HookRegistryService, ImageFieldComponent, InMemoryEntityRefCacheStore, LOOKUP_REGISTRY, LookupRegistryService, MASKED_ROLES, MonthYearFieldComponent, MultiSelectFieldComponent, NumberFieldComponent, PasswordFieldComponent, RECORD_MIGRATIONS, RadioFieldComponent, RbacService, RulesEvaluationService, SYSTEM_DEFAULT_CAN_EDIT, TextFieldComponent, TextareaFieldComponent, UPLOAD_HANDLER, VALIDATION_MESSAGES, VALIDATOR_REGISTRY, ValidationMessagesService, ValidatorRegistryService, builtInFieldTypes, provideBuiltInFieldTypes, provideFieldTypes, provideNgxDynamicEntity, refreshChoiceOptions };
4461
4979
  //# sourceMappingURL=ngx-dynamic-entity.mjs.map