ngx-dynamic-entity 2.1.0 → 2.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.
@@ -2,7 +2,7 @@ import * as i0 from '@angular/core';
2
2
  import { InjectionToken, makeEnvironmentProviders, inject, Injectable, signal, Input, ChangeDetectionStrategy, Component, ChangeDetectorRef, DestroyRef, Injector, isDevMode, ViewContainerRef, ViewChild, forwardRef, Optional, Inject, EventEmitter, ElementRef, computed, afterNextRender, HostListener, ViewChildren, Output } from '@angular/core';
3
3
  import * as i1 from '@angular/forms';
4
4
  import { ReactiveFormsModule, Validators, FormBuilder, FormGroup, FormArray, FormsModule } from '@angular/forms';
5
- import { resolveLabel, formatDisplayValue, normalizeLookupValues, lookupValuesToOptions, valuesMatch, resolveOptionLabel, resolveOptionValue, buildReferenceCacheKey, normalizeReferenceOptions, entityKeyFromCacheKey, applyCascadeFilter, resolveEffectiveMask, shouldMaskField, findTab, fileRefName, evaluateFormRules, filterRulesForTab, evaluateFieldVisibility, getTabPath, parseFieldRef, setTabData, setValueByPath, normalizeArrayStructures, getTabData, getValueByPath, normalizeConfigOptions, migrateRecord, ambiguousFieldIds, toRefToken, fieldRefFor, collectFieldScopes, refOf, applyAutoPatch, applyPatchOnTrue, fieldsUnderTab, valuesEqual, parseCsv, deriveImportColumns, suggestMapping, applyMapping, toCsv, stripIndices, CORE_VERSION, coerceCell, buildTemplateSpec, collectLeafTargets, formatConfigProblems } from '@dynamic-entity/core';
5
+ import { resolveLabel, formatDisplayValue, normalizeLookupValues, lookupValuesToOptions, valuesMatch, resolveOptionLabel, resolveOptionValue, buildReferenceCacheKey, normalizeReferenceOptions, entityKeyFromCacheKey, applyCascadeFilter, resolveEffectiveMask, shouldMaskField, findTab, isValidUrl, isValidPhone, fileRefName, sliderBounds, ratingScale, normalizeHexColor, normalizeTags, evaluateFormRules, filterRulesForTab, evaluateFieldVisibility, getTabPath, fieldRefFor, toRefToken, refOf, parseFieldRef, setTabData, setValueByPath, normalizeArrayStructures, getTabData, getValueByPath, normalizeConfigOptions, migrateRecord, ambiguousFieldIds, collectFieldScopes, applyAutoPatch, isUnsafePath, fieldsUnderTab, valuesEqual, parseCsv, deriveImportColumns, suggestMapping, applyMapping, toCsv, stripIndices, CORE_VERSION, coerceCell, buildTemplateSpec, collectLeafTargets, formatConfigProblems } 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';
@@ -196,7 +196,7 @@ const ENTITY_REF_CACHE_STORE = new InjectionToken('ENTITY_REF_CACHE_STORE', {
196
196
  */
197
197
  const SYSTEM_DEFAULT_CAN_EDIT = new InjectionToken('SYSTEM_DEFAULT_CAN_EDIT');
198
198
  /**
199
- * Registry/Resolver for resolving an EntityFormConfig by entity key across entities (Phase 8).
199
+ * Registry/Resolver for resolving an EntityFormConfig by entity key across entities.
200
200
  * Enables cross-entity field referencing (`isReferenced`, `referencedEntityKey`, `referencedFieldId`, `hasDrift`).
201
201
  */
202
202
  const CONFIG_SOURCE = new InjectionToken('CONFIG_SOURCE');
@@ -224,8 +224,7 @@ const MARKDOWN_RENDERER = new InjectionToken('MARKDOWN_RENDERER');
224
224
  *
225
225
  * Masking is presentational — see SECURITY.md — and what it prints is a product decision:
226
226
  * `••••••••` reads as a redaction, "Hidden" reads as a permission, and a localized string
227
- * reads as neither in English. It was a literal repeated across every field component, so
228
- * there was no way to choose.
227
+ * reads as neither in English, so it is the host's to choose.
229
228
  *
230
229
  * @example
231
230
  * { provide: MASKED_PLACEHOLDER, useValue: '••••••••' }
@@ -303,6 +302,10 @@ const DEFAULT_VALIDATION_MESSAGES = {
303
302
  required: 'This field is required.',
304
303
  requiredSelection: 'Please select an option.',
305
304
  email: 'Please enter a valid email address.',
305
+ url: 'Please enter a full web address, starting with https://.',
306
+ phone: 'Please enter a valid phone number.',
307
+ minItems: (_lang, err) => `At least ${err?.requiredLength} required.`,
308
+ maxItems: (_lang, err) => `No more than ${err?.requiredLength} allowed.`,
306
309
  pattern: 'Invalid format.',
307
310
  minlength: (_lang, err) => `Minimum ${err?.requiredLength} characters required.`,
308
311
  maxlength: (_lang, err) => `Maximum ${err?.requiredLength} characters allowed.`,
@@ -319,18 +322,35 @@ const DEFAULT_VALIDATION_MESSAGES = {
319
322
  * first error actually on the control — so the shared list costs nothing and spares every
320
323
  * field component a near-identical copy of it.
321
324
  */
322
- const COMMON_ORDER = ['required', 'email', 'min', 'max', 'minlength', 'maxlength', 'pattern'];
325
+ const COMMON_ORDER = [
326
+ 'required',
327
+ 'email',
328
+ 'url',
329
+ 'phone',
330
+ 'min',
331
+ 'max',
332
+ 'minlength',
333
+ 'maxlength',
334
+ 'pattern',
335
+ ];
323
336
  /**
324
337
  * Where a field type disagrees with the common list.
325
338
  *
326
- * Only three do, and each for a reason about how the control reads rather than what it can
327
- * raise: a select says "Please select an option" where an input says "This field is
328
- * required"; a number that is neither required nor out of range is not "invalid", it is not a
329
- * number; and a control with no text in it cannot fail a length or format rule.
339
+ * Each for a reason about how the control reads rather than what it can raise: a select says
340
+ * "Please select an option" where an input says "This field is required"; a number that is
341
+ * neither required nor out of range is not "invalid", it is not a number; a control with no
342
+ * text in it cannot fail a length or format rule; and a `tags` list is counted in items, not
343
+ * characters, although Angular reports both as `minlength`.
330
344
  */
331
345
  const ORDER_BY_FIELD_TYPE = {
332
346
  dropdown: { order: [['required', 'requiredSelection']], fallback: 'invalidSelection' },
333
347
  number: { order: ['required', 'min', 'max'], fallback: 'invalidNumber' },
348
+ slider: { order: ['required', 'min', 'max'], fallback: 'invalid' },
349
+ rating: { order: ['required', 'min', 'max'], fallback: 'invalid' },
350
+ tags: {
351
+ order: ['required', ['minlength', 'minItems'], ['maxlength', 'maxItems'], 'pattern'],
352
+ fallback: 'invalid',
353
+ },
334
354
  boolean: { order: ['required', 'pattern'], fallback: 'invalid' },
335
355
  checkbox: { order: ['required', 'pattern'], fallback: 'invalid' },
336
356
  file: { order: ['required', 'pattern'], fallback: 'invalid' },
@@ -363,12 +383,10 @@ class ValidationMessagesService {
363
383
  /**
364
384
  * The message for a field, chosen by its **type** rather than by a list the caller supplies.
365
385
  *
366
- * Every field component used to pass its own copy of the key order, and the copies were
367
- * near-identical — which was tolerable while the field itself was the only thing that
368
- * rendered a message. It stopped being tolerable when the error summary above the form
369
- * started naming what is wrong with each field: the summary has no field component to ask
370
- * (the offending field is usually on a tab that is not rendered), so it would have needed a
371
- * second copy of the same table, and a dropdown would have said "This field is required" in
386
+ * Both the field itself and the error summary above the form render this message, and the
387
+ * summary has no field component to ask (the offending field is usually on a tab that is
388
+ * not rendered). Per-component key orders would drift, and a dropdown would say "This
389
+ * field is required" in
372
390
  * the summary and "Please select an option" under the control.
373
391
  *
374
392
  * One table, read from both places, is what keeps those two sentences the same sentence.
@@ -388,10 +406,10 @@ class ValidationMessagesService {
388
406
  return '';
389
407
  return typeof message === 'function' ? message(language, error) : message;
390
408
  }
391
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: ValidationMessagesService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
392
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: ValidationMessagesService, providedIn: 'root' });
409
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: ValidationMessagesService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
410
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: ValidationMessagesService, providedIn: 'root' });
393
411
  }
394
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: ValidationMessagesService, decorators: [{
412
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: ValidationMessagesService, decorators: [{
395
413
  type: Injectable,
396
414
  args: [{ providedIn: 'root' }]
397
415
  }] });
@@ -399,14 +417,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
399
417
  /**
400
418
  * DOM ids that survive being rendered more than once.
401
419
  *
402
- * A field component used to derive its control's id straight from `field.id`. That is unique
403
- * in a config and *not* unique in a document: an `array` field renders the same child fields
404
- * once per row, so a two-row Contacts array put two `#name` inputs on the page. `<label for>`
405
- * resolves to the first match in document order, so clicking the second row's label focused
406
- * the first row's input, and a screen reader read the same association twice. Duplicate ids on
407
- * focusable elements are also a WCAG failure in their own right.
420
+ * `field.id` is unique in a config and *not* unique in a document: an `array` field renders
421
+ * the same child fields once per row. `<label for>` resolves to the first match, so a second
422
+ * row's label would focus the first row's input — and duplicate ids on focusable elements are
423
+ * a WCAG failure in their own right.
408
424
  *
409
- * The fix is a counter, which is what Angular Material does for the same reason: the id stays
425
+ * The answer is a counter, which is what Angular Material does for the same reason: the id stays
410
426
  * readable — `email-de7` — and is unique for the lifetime of the page. It is deliberately not
411
427
  * derived from the row index, because a field component is given exactly five inputs and none
412
428
  * of them says where it is; adding a sixth would change the contract every custom field type
@@ -506,8 +522,8 @@ class TextFieldComponent {
506
522
  return '';
507
523
  return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
508
524
  }
509
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: TextFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
510
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: TextFieldComponent, isStandalone: true, selector: "ngx-text-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
525
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: TextFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
526
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: TextFieldComponent, isStandalone: true, selector: "ngx-text-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
511
527
  <div
512
528
  class="ngx-field ngx-field--text"
513
529
  [attr.data-testid]="'field-' + field.id"
@@ -567,7 +583,7 @@ class TextFieldComponent {
567
583
  </div>
568
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.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
569
585
  }
570
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: TextFieldComponent, decorators: [{
586
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: TextFieldComponent, decorators: [{
571
587
  type: Component,
572
588
  args: [{
573
589
  changeDetection: ChangeDetectionStrategy.OnPush,
@@ -669,12 +685,6 @@ class TextareaFieldComponent {
669
685
  get placeholder() {
670
686
  return resolveLabel(this.field?.placeholder, this.language);
671
687
  }
672
- /**
673
- * Resolved through `ValidationMessagesService`, so `provideNgxDynamicEntity({
674
- * validationMessages })` reaches this field. It used to render a fixed
675
- * "This field has an error", which made a documented, configurable feature work on three
676
- * of fifteen field types.
677
- */
678
688
  /** Author help text, shown under the control and named by `aria-describedby`. */
679
689
  get hint() {
680
690
  return resolveLabel(this.field?.hint, this.language);
@@ -688,8 +698,8 @@ class TextareaFieldComponent {
688
698
  return '';
689
699
  return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
690
700
  }
691
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: TextareaFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
692
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: TextareaFieldComponent, isStandalone: true, selector: "ngx-textarea-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
701
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: TextareaFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
702
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: TextareaFieldComponent, isStandalone: true, selector: "ngx-textarea-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
693
703
  <div
694
704
  class="ngx-field ngx-field--textarea"
695
705
  [attr.data-testid]="'field-' + field.id"
@@ -738,7 +748,7 @@ class TextareaFieldComponent {
738
748
  </div>
739
749
  `, 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 });
740
750
  }
741
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: TextareaFieldComponent, decorators: [{
751
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: TextareaFieldComponent, decorators: [{
742
752
  type: Component,
743
753
  args: [{
744
754
  changeDetection: ChangeDetectionStrategy.OnPush,
@@ -865,6 +875,11 @@ const DEFAULT_UI_TEXT = {
865
875
  noImage: 'No image',
866
876
  uploadImage: 'Upload',
867
877
  changeImage: 'Change',
878
+ // Rating, colour and tags
879
+ ratingValue: '{value} of {max}',
880
+ clearValue: 'Clear {field}',
881
+ addTagPlaceholder: 'Type and press Enter',
882
+ removeTag: 'Remove {tag}',
868
883
  // Month / year pickers
869
884
  month: 'Month',
870
885
  year: 'Year',
@@ -1007,10 +1022,10 @@ class UiTextService {
1007
1022
  text(key, language = 'en', params) {
1008
1023
  return resolveUiText(this.overrides, DEFAULT_UI_TEXT, key, language, params);
1009
1024
  }
1010
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: UiTextService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1011
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: UiTextService, providedIn: 'root' });
1025
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: UiTextService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1026
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: UiTextService, providedIn: 'root' });
1012
1027
  }
1013
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: UiTextService, decorators: [{
1028
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: UiTextService, decorators: [{
1014
1029
  type: Injectable,
1015
1030
  args: [{ providedIn: 'root' }]
1016
1031
  }] });
@@ -1124,12 +1139,6 @@ class MarkdownFieldComponent {
1124
1139
  this.cached = { source, html };
1125
1140
  return html;
1126
1141
  }
1127
- /**
1128
- * Resolved through `ValidationMessagesService`, so `provideNgxDynamicEntity({
1129
- * validationMessages })` reaches this field. It used to render a fixed
1130
- * "This field has an error", which made a documented, configurable feature work on three
1131
- * of fifteen field types.
1132
- */
1133
1142
  /** Author help text, shown under the control and named by `aria-describedby`. */
1134
1143
  get hint() {
1135
1144
  return resolveLabel(this.field?.hint, this.language);
@@ -1143,8 +1152,8 @@ class MarkdownFieldComponent {
1143
1152
  return '';
1144
1153
  return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
1145
1154
  }
1146
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MarkdownFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1147
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: MarkdownFieldComponent, isStandalone: true, selector: "ngx-markdown-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, usesOnChanges: true, ngImport: i0, template: `
1155
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: MarkdownFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1156
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: MarkdownFieldComponent, isStandalone: true, selector: "ngx-markdown-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, usesOnChanges: true, ngImport: i0, template: `
1148
1157
  <div
1149
1158
  class="ngx-field ngx-field--markdown"
1150
1159
  [attr.data-testid]="'field-' + field.id"
@@ -1244,7 +1253,7 @@ class MarkdownFieldComponent {
1244
1253
  </div>
1245
1254
  `, 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 });
1246
1255
  }
1247
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MarkdownFieldComponent, decorators: [{
1256
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: MarkdownFieldComponent, decorators: [{
1248
1257
  type: Component,
1249
1258
  args: [{
1250
1259
  changeDetection: ChangeDetectionStrategy.OnPush,
@@ -1399,8 +1408,8 @@ class NumberFieldComponent {
1399
1408
  return '';
1400
1409
  return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
1401
1410
  }
1402
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: NumberFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1403
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: NumberFieldComponent, isStandalone: true, selector: "ngx-number-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
1411
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: NumberFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1412
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: NumberFieldComponent, isStandalone: true, selector: "ngx-number-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
1404
1413
  <div
1405
1414
  class="ngx-field ngx-field--number"
1406
1415
  [attr.data-testid]="'field-' + field.id"
@@ -1460,7 +1469,7 @@ class NumberFieldComponent {
1460
1469
  </div>
1461
1470
  `, 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 });
1462
1471
  }
1463
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: NumberFieldComponent, decorators: [{
1472
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: NumberFieldComponent, decorators: [{
1464
1473
  type: Component,
1465
1474
  args: [{
1466
1475
  changeDetection: ChangeDetectionStrategy.OnPush,
@@ -1575,12 +1584,6 @@ class CurrencyFieldComponent {
1575
1584
  return '$';
1576
1585
  }
1577
1586
  }
1578
- /**
1579
- * Resolved through `ValidationMessagesService`, so `provideNgxDynamicEntity({
1580
- * validationMessages })` reaches this field. It used to render a fixed
1581
- * "This field has an error", which made a documented, configurable feature work on three
1582
- * of fifteen field types.
1583
- */
1584
1587
  /** Author help text, shown under the control and named by `aria-describedby`. */
1585
1588
  get hint() {
1586
1589
  return resolveLabel(this.field?.hint, this.language);
@@ -1594,8 +1597,8 @@ class CurrencyFieldComponent {
1594
1597
  return '';
1595
1598
  return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
1596
1599
  }
1597
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: CurrencyFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1598
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: CurrencyFieldComponent, isStandalone: true, selector: "ngx-currency-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
1600
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: CurrencyFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1601
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: CurrencyFieldComponent, isStandalone: true, selector: "ngx-currency-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
1599
1602
  <div
1600
1603
  class="ngx-field ngx-field--currency"
1601
1604
  [attr.data-testid]="'field-' + field.id"
@@ -1650,7 +1653,7 @@ class CurrencyFieldComponent {
1650
1653
  </div>
1651
1654
  `, 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 });
1652
1655
  }
1653
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: CurrencyFieldComponent, decorators: [{
1656
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: CurrencyFieldComponent, decorators: [{
1654
1657
  type: Component,
1655
1658
  args: [{
1656
1659
  changeDetection: ChangeDetectionStrategy.OnPush,
@@ -1748,12 +1751,6 @@ class EmailFieldComponent {
1748
1751
  get placeholder() {
1749
1752
  return resolveLabel(this.field?.placeholder, this.language);
1750
1753
  }
1751
- /**
1752
- * Resolved through `ValidationMessagesService`, so `provideNgxDynamicEntity({
1753
- * validationMessages })` reaches this field. It used to render a fixed
1754
- * "This field has an error", which made a documented, configurable feature work on three
1755
- * of fifteen field types.
1756
- */
1757
1754
  /** Author help text, shown under the control and named by `aria-describedby`. */
1758
1755
  get hint() {
1759
1756
  return resolveLabel(this.field?.hint, this.language);
@@ -1767,8 +1764,8 @@ class EmailFieldComponent {
1767
1764
  return '';
1768
1765
  return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
1769
1766
  }
1770
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: EmailFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1771
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: EmailFieldComponent, isStandalone: true, selector: "ngx-email-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
1767
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: EmailFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1768
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: EmailFieldComponent, isStandalone: true, selector: "ngx-email-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
1772
1769
  <div
1773
1770
  class="ngx-field ngx-field--email"
1774
1771
  [attr.data-testid]="'field-' + field.id"
@@ -1816,10 +1813,7 @@ class EmailFieldComponent {
1816
1813
  [attr.disabled]="field.disabled ? true : null"
1817
1814
  />
1818
1815
  @if (errorMessage) {
1819
- <!-- One branch, resolved by the service. The template used to spell out "Email is
1820
- required" and "Enter a valid email address" itself, so a host configuring
1821
- validationMessages was overridden by markup it could not reach. The service
1822
- defaults say the same thing. -->
1816
+ <!-- One branch, resolved by the service, so a host's validationMessages apply. -->
1823
1817
  <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'" [id]="domId('-error')">{{
1824
1818
  errorMessage
1825
1819
  }}</span>
@@ -1828,7 +1822,7 @@ class EmailFieldComponent {
1828
1822
  </div>
1829
1823
  `, 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 });
1830
1824
  }
1831
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: EmailFieldComponent, decorators: [{
1825
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: EmailFieldComponent, decorators: [{
1832
1826
  type: Component,
1833
1827
  args: [{
1834
1828
  changeDetection: ChangeDetectionStrategy.OnPush,
@@ -1883,10 +1877,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
1883
1877
  [attr.disabled]="field.disabled ? true : null"
1884
1878
  />
1885
1879
  @if (errorMessage) {
1886
- <!-- One branch, resolved by the service. The template used to spell out "Email is
1887
- required" and "Enter a valid email address" itself, so a host configuring
1888
- validationMessages was overridden by markup it could not reach. The service
1889
- defaults say the same thing. -->
1880
+ <!-- One branch, resolved by the service, so a host's validationMessages apply. -->
1890
1881
  <span class="ngx-field__error" [attr.data-testid]="'field-' + field.id + '-error'" [id]="domId('-error')">{{
1891
1882
  errorMessage
1892
1883
  }}</span>
@@ -1937,12 +1928,6 @@ class PasswordFieldComponent {
1937
1928
  toggleVisibility() {
1938
1929
  this.visible.update(v => !v);
1939
1930
  }
1940
- /**
1941
- * Resolved through `ValidationMessagesService`, so `provideNgxDynamicEntity({
1942
- * validationMessages })` reaches this field. It used to render a fixed
1943
- * "This field has an error", which made a documented, configurable feature work on three
1944
- * of fifteen field types.
1945
- */
1946
1931
  /** Author help text, shown under the control and named by `aria-describedby`. */
1947
1932
  get hint() {
1948
1933
  return resolveLabel(this.field?.hint, this.language);
@@ -1956,8 +1941,8 @@ class PasswordFieldComponent {
1956
1941
  return '';
1957
1942
  return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
1958
1943
  }
1959
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: PasswordFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1960
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: PasswordFieldComponent, isStandalone: true, selector: "ngx-password-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
1944
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: PasswordFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1945
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: PasswordFieldComponent, isStandalone: true, selector: "ngx-password-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
1961
1946
  <div
1962
1947
  class="ngx-field ngx-field--password"
1963
1948
  [attr.data-testid]="'field-' + field.id"
@@ -2017,7 +2002,7 @@ class PasswordFieldComponent {
2017
2002
  </div>
2018
2003
  `, 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 });
2019
2004
  }
2020
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: PasswordFieldComponent, decorators: [{
2005
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: PasswordFieldComponent, decorators: [{
2021
2006
  type: Component,
2022
2007
  args: [{
2023
2008
  changeDetection: ChangeDetectionStrategy.OnPush,
@@ -2118,12 +2103,6 @@ class CheckboxFieldComponent {
2118
2103
  get label() {
2119
2104
  return resolveLabel(this.field?.label, this.language);
2120
2105
  }
2121
- /**
2122
- * Configured messages reach this field too.
2123
- *
2124
- * It rendered no error element at all, so a required checkbox left unticked told the user
2125
- * nothing — and `validationMessages` could not reach a field that never displayed one.
2126
- */
2127
2106
  /** Author help text, shown under the control and named by `aria-describedby`. */
2128
2107
  get hint() {
2129
2108
  return resolveLabel(this.field?.hint, this.language);
@@ -2137,8 +2116,8 @@ class CheckboxFieldComponent {
2137
2116
  return '';
2138
2117
  return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
2139
2118
  }
2140
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: CheckboxFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2141
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: CheckboxFieldComponent, isStandalone: true, selector: "ngx-checkbox-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
2119
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: CheckboxFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2120
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: CheckboxFieldComponent, isStandalone: true, selector: "ngx-checkbox-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
2142
2121
  <div
2143
2122
  class="ngx-field ngx-field--checkbox"
2144
2123
  [attr.data-testid]="'field-' + field.id"
@@ -2194,7 +2173,7 @@ class CheckboxFieldComponent {
2194
2173
  </div>
2195
2174
  `, 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 });
2196
2175
  }
2197
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: CheckboxFieldComponent, decorators: [{
2176
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: CheckboxFieldComponent, decorators: [{
2198
2177
  type: Component,
2199
2178
  args: [{
2200
2179
  changeDetection: ChangeDetectionStrategy.OnPush,
@@ -2296,12 +2275,6 @@ class BooleanFieldComponent {
2296
2275
  get label() {
2297
2276
  return resolveLabel(this.field?.label, this.language);
2298
2277
  }
2299
- /**
2300
- * Configured messages reach this field too.
2301
- *
2302
- * It rendered no error element at all, so a required checkbox left unticked told the user
2303
- * nothing — and `validationMessages` could not reach a field that never displayed one.
2304
- */
2305
2278
  /** Author help text, shown under the control and named by `aria-describedby`. */
2306
2279
  get hint() {
2307
2280
  return resolveLabel(this.field?.hint, this.language);
@@ -2315,8 +2288,8 @@ class BooleanFieldComponent {
2315
2288
  return '';
2316
2289
  return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
2317
2290
  }
2318
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: BooleanFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2319
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: BooleanFieldComponent, isStandalone: true, selector: "ngx-boolean-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
2291
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: BooleanFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2292
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: BooleanFieldComponent, isStandalone: true, selector: "ngx-boolean-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
2320
2293
  <div
2321
2294
  class="ngx-field ngx-field--boolean"
2322
2295
  [attr.data-testid]="'field-' + field.id"
@@ -2380,7 +2353,7 @@ class BooleanFieldComponent {
2380
2353
  </div>
2381
2354
  `, 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 });
2382
2355
  }
2383
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: BooleanFieldComponent, decorators: [{
2356
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: BooleanFieldComponent, decorators: [{
2384
2357
  type: Component,
2385
2358
  args: [{
2386
2359
  changeDetection: ChangeDetectionStrategy.OnPush,
@@ -2486,14 +2459,10 @@ class DateFieldComponent {
2486
2459
  /**
2487
2460
  * ISO 8601 date → display string, through the formatter the host configured.
2488
2461
  *
2489
- * This used to call `toLocaleDateString()` directly, which meant `setDateFormatters` — the
2490
- * one seam for choosing how a date is rendered — reached the record summary and the `time`
2491
- * field but not this one. A host that configured formatters got them almost everywhere,
2492
- * and silently did not get them on the field type most likely to be the reason they
2493
- * configured formatters at all.
2462
+ * Never call `toLocaleDateString()` directly here: `setDateFormatters` is the one seam for
2463
+ * choosing how a date renders, and every date display has to go through it.
2494
2464
  *
2495
- * The default formatter is `toLocaleDateString()` with no locale, so an application that
2496
- * has not called `setDateFormatters` renders exactly what it rendered before.
2465
+ * The default formatter is `toLocaleDateString()` with no locale.
2497
2466
  */
2498
2467
  formatDate(value) {
2499
2468
  if (!value)
@@ -2508,12 +2477,6 @@ class DateFieldComponent {
2508
2477
  return value;
2509
2478
  return formatDisplayValue('date', undefined, value, this.language);
2510
2479
  }
2511
- /**
2512
- * Resolved through `ValidationMessagesService`, so `provideNgxDynamicEntity({
2513
- * validationMessages })` reaches this field. It used to render a fixed
2514
- * "This field has an error", which made a documented, configurable feature work on three
2515
- * of fifteen field types.
2516
- */
2517
2480
  /** Author help text, shown under the control and named by `aria-describedby`. */
2518
2481
  get hint() {
2519
2482
  return resolveLabel(this.field?.hint, this.language);
@@ -2527,8 +2490,8 @@ class DateFieldComponent {
2527
2490
  return '';
2528
2491
  return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
2529
2492
  }
2530
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: DateFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2531
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: DateFieldComponent, isStandalone: true, selector: "ngx-date-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
2493
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: DateFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2494
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: DateFieldComponent, isStandalone: true, selector: "ngx-date-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
2532
2495
  <div
2533
2496
  class="ngx-field ngx-field--date"
2534
2497
  [attr.data-testid]="'field-' + field.id"
@@ -2578,7 +2541,7 @@ class DateFieldComponent {
2578
2541
  </div>
2579
2542
  `, 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 });
2580
2543
  }
2581
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: DateFieldComponent, decorators: [{
2544
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: DateFieldComponent, decorators: [{
2582
2545
  type: Component,
2583
2546
  args: [{
2584
2547
  changeDetection: ChangeDetectionStrategy.OnPush,
@@ -2652,8 +2615,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
2652
2615
  *
2653
2616
  * A bare calendar date is pinned to *local* midnight. `new Date('2020-01-01')` reads a
2654
2617
  * date-only string as UTC midnight, which renders as the previous day anywhere west of
2655
- * Greenwich — and every legacy `datetime` value has exactly that shape, because the type
2656
- * used to render through `DateFieldComponent` and its `type="date"` input.
2618
+ * Greenwich — and legacy `datetime` values written by a `type="date"` input have exactly
2619
+ * that shape.
2657
2620
  */
2658
2621
  function parseStored(value) {
2659
2622
  if (value === null || value === undefined || value === '')
@@ -2734,12 +2697,6 @@ class DateTimeFieldComponent {
2734
2697
  return '—';
2735
2698
  return formatDisplayValue('datetime', undefined, d.toISOString(), this.language);
2736
2699
  }
2737
- /**
2738
- * Resolved through `ValidationMessagesService`, so `provideNgxDynamicEntity({
2739
- * validationMessages })` reaches this field. It used to render a fixed
2740
- * "This field has an error", which made a documented, configurable feature work on three
2741
- * of fifteen field types.
2742
- */
2743
2700
  /** Author help text, shown under the control and named by `aria-describedby`. */
2744
2701
  get hint() {
2745
2702
  return resolveLabel(this.field?.hint, this.language);
@@ -2753,8 +2710,8 @@ class DateTimeFieldComponent {
2753
2710
  return '';
2754
2711
  return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
2755
2712
  }
2756
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: DateTimeFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2757
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: DateTimeFieldComponent, isStandalone: true, selector: "ngx-date-time-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
2713
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: DateTimeFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2714
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: DateTimeFieldComponent, isStandalone: true, selector: "ngx-date-time-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
2758
2715
  <div
2759
2716
  class="ngx-field ngx-field--datetime"
2760
2717
  [attr.data-testid]="'field-' + field.id"
@@ -2805,7 +2762,7 @@ class DateTimeFieldComponent {
2805
2762
  </div>
2806
2763
  `, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2807
2764
  }
2808
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: DateTimeFieldComponent, decorators: [{
2765
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: DateTimeFieldComponent, decorators: [{
2809
2766
  type: Component,
2810
2767
  args: [{
2811
2768
  changeDetection: ChangeDetectionStrategy.OnPush,
@@ -2911,12 +2868,6 @@ class TimeFieldComponent {
2911
2868
  formatTime(value) {
2912
2869
  return formatDisplayValue('time', undefined, value, this.language);
2913
2870
  }
2914
- /**
2915
- * Resolved through `ValidationMessagesService`, so `provideNgxDynamicEntity({
2916
- * validationMessages })` reaches this field. It used to render a fixed
2917
- * "This field has an error", which made a documented, configurable feature work on three
2918
- * of fifteen field types.
2919
- */
2920
2871
  /** Author help text, shown under the control and named by `aria-describedby`. */
2921
2872
  get hint() {
2922
2873
  return resolveLabel(this.field?.hint, this.language);
@@ -2930,8 +2881,8 @@ class TimeFieldComponent {
2930
2881
  return '';
2931
2882
  return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
2932
2883
  }
2933
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: TimeFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2934
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: TimeFieldComponent, isStandalone: true, selector: "ngx-time-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
2884
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: TimeFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2885
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: TimeFieldComponent, isStandalone: true, selector: "ngx-time-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
2935
2886
  <div
2936
2887
  class="ngx-field ngx-field--time"
2937
2888
  [attr.data-testid]="'field-' + field.id"
@@ -2981,7 +2932,7 @@ class TimeFieldComponent {
2981
2932
  </div>
2982
2933
  `, 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 });
2983
2934
  }
2984
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: TimeFieldComponent, decorators: [{
2935
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: TimeFieldComponent, decorators: [{
2985
2936
  type: Component,
2986
2937
  args: [{
2987
2938
  changeDetection: ChangeDetectionStrategy.OnPush,
@@ -3120,12 +3071,6 @@ class MonthYearFieldComponent {
3120
3071
  const monthName = MONTH_NAMES[parseInt(monthNum, 10) - 1];
3121
3072
  return monthName && year ? `${monthName} ${year}` : String(value);
3122
3073
  }
3123
- /**
3124
- * Resolved through `ValidationMessagesService`, so `provideNgxDynamicEntity({
3125
- * validationMessages })` reaches this field. It used to render a fixed
3126
- * "This field has an error", which made a documented, configurable feature work on three
3127
- * of fifteen field types.
3128
- */
3129
3074
  /** Author help text, shown under the control and named by `aria-describedby`. */
3130
3075
  get hint() {
3131
3076
  return resolveLabel(this.field?.hint, this.language);
@@ -3139,8 +3084,8 @@ class MonthYearFieldComponent {
3139
3084
  return '';
3140
3085
  return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
3141
3086
  }
3142
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MonthYearFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3143
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: MonthYearFieldComponent, isStandalone: true, selector: "ngx-month-year-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
3087
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: MonthYearFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3088
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: MonthYearFieldComponent, isStandalone: true, selector: "ngx-month-year-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
3144
3089
  <div
3145
3090
  class="ngx-field ngx-field--month-year"
3146
3091
  [attr.data-testid]="'field-' + field.id"
@@ -3219,7 +3164,7 @@ class MonthYearFieldComponent {
3219
3164
  </div>
3220
3165
  `, 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 });
3221
3166
  }
3222
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MonthYearFieldComponent, decorators: [{
3167
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: MonthYearFieldComponent, decorators: [{
3223
3168
  type: Component,
3224
3169
  args: [{
3225
3170
  changeDetection: ChangeDetectionStrategy.OnPush,
@@ -3520,10 +3465,10 @@ class LookupRegistryService {
3520
3465
  value !== null &&
3521
3466
  typeof value.subscribe === 'function');
3522
3467
  }
3523
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: LookupRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
3524
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: LookupRegistryService, providedIn: 'root' });
3468
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: LookupRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
3469
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: LookupRegistryService, providedIn: 'root' });
3525
3470
  }
3526
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: LookupRegistryService, decorators: [{
3471
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: LookupRegistryService, decorators: [{
3527
3472
  type: Injectable,
3528
3473
  args: [{ providedIn: 'root' }]
3529
3474
  }] });
@@ -3610,10 +3555,7 @@ class DropdownFieldComponent {
3610
3555
  * The value this option stores — the option object itself.
3611
3556
  *
3612
3557
  * Kept as a method rather than binding `option` straight into the template, because it is
3613
- * the one place that says *why* the two are the same thing. It used to delegate to
3614
- * `getOptionStoredValue`, an exported identity function whose two branches returned their
3615
- * argument unchanged; naming a transformation that does not happen is worse than naming
3616
- * nothing, because a reader has to go and check.
3558
+ * the one place that says *why* the two are the same thing.
3617
3559
  */
3618
3560
  getOptStoredVal(option) {
3619
3561
  return option;
@@ -3652,8 +3594,8 @@ class DropdownFieldComponent {
3652
3594
  // dropdown than "This field is required", and both stay independently overridable.
3653
3595
  return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
3654
3596
  }
3655
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: DropdownFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3656
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: DropdownFieldComponent, isStandalone: true, selector: "ngx-dropdown-field", inputs: { field: "field", language: "language", control: "control", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
3597
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: DropdownFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3598
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: DropdownFieldComponent, isStandalone: true, selector: "ngx-dropdown-field", inputs: { field: "field", language: "language", control: "control", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
3657
3599
  <div
3658
3600
  class="ngx-field ngx-field--dropdown"
3659
3601
  [attr.data-testid]="'field-' + field.id"
@@ -3723,7 +3665,7 @@ class DropdownFieldComponent {
3723
3665
  </div>
3724
3666
  `, 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 });
3725
3667
  }
3726
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: DropdownFieldComponent, decorators: [{
3668
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: DropdownFieldComponent, decorators: [{
3727
3669
  type: Component,
3728
3670
  args: [{
3729
3671
  changeDetection: ChangeDetectionStrategy.OnPush,
@@ -3875,10 +3817,7 @@ class RadioFieldComponent {
3875
3817
  * The value this option stores — the option object itself.
3876
3818
  *
3877
3819
  * Kept as a method rather than binding `option` straight into the template, because it is
3878
- * the one place that says *why* the two are the same thing. It used to delegate to
3879
- * `getOptionStoredValue`, an exported identity function whose two branches returned their
3880
- * argument unchanged; naming a transformation that does not happen is worse than naming
3881
- * nothing, because a reader has to go and check.
3820
+ * the one place that says *why* the two are the same thing.
3882
3821
  */
3883
3822
  getOptStoredVal(option) {
3884
3823
  return option;
@@ -3928,12 +3867,6 @@ class RadioFieldComponent {
3928
3867
  return cached;
3929
3868
  return typeof value === 'object' ? resolveLabel(value, this.language) : String(value ?? '—');
3930
3869
  }
3931
- /**
3932
- * Resolved through `ValidationMessagesService`, so `provideNgxDynamicEntity({
3933
- * validationMessages })` reaches this field. It used to render a fixed
3934
- * "This field has an error", which made a documented, configurable feature work on three
3935
- * of fifteen field types.
3936
- */
3937
3870
  /** Author help text, shown under the control and named by `aria-describedby`. */
3938
3871
  get hint() {
3939
3872
  return resolveLabel(this.field?.hint, this.language);
@@ -3947,8 +3880,8 @@ class RadioFieldComponent {
3947
3880
  return '';
3948
3881
  return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
3949
3882
  }
3950
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: RadioFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3951
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: RadioFieldComponent, isStandalone: true, selector: "ngx-radio-field", inputs: { field: "field", language: "language", control: "control", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
3883
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: RadioFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3884
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: RadioFieldComponent, isStandalone: true, selector: "ngx-radio-field", inputs: { field: "field", language: "language", control: "control", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
3952
3885
  <div
3953
3886
  class="ngx-field ngx-field--radio"
3954
3887
  [attr.data-testid]="'field-' + field.id"
@@ -4012,7 +3945,7 @@ class RadioFieldComponent {
4012
3945
  </div>
4013
3946
  `, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4014
3947
  }
4015
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: RadioFieldComponent, decorators: [{
3948
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: RadioFieldComponent, decorators: [{
4016
3949
  type: Component,
4017
3950
  args: [{
4018
3951
  changeDetection: ChangeDetectionStrategy.OnPush,
@@ -4141,10 +4074,7 @@ class MultiSelectFieldComponent {
4141
4074
  * The value this option stores — the option object itself.
4142
4075
  *
4143
4076
  * Kept as a method rather than binding `option` straight into the template, because it is
4144
- * the one place that says *why* the two are the same thing. It used to delegate to
4145
- * `getOptionStoredValue`, an exported identity function whose two branches returned their
4146
- * argument unchanged; naming a transformation that does not happen is worse than naming
4147
- * nothing, because a reader has to go and check.
4077
+ * the one place that says *why* the two are the same thing.
4148
4078
  */
4149
4079
  getOptStoredVal(option) {
4150
4080
  return option;
@@ -4168,12 +4098,6 @@ class MultiSelectFieldComponent {
4168
4098
  })
4169
4099
  .join(', ');
4170
4100
  }
4171
- /**
4172
- * Resolved through `ValidationMessagesService`, so `provideNgxDynamicEntity({
4173
- * validationMessages })` reaches this field. It used to render a fixed
4174
- * "This field has an error", which made a documented, configurable feature work on three
4175
- * of fifteen field types.
4176
- */
4177
4101
  /** Author help text, shown under the control and named by `aria-describedby`. */
4178
4102
  get hint() {
4179
4103
  return resolveLabel(this.field?.hint, this.language);
@@ -4187,8 +4111,8 @@ class MultiSelectFieldComponent {
4187
4111
  return '';
4188
4112
  return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
4189
4113
  }
4190
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MultiSelectFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4191
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: MultiSelectFieldComponent, isStandalone: true, selector: "ngx-multi-select-field", inputs: { field: "field", language: "language", control: "control", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
4114
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: MultiSelectFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4115
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: MultiSelectFieldComponent, isStandalone: true, selector: "ngx-multi-select-field", inputs: { field: "field", language: "language", control: "control", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
4192
4116
  <div
4193
4117
  class="ngx-field ngx-field--multiSelect"
4194
4118
  [attr.data-testid]="'field-' + field.id"
@@ -4248,7 +4172,7 @@ class MultiSelectFieldComponent {
4248
4172
  </div>
4249
4173
  `, 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 });
4250
4174
  }
4251
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MultiSelectFieldComponent, decorators: [{
4175
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: MultiSelectFieldComponent, decorators: [{
4252
4176
  type: Component,
4253
4177
  args: [{
4254
4178
  changeDetection: ChangeDetectionStrategy.OnPush,
@@ -4347,10 +4271,10 @@ class EntityRefRegistryService {
4347
4271
  has(entityKey) {
4348
4272
  return this.registry.has(entityKey);
4349
4273
  }
4350
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: EntityRefRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
4351
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: EntityRefRegistryService, providedIn: 'root' });
4274
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: EntityRefRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
4275
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: EntityRefRegistryService, providedIn: 'root' });
4352
4276
  }
4353
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: EntityRefRegistryService, decorators: [{
4277
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: EntityRefRegistryService, decorators: [{
4354
4278
  type: Injectable,
4355
4279
  args: [{ providedIn: 'root' }]
4356
4280
  }] });
@@ -4510,10 +4434,10 @@ class EntityReferenceService {
4510
4434
  matches(a, b, lang = 'en') {
4511
4435
  return valuesMatch(a, b, lang);
4512
4436
  }
4513
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: EntityReferenceService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
4514
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: EntityReferenceService, providedIn: 'root' });
4437
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: EntityReferenceService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
4438
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: EntityReferenceService, providedIn: 'root' });
4515
4439
  }
4516
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: EntityReferenceService, decorators: [{
4440
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: EntityReferenceService, decorators: [{
4517
4441
  type: Injectable,
4518
4442
  args: [{ providedIn: 'root' }]
4519
4443
  }] });
@@ -4634,10 +4558,10 @@ class CascadeDataService {
4634
4558
  walkTabs(config?.tabs);
4635
4559
  return found;
4636
4560
  }
4637
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: CascadeDataService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
4638
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: CascadeDataService, providedIn: 'root' });
4561
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: CascadeDataService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
4562
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: CascadeDataService, providedIn: 'root' });
4639
4563
  }
4640
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: CascadeDataService, decorators: [{
4564
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: CascadeDataService, decorators: [{
4641
4565
  type: Injectable,
4642
4566
  args: [{ providedIn: 'root' }]
4643
4567
  }] });
@@ -4658,10 +4582,10 @@ class EntityRefSelectionService {
4658
4582
  emit(fieldId, option) {
4659
4583
  this.subject.next({ fieldId, option });
4660
4584
  }
4661
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: EntityRefSelectionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
4662
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: EntityRefSelectionService, providedIn: 'root' });
4585
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: EntityRefSelectionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
4586
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: EntityRefSelectionService, providedIn: 'root' });
4663
4587
  }
4664
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: EntityRefSelectionService, decorators: [{
4588
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: EntityRefSelectionService, decorators: [{
4665
4589
  type: Injectable,
4666
4590
  args: [{ providedIn: 'root' }]
4667
4591
  }] });
@@ -4776,12 +4700,6 @@ class EntityRefFieldComponent {
4776
4700
  void this.reload();
4777
4701
  });
4778
4702
  }
4779
- /**
4780
- * Resolved through `ValidationMessagesService`, so `provideNgxDynamicEntity({
4781
- * validationMessages })` reaches this field. It used to render a fixed
4782
- * "This field has an error", which made a documented, configurable feature work on three
4783
- * of fifteen field types.
4784
- */
4785
4703
  /** Author help text, shown under the control and named by `aria-describedby`. */
4786
4704
  get hint() {
4787
4705
  return resolveLabel(this.field?.hint, this.language);
@@ -4795,8 +4713,8 @@ class EntityRefFieldComponent {
4795
4713
  return '';
4796
4714
  return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
4797
4715
  }
4798
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: EntityRefFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4799
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: EntityRefFieldComponent, isStandalone: true, selector: "ngx-entity-ref-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
4716
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: EntityRefFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4717
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: EntityRefFieldComponent, isStandalone: true, selector: "ngx-entity-ref-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
4800
4718
  <div
4801
4719
  class="ngx-field ngx-field--entity-ref"
4802
4720
  [attr.data-testid]="'field-' + field.id"
@@ -4861,7 +4779,7 @@ class EntityRefFieldComponent {
4861
4779
  </div>
4862
4780
  `, 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 });
4863
4781
  }
4864
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: EntityRefFieldComponent, decorators: [{
4782
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: EntityRefFieldComponent, decorators: [{
4865
4783
  type: Component,
4866
4784
  args: [{
4867
4785
  changeDetection: ChangeDetectionStrategy.OnPush,
@@ -4989,10 +4907,10 @@ class FieldRegistryService {
4989
4907
  registeredTypes() {
4990
4908
  return [...new Set([...this.dynamicRegistry.keys(), ...this.consumerRegistry.keys()])].sort();
4991
4909
  }
4992
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: FieldRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
4993
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: FieldRegistryService, providedIn: 'root' });
4910
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: FieldRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
4911
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: FieldRegistryService, providedIn: 'root' });
4994
4912
  }
4995
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: FieldRegistryService, decorators: [{
4913
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: FieldRegistryService, decorators: [{
4996
4914
  type: Injectable,
4997
4915
  args: [{ providedIn: 'root' }]
4998
4916
  }], ctorParameters: () => [] });
@@ -5031,10 +4949,10 @@ class RbacService {
5031
4949
  canDelete: this.hasPermission(userRoles, config.permissions?.delete),
5032
4950
  };
5033
4951
  }
5034
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: RbacService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
5035
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: RbacService, providedIn: 'root' });
4952
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: RbacService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
4953
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: RbacService, providedIn: 'root' });
5036
4954
  }
5037
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: RbacService, decorators: [{
4955
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: RbacService, decorators: [{
5038
4956
  type: Injectable,
5039
4957
  args: [{ providedIn: 'root' }]
5040
4958
  }] });
@@ -5127,7 +5045,7 @@ class DynamicFieldComponent {
5127
5045
  *
5128
5046
  * `detectChanges()` rather than `markForCheck()`: a selection that arrives from a native
5129
5047
  * `change` event (or Playwright) may not be followed by another Angular tick, so a dirty
5130
- * flag would never be flushed. Running CD on the hosted component now is the copy showing up.
5048
+ * flag would never be flushed. Running CD on the hosted component immediately is what shows the copied value.
5131
5049
  *
5132
5050
  * Done here rather than in each of the eighteen field components: the host owns the
5133
5051
  * component reference, so one subscription covers every type including custom ones.
@@ -5156,10 +5074,10 @@ class DynamicFieldComponent {
5156
5074
  ref.setInput('readonly', this.readonly || !!this.field.readonly);
5157
5075
  ref.setInput('masked', masked);
5158
5076
  }
5159
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: DynamicFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5160
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.22", type: DynamicFieldComponent, isStandalone: true, selector: "ngx-dynamic-field", inputs: { field: "field", control: "control", config: "config", currentTabId: "currentTabId", language: "language", readonly: "readonly", userRoles: "userRoles" }, viewQueries: [{ propertyName: "fieldHost", first: true, predicate: ["fieldHost"], descendants: true, read: ViewContainerRef, static: true }], usesOnChanges: true, ngImport: i0, template: `<ng-container #fieldHost></ng-container>`, isInline: true, styles: [":host{display:block;width:100%;min-width:0}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5077
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: DynamicFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5078
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.24", type: DynamicFieldComponent, isStandalone: true, selector: "ngx-dynamic-field", inputs: { field: "field", control: "control", config: "config", currentTabId: "currentTabId", language: "language", readonly: "readonly", userRoles: "userRoles" }, viewQueries: [{ propertyName: "fieldHost", first: true, predicate: ["fieldHost"], descendants: true, read: ViewContainerRef, static: true }], usesOnChanges: true, ngImport: i0, template: `<ng-container #fieldHost></ng-container>`, isInline: true, styles: [":host{display:block;width:100%;min-width:0}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5161
5079
  }
5162
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: DynamicFieldComponent, decorators: [{
5080
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: DynamicFieldComponent, decorators: [{
5163
5081
  type: Component,
5164
5082
  args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'ngx-dynamic-field', standalone: true, template: `<ng-container #fieldHost></ng-container>`, styles: [":host{display:block;width:100%;min-width:0}\n"] }]
5165
5083
  }], propDecorators: { field: [{
@@ -5203,8 +5121,8 @@ class GroupFieldComponent {
5203
5121
  const group = this.control;
5204
5122
  return group?.get(childId);
5205
5123
  }
5206
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: GroupFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5207
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", 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: `
5124
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: GroupFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5125
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", 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: `
5208
5126
  <fieldset
5209
5127
  class="ngx-field ngx-field--group"
5210
5128
  [attr.data-testid]="'field-' + field.id"
@@ -5245,7 +5163,7 @@ class GroupFieldComponent {
5245
5163
  </fieldset>
5246
5164
  `, 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 });
5247
5165
  }
5248
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: GroupFieldComponent, decorators: [{
5166
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: GroupFieldComponent, decorators: [{
5249
5167
  type: Component,
5250
5168
  args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'ngx-group-field', standalone: true, imports: [ReactiveFormsModule, forwardRef(() => DynamicFieldComponent)], template: `
5251
5169
  <fieldset
@@ -5303,6 +5221,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
5303
5221
  type: Input
5304
5222
  }] } });
5305
5223
 
5224
+ /**
5225
+ * A format check that, like `Validators.email`, passes an empty value.
5226
+ *
5227
+ * Emptiness is `required`'s question. Answering it here too would give an optional field an
5228
+ * error the moment it was cleared.
5229
+ */
5230
+ function formatValidator(key, test) {
5231
+ return (control) => {
5232
+ const value = control.value;
5233
+ if (value === null || value === undefined || value === '')
5234
+ return null;
5235
+ return test(String(value)) ? null : { [key]: true };
5236
+ };
5237
+ }
5238
+ /** The built-in web address check. The rule itself is in core, so an import applies the same one. */
5239
+ const urlValidator = formatValidator('url', isValidUrl);
5240
+ /** The built-in phone number check. The rule itself is in core, so an import applies the same one. */
5241
+ const phoneValidator = formatValidator('phone', isValidPhone);
5306
5242
  /**
5307
5243
  * ValidatorRegistryService — resolves validator configs / key strings to Angular ValidatorFns.
5308
5244
  */
@@ -5321,6 +5257,10 @@ class ValidatorRegistryService {
5321
5257
  return Validators.required;
5322
5258
  if (validatorKey === 'email')
5323
5259
  return Validators.email;
5260
+ if (validatorKey === 'url')
5261
+ return urlValidator;
5262
+ if (validatorKey === 'phone')
5263
+ return phoneValidator;
5324
5264
  const [name, param] = validatorKey.split(':');
5325
5265
  const value = parseFloat(param);
5326
5266
  if (name === 'min' && !isNaN(value))
@@ -5370,10 +5310,12 @@ class ValidatorRegistryService {
5370
5310
  }
5371
5311
  if (config.email)
5372
5312
  fnList.push(Validators.email);
5373
- // Named validators from the consumer registry. This branch previously hardcoded the
5374
- // built-ins and consulted the registry only for the `string[]` form of this config, so
5375
- // a validator the consumer had registered could not be named from a typed schema
5376
- // without casting it to `any`.
5313
+ if (config.url)
5314
+ fnList.push(urlValidator);
5315
+ if (config.phone)
5316
+ fnList.push(phoneValidator);
5317
+ // Named validators from the consumer registry, so a registered validator can be named
5318
+ // from a typed schema.
5377
5319
  for (const key of config.custom ?? []) {
5378
5320
  const fn = this.resolve(key);
5379
5321
  if (fn)
@@ -5409,10 +5351,9 @@ class ValidatorRegistryService {
5409
5351
  /**
5410
5352
  * Say when a named validator resolved to nothing.
5411
5353
  *
5412
- * It used to be dropped in silence, which is the worst failure mode this library has: the
5413
- * schema says the field is checked, the field is not checked, the form saves, and nothing
5414
- * anywhere said so. For `customAsync` that is typically a uniqueness check, so the duplicate
5415
- * lands in the database.
5354
+ * Silence would be the worst failure mode this library has: the schema says the field is
5355
+ * checked, the field is not checked, the form saves, and nothing says so. For
5356
+ * `customAsync` that is typically a uniqueness check, so the duplicate lands in the database.
5416
5357
  *
5417
5358
  * Dev only, once per name — this runs while controls are built, which happens on every
5418
5359
  * config change. `validateConfig` with `knownValidators` is the build-time version of the
@@ -5430,10 +5371,10 @@ class ValidatorRegistryService {
5430
5371
  this.warned.add(id);
5431
5372
  console.warn(`[ngx-dynamic-entity] ${message}`);
5432
5373
  }
5433
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: ValidatorRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
5434
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: ValidatorRegistryService, providedIn: 'root' });
5374
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: ValidatorRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
5375
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: ValidatorRegistryService, providedIn: 'root' });
5435
5376
  }
5436
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: ValidatorRegistryService, decorators: [{
5377
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: ValidatorRegistryService, decorators: [{
5437
5378
  type: Injectable,
5438
5379
  args: [{ providedIn: 'root' }]
5439
5380
  }] });
@@ -5486,8 +5427,8 @@ class ArrayFieldComponent {
5486
5427
  this.formArray.removeAt(index);
5487
5428
  this.cdr.markForCheck();
5488
5429
  }
5489
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: ArrayFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5490
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", 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: `
5430
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: ArrayFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5431
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", 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: `
5491
5432
  <div
5492
5433
  class="ngx-field ngx-field--array"
5493
5434
  [attr.data-testid]="'field-' + field.id"
@@ -5566,7 +5507,7 @@ class ArrayFieldComponent {
5566
5507
  </div>
5567
5508
  `, 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 });
5568
5509
  }
5569
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: ArrayFieldComponent, decorators: [{
5510
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: ArrayFieldComponent, decorators: [{
5570
5511
  type: Component,
5571
5512
  args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'ngx-array-field', standalone: true, imports: [ReactiveFormsModule, forwardRef(() => DynamicFieldComponent)], template: `
5572
5513
  <div
@@ -5740,10 +5681,10 @@ class FileUploadService {
5740
5681
  value !== null &&
5741
5682
  typeof value.subscribe === 'function');
5742
5683
  }
5743
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: FileUploadService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
5744
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: FileUploadService, providedIn: 'root' });
5684
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: FileUploadService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
5685
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: FileUploadService, providedIn: 'root' });
5745
5686
  }
5746
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: FileUploadService, decorators: [{
5687
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: FileUploadService, decorators: [{
5747
5688
  type: Injectable,
5748
5689
  args: [{ providedIn: 'root' }]
5749
5690
  }] });
@@ -5828,8 +5769,8 @@ class ImageFieldComponent {
5828
5769
  return '';
5829
5770
  return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
5830
5771
  }
5831
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: ImageFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5832
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: ImageFieldComponent, isStandalone: true, selector: "ngx-image-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
5772
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: ImageFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5773
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: ImageFieldComponent, isStandalone: true, selector: "ngx-image-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
5833
5774
  <div
5834
5775
  class="ngx-field ngx-field--image"
5835
5776
  [attr.data-testid]="'field-' + field.id"
@@ -5908,7 +5849,7 @@ class ImageFieldComponent {
5908
5849
  </div>
5909
5850
  `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
5910
5851
  }
5911
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: ImageFieldComponent, decorators: [{
5852
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: ImageFieldComponent, decorators: [{
5912
5853
  type: Component,
5913
5854
  args: [{
5914
5855
  changeDetection: ChangeDetectionStrategy.OnPush,
@@ -6078,8 +6019,8 @@ class FileFieldComponent {
6078
6019
  return '';
6079
6020
  return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
6080
6021
  }
6081
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: FileFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
6082
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: FileFieldComponent, isStandalone: true, selector: "ngx-file-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
6022
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: FileFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
6023
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: FileFieldComponent, isStandalone: true, selector: "ngx-file-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
6083
6024
  <div
6084
6025
  class="ngx-field ngx-field--file"
6085
6026
  [attr.data-testid]="'field-' + field.id"
@@ -6161,7 +6102,7 @@ class FileFieldComponent {
6161
6102
  </div>
6162
6103
  `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
6163
6104
  }
6164
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: FileFieldComponent, decorators: [{
6105
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: FileFieldComponent, decorators: [{
6165
6106
  type: Component,
6166
6107
  args: [{
6167
6108
  changeDetection: ChangeDetectionStrategy.OnPush,
@@ -6263,220 +6204,1701 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
6263
6204
  }] } });
6264
6205
 
6265
6206
  /**
6266
- * Field-type registration — the tree-shaking seam.
6267
- *
6268
- * `FieldRegistryService` imports no components. Whatever you register here is what gets
6269
- * bundled, so an app that only uses text/number/dropdown pays for three components and
6270
- * their imports, not twenty-one.
6271
- *
6272
- * ```ts
6273
- * // Everything (convenient; bundles all built-ins):
6274
- * provideNgxDynamicEntity(), provideBuiltInFieldTypes()
6275
- *
6276
- * // Only what this app renders:
6277
- * provideFieldTypes({ text: TextFieldComponent, number: NumberFieldComponent })
6278
- * ```
6279
- *
6280
- * Both merge into `FIELD_TYPE_REGISTRY`; later registrations win on key collision.
6281
- */
6282
- /** Map of every built-in field type key → component. Referencing this pulls in all of them. */
6283
- function builtInFieldTypes() {
6284
- return {
6285
- text: TextFieldComponent,
6286
- textarea: TextareaFieldComponent,
6287
- markdown: MarkdownFieldComponent,
6288
- number: NumberFieldComponent,
6289
- currency: CurrencyFieldComponent,
6290
- email: EmailFieldComponent,
6291
- password: PasswordFieldComponent,
6292
- checkbox: CheckboxFieldComponent,
6293
- boolean: BooleanFieldComponent,
6294
- date: DateFieldComponent,
6295
- datetime: DateTimeFieldComponent,
6296
- time: TimeFieldComponent,
6297
- monthYear: MonthYearFieldComponent,
6298
- dropdown: DropdownFieldComponent,
6299
- radio: RadioFieldComponent,
6300
- multiSelect: MultiSelectFieldComponent,
6301
- 'entity-ref': EntityRefFieldComponent,
6302
- group: GroupFieldComponent,
6303
- array: ArrayFieldComponent,
6304
- image: ImageFieldComponent,
6305
- file: FileFieldComponent,
6306
- };
6307
- }
6308
- /** Register a specific set of field types. Only the components you name are bundled. */
6309
- function provideFieldTypes(types) {
6310
- return makeEnvironmentProviders([
6311
- { provide: FIELD_TYPE_SETS, useValue: types, multi: true },
6312
- ]);
6313
- }
6314
- /** Register all 21 built-in field type keys. Convenience for apps that use most of them. */
6315
- function provideBuiltInFieldTypes() {
6316
- return provideFieldTypes(builtInFieldTypes());
6317
- }
6318
-
6319
- /**
6320
- * HookRegistryService — stores and runs Angular-side pre/post hooks.
6321
- * Consumers register hooks via provideNgxDynamicEntity({ hooks: { 'clients:beforeSave': fn } }).
6322
- */
6323
- class HookRegistryService {
6324
- registry = inject(HOOK_REGISTRY, { optional: true }) ?? new Map();
6325
- has(key) {
6326
- return this.registry.has(key);
6327
- }
6328
- async run(key, data, context = {}) {
6329
- const hook = this.registry.get(key);
6330
- if (!hook)
6331
- return data;
6332
- return hook(data, context);
6333
- }
6334
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: HookRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
6335
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: HookRegistryService, providedIn: 'root' });
6336
- }
6337
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: HookRegistryService, decorators: [{
6338
- type: Injectable,
6339
- args: [{ providedIn: 'root' }]
6340
- }] });
6341
-
6342
- /**
6343
- * RulesEvaluationService — evaluates rules, and owns what a result *means*.
6207
+ * URL field: a `type="url"` input, and a link when read-only.
6344
6208
  *
6345
- * It used to be two passthrough methods, with the interpretation spread across the component:
6346
- * the render filter decided one way what a rule result did to a field, and the validity sync
6347
- * decided again forty lines further down. They disagreed about what "every field" meant, and
6348
- * the consequence was a required field inside a `group` that a rule had hidden holding
6349
- * `form.invalid` true forever with nothing on screen to explain it.
6350
- *
6351
- * Precedence lives here now, in one method that both callers go through. Still stateless —
6352
- * every method takes the result it is interpreting, so there is nothing to go stale against a
6353
- * rebuilt form and nothing to reset between configs.
6209
+ * The read-only link is rendered only for a value that passes `isValidUrl`, meaning an http or
6210
+ * https address. A record can hold anything, including `javascript:`, and a field whose job is
6211
+ * to show an address must not turn a stored script into something to click. Anything else is
6212
+ * shown as plain text.
6354
6213
  */
6355
- class RulesEvaluationService {
6214
+ class UrlFieldComponent {
6356
6215
  /**
6357
- * Evaluate rules against form values and optional original baseline values.
6216
+ * Unique to this component instance: an `array` renders the same field once per row, and a
6217
+ * DOM id may not repeat. See `field-dom-id.ts`.
6358
6218
  */
6359
- evaluate(rules, values, originalValues, options) {
6360
- return evaluateFormRules(rules, values, originalValues, options);
6219
+ instanceId = nextFieldInstanceId();
6220
+ domId(suffix = '') {
6221
+ return fieldDomId(this.field, this.instanceId, suffix);
6361
6222
  }
6362
- /**
6363
- * Filter rules pertinent to a single tab.
6364
- */
6365
- filterForTab(rules, tabId, config) {
6366
- return filterRulesForTab(rules, tabId, config);
6223
+ /** Overridable via MASKED_PLACEHOLDER; the default is the historic literal. */
6224
+ maskedText = inject(MASKED_PLACEHOLDER, { optional: true }) ?? 'XXXXXXXXX';
6225
+ messages = inject(ValidationMessagesService);
6226
+ field;
6227
+ control;
6228
+ language = 'en';
6229
+ readonly = false;
6230
+ masked = false;
6231
+ get label() {
6232
+ return resolveLabel(this.field?.label, this.language);
6367
6233
  }
6368
- /**
6369
- * Whether a field renders, under the precedence documented on `RuleEvaluationResult`.
6370
- *
6371
- * An explicit hide beats everything. A show beats static hiding — `visibility: false`, or a
6372
- * `showWhen` that does not currently hold — because a rule that could only hide what the
6373
- * config already hides would have nothing to say, and `{ visibility: true }` was a
6374
- * documented no-op until it did. Otherwise the config's own visibility decides.
6375
- */
6376
- isFieldVisible(result, field, names, values) {
6377
- const sets = this.nameSets(result);
6378
- if (names.some(name => sets.hidden.has(name)))
6379
- return false;
6380
- if (names.some(name => sets.shown.has(name)))
6381
- return true;
6382
- return evaluateFieldVisibility(field, values);
6234
+ get placeholder() {
6235
+ return resolveLabel(this.field?.placeholder, this.language);
6383
6236
  }
6384
- /** The same precedence for a tab. */
6385
- isTabVisible(result, tab) {
6386
- if (result.hiddenTabs.includes(tab.id))
6387
- return false;
6388
- return tab.visibility !== false || result.shownTabs.includes(tab.id);
6237
+ /** Author help text, shown under the control and named by `aria-describedby`. */
6238
+ get hint() {
6239
+ return resolveLabel(this.field?.hint, this.language);
6389
6240
  }
6390
- /**
6391
- * Keep a hidden field's control out of form validity.
6392
- *
6393
- * A field hidden by a rule or a `showWhen` is filtered out of the render, but its
6394
- * validators stay attached — so a hidden *required* field holds `form.invalid` true
6395
- * forever, permanently disabling Save with nothing on screen to explain why.
6396
- *
6397
- * Disabling is the fix rather than stripping validators: Angular excludes disabled controls
6398
- * from validity, and the field's own validators survive intact for when it comes back.
6399
- * Values are untouched — a disabled control still carries its value, so rule evaluation and
6400
- * the emitted record see exactly what they saw before. That also keeps this from feeding
6401
- * back on itself: hiding a field cannot change the values the rules are evaluated over.
6402
- *
6403
- * The predicate is `isFieldVisible`, the same one the render filter uses, so what is on
6404
- * screen and what counts toward validity cannot drift apart.
6405
- *
6406
- * Entries must arrive parent-first, which is what `collectFieldScopes` yields: a container's
6407
- * children are skipped once the container itself is hidden, because Angular's `disable()`
6408
- * cascades to descendants and a child's `enable()` walks back up recalculating ancestors.
6409
- * Toggling a child inside a hidden group would fight its parent.
6410
- */
6411
- syncHiddenFieldState(entries, ctx) {
6412
- const hiddenContainers = [];
6413
- for (const entry of entries) {
6414
- const field = entry.field;
6415
- if (!field?.id)
6416
- continue;
6417
- const address = ctx.addressOf(field);
6418
- if (hiddenContainers.some(prefix => address.startsWith(`${prefix}.`)))
6419
- continue;
6420
- const hidden = !this.isFieldVisible(ctx.result, field, ctx.namesOf(field), ctx.values);
6421
- if (hidden && (field.type === 'group' || field.type === 'array'))
6422
- hiddenContainers.push(address);
6423
- const ctrl = ctx.controlFor(entry);
6424
- if (!ctrl)
6425
- continue;
6426
- if (hidden) {
6427
- if (ctrl.enabled)
6428
- ctrl.disable({ emitEvent: false });
6429
- }
6430
- else if (ctrl.disabled && !field.disabled) {
6431
- ctrl.enable({ emitEvent: false });
6432
- }
6433
- }
6241
+ /** Whether the stored value is an http(s) address, and so safe to render as a link. */
6242
+ get linkable() {
6243
+ const value = this.control?.value;
6244
+ return typeof value === 'string' && isValidUrl(value);
6434
6245
  }
6435
- /**
6436
- * The hidden and shown names as sets, memoised against the result they came from.
6437
- *
6438
- * `isFieldVisible` runs once per field per change-detection pass; building two sets from
6439
- * two arrays each time turns a rule result into allocation proportional to the form. A
6440
- * `WeakMap` keyed by the result means it is built once per evaluation and collected with it.
6441
- */
6442
- nameSets(result) {
6443
- let sets = this.nameSetCache.get(result);
6444
- if (!sets) {
6445
- sets = { hidden: new Set(result.hiddenFields), shown: new Set(result.shownFields) };
6446
- this.nameSetCache.set(result, sets);
6447
- }
6448
- return sets;
6246
+ /** The ids of whatever is describing this control right now. */
6247
+ describedBy() {
6248
+ return fieldDescribedBy(s => this.domId(s), { hint: !!this.hint, error: !!this.errorMessage });
6449
6249
  }
6450
- nameSetCache = new WeakMap();
6451
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: RulesEvaluationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
6452
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: RulesEvaluationService, providedIn: 'root' });
6453
- }
6454
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: RulesEvaluationService, decorators: [{
6455
- type: Injectable,
6456
- args: [{ providedIn: 'root' }]
6457
- }] });
6458
-
6459
- class ConfigSourceService {
6460
- cache = new Map();
6461
- inFlight = new Map();
6462
- handler;
6463
- constructor(handler) {
6464
- this.handler = handler;
6250
+ get errorMessage() {
6251
+ if (!this.control?.errors || !this.control.touched)
6252
+ return '';
6253
+ return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
6465
6254
  }
6466
- /** Retrieve an entity config by key, with caching and in-flight deduplication. */
6467
- async getConfig(entityKey) {
6468
- if (!entityKey || !this.handler)
6469
- return undefined;
6470
- if (this.cache.has(entityKey))
6471
- return this.cache.get(entityKey);
6472
- if (this.inFlight.has(entityKey))
6473
- return this.inFlight.get(entityKey);
6474
- const promise = (async () => {
6475
- try {
6476
- const raw = this.handler(entityKey);
6477
- let resolved;
6478
- if (isObservable(raw)) {
6479
- resolved = await firstValueFrom(raw);
6255
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: UrlFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
6256
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: UrlFieldComponent, isStandalone: true, selector: "ngx-url-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
6257
+ <div
6258
+ class="ngx-field ngx-field--url"
6259
+ [attr.data-testid]="'field-' + field.id"
6260
+ [attr.data-field-type]="field.type"
6261
+ [class.ngx-field--readonly]="readonly"
6262
+ [class.ngx-field--masked]="masked"
6263
+ [class.ngx-field--invalid]="control && control.invalid && control.touched"
6264
+ >
6265
+ <div class="ngx-field__label-row">
6266
+ <label class="ngx-field__label" [attr.for]="domId()">
6267
+ {{ label }}
6268
+ @if (field.validators?.required) {
6269
+ <span class="ngx-field__req">*</span>
6270
+ }
6271
+ </label>
6272
+ @if (hint) {
6273
+ <span class="ngx-field__hint-wrap">
6274
+ <span class="ngx-field__hint-icon" aria-hidden="true">i</span>
6275
+ <span
6276
+ class="ngx-field__hint"
6277
+ role="tooltip"
6278
+ [attr.data-testid]="'field-' + field.id + '-hint'"
6279
+ [id]="domId('-hint')"
6280
+ >{{ hint }}</span
6281
+ >
6282
+ </span>
6283
+ }
6284
+ </div>
6285
+ @if (masked) {
6286
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
6287
+ maskedText
6288
+ }}</span>
6289
+ } @else if (readonly) {
6290
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">
6291
+ @if (linkable) {
6292
+ <a [href]="control.value" class="ngx-field__link" target="_blank" rel="noopener noreferrer">{{
6293
+ control.value
6294
+ }}</a>
6295
+ } @else {
6296
+ {{ control.value || '—' }}
6297
+ }
6298
+ </span>
6299
+ } @else {
6300
+ <input
6301
+ [id]="domId()"
6302
+ class="ngx-field__input"
6303
+ [attr.data-testid]="'field-' + field.id + '-input'"
6304
+ type="url"
6305
+ inputmode="url"
6306
+ autocomplete="url"
6307
+ [formControl]="$any(control)"
6308
+ [attr.aria-invalid]="control.invalid && control.touched"
6309
+ [attr.aria-describedby]="describedBy()"
6310
+ [placeholder]="placeholder || 'https://example.com'"
6311
+ [attr.disabled]="field.disabled ? true : null"
6312
+ />
6313
+ @if (errorMessage) {
6314
+ <span
6315
+ class="ngx-field__error"
6316
+ [attr.data-testid]="'field-' + field.id + '-error'"
6317
+ [id]="domId('-error')"
6318
+ role="alert"
6319
+ >{{ errorMessage }}</span
6320
+ >
6321
+ }
6322
+ }
6323
+ </div>
6324
+ `, 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 });
6325
+ }
6326
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: UrlFieldComponent, decorators: [{
6327
+ type: Component,
6328
+ args: [{
6329
+ changeDetection: ChangeDetectionStrategy.OnPush,
6330
+ selector: 'ngx-url-field',
6331
+ standalone: true,
6332
+ imports: [ReactiveFormsModule],
6333
+ template: `
6334
+ <div
6335
+ class="ngx-field ngx-field--url"
6336
+ [attr.data-testid]="'field-' + field.id"
6337
+ [attr.data-field-type]="field.type"
6338
+ [class.ngx-field--readonly]="readonly"
6339
+ [class.ngx-field--masked]="masked"
6340
+ [class.ngx-field--invalid]="control && control.invalid && control.touched"
6341
+ >
6342
+ <div class="ngx-field__label-row">
6343
+ <label class="ngx-field__label" [attr.for]="domId()">
6344
+ {{ label }}
6345
+ @if (field.validators?.required) {
6346
+ <span class="ngx-field__req">*</span>
6347
+ }
6348
+ </label>
6349
+ @if (hint) {
6350
+ <span class="ngx-field__hint-wrap">
6351
+ <span class="ngx-field__hint-icon" aria-hidden="true">i</span>
6352
+ <span
6353
+ class="ngx-field__hint"
6354
+ role="tooltip"
6355
+ [attr.data-testid]="'field-' + field.id + '-hint'"
6356
+ [id]="domId('-hint')"
6357
+ >{{ hint }}</span
6358
+ >
6359
+ </span>
6360
+ }
6361
+ </div>
6362
+ @if (masked) {
6363
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
6364
+ maskedText
6365
+ }}</span>
6366
+ } @else if (readonly) {
6367
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">
6368
+ @if (linkable) {
6369
+ <a [href]="control.value" class="ngx-field__link" target="_blank" rel="noopener noreferrer">{{
6370
+ control.value
6371
+ }}</a>
6372
+ } @else {
6373
+ {{ control.value || '—' }}
6374
+ }
6375
+ </span>
6376
+ } @else {
6377
+ <input
6378
+ [id]="domId()"
6379
+ class="ngx-field__input"
6380
+ [attr.data-testid]="'field-' + field.id + '-input'"
6381
+ type="url"
6382
+ inputmode="url"
6383
+ autocomplete="url"
6384
+ [formControl]="$any(control)"
6385
+ [attr.aria-invalid]="control.invalid && control.touched"
6386
+ [attr.aria-describedby]="describedBy()"
6387
+ [placeholder]="placeholder || 'https://example.com'"
6388
+ [attr.disabled]="field.disabled ? true : null"
6389
+ />
6390
+ @if (errorMessage) {
6391
+ <span
6392
+ class="ngx-field__error"
6393
+ [attr.data-testid]="'field-' + field.id + '-error'"
6394
+ [id]="domId('-error')"
6395
+ role="alert"
6396
+ >{{ errorMessage }}</span
6397
+ >
6398
+ }
6399
+ }
6400
+ </div>
6401
+ `,
6402
+ }]
6403
+ }], propDecorators: { field: [{
6404
+ type: Input
6405
+ }], control: [{
6406
+ type: Input
6407
+ }], language: [{
6408
+ type: Input
6409
+ }], readonly: [{
6410
+ type: Input
6411
+ }], masked: [{
6412
+ type: Input
6413
+ }] } });
6414
+
6415
+ /**
6416
+ * Phone field: a `type="tel"` input, and a `tel:` link when read-only.
6417
+ *
6418
+ * The value is stored exactly as typed, spaces and brackets included. The number is shown back
6419
+ * to people far more often than it is dialled, and `+44 20 7946 0958` is easier to read than
6420
+ * its digits run together. Only the link target is stripped down to what a dialler needs.
6421
+ */
6422
+ class PhoneFieldComponent {
6423
+ /**
6424
+ * Unique to this component instance: an `array` renders the same field once per row, and a
6425
+ * DOM id may not repeat. See `field-dom-id.ts`.
6426
+ */
6427
+ instanceId = nextFieldInstanceId();
6428
+ domId(suffix = '') {
6429
+ return fieldDomId(this.field, this.instanceId, suffix);
6430
+ }
6431
+ /** Overridable via MASKED_PLACEHOLDER; the default is the historic literal. */
6432
+ maskedText = inject(MASKED_PLACEHOLDER, { optional: true }) ?? 'XXXXXXXXX';
6433
+ messages = inject(ValidationMessagesService);
6434
+ field;
6435
+ control;
6436
+ language = 'en';
6437
+ readonly = false;
6438
+ masked = false;
6439
+ get label() {
6440
+ return resolveLabel(this.field?.label, this.language);
6441
+ }
6442
+ get placeholder() {
6443
+ return resolveLabel(this.field?.placeholder, this.language);
6444
+ }
6445
+ /** Author help text, shown under the control and named by `aria-describedby`. */
6446
+ get hint() {
6447
+ return resolveLabel(this.field?.hint, this.language);
6448
+ }
6449
+ /** `tel:` plus the digits and a leading `+`, or null for a value that is not a phone number. */
6450
+ get dialHref() {
6451
+ const value = this.control?.value;
6452
+ if (typeof value !== 'string' || !isValidPhone(value))
6453
+ return null;
6454
+ return `tel:${value.trim().startsWith('+') ? '+' : ''}${value.replace(/\D/g, '')}`;
6455
+ }
6456
+ /** The ids of whatever is describing this control right now. */
6457
+ describedBy() {
6458
+ return fieldDescribedBy(s => this.domId(s), { hint: !!this.hint, error: !!this.errorMessage });
6459
+ }
6460
+ get errorMessage() {
6461
+ if (!this.control?.errors || !this.control.touched)
6462
+ return '';
6463
+ return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
6464
+ }
6465
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: PhoneFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
6466
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: PhoneFieldComponent, isStandalone: true, selector: "ngx-phone-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
6467
+ <div
6468
+ class="ngx-field ngx-field--phone"
6469
+ [attr.data-testid]="'field-' + field.id"
6470
+ [attr.data-field-type]="field.type"
6471
+ [class.ngx-field--readonly]="readonly"
6472
+ [class.ngx-field--masked]="masked"
6473
+ [class.ngx-field--invalid]="control && control.invalid && control.touched"
6474
+ >
6475
+ <div class="ngx-field__label-row">
6476
+ <label class="ngx-field__label" [attr.for]="domId()">
6477
+ {{ label }}
6478
+ @if (field.validators?.required) {
6479
+ <span class="ngx-field__req">*</span>
6480
+ }
6481
+ </label>
6482
+ @if (hint) {
6483
+ <span class="ngx-field__hint-wrap">
6484
+ <span class="ngx-field__hint-icon" aria-hidden="true">i</span>
6485
+ <span
6486
+ class="ngx-field__hint"
6487
+ role="tooltip"
6488
+ [attr.data-testid]="'field-' + field.id + '-hint'"
6489
+ [id]="domId('-hint')"
6490
+ >{{ hint }}</span
6491
+ >
6492
+ </span>
6493
+ }
6494
+ </div>
6495
+ @if (masked) {
6496
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
6497
+ maskedText
6498
+ }}</span>
6499
+ } @else if (readonly) {
6500
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">
6501
+ @if (dialHref) {
6502
+ <a [href]="dialHref" class="ngx-field__link">{{ control.value }}</a>
6503
+ } @else {
6504
+ {{ control.value || '—' }}
6505
+ }
6506
+ </span>
6507
+ } @else {
6508
+ <input
6509
+ [id]="domId()"
6510
+ class="ngx-field__input"
6511
+ [attr.data-testid]="'field-' + field.id + '-input'"
6512
+ type="tel"
6513
+ inputmode="tel"
6514
+ autocomplete="tel"
6515
+ [formControl]="$any(control)"
6516
+ [attr.aria-invalid]="control.invalid && control.touched"
6517
+ [attr.aria-describedby]="describedBy()"
6518
+ [placeholder]="placeholder"
6519
+ [attr.disabled]="field.disabled ? true : null"
6520
+ />
6521
+ @if (errorMessage) {
6522
+ <span
6523
+ class="ngx-field__error"
6524
+ [attr.data-testid]="'field-' + field.id + '-error'"
6525
+ [id]="domId('-error')"
6526
+ role="alert"
6527
+ >{{ errorMessage }}</span
6528
+ >
6529
+ }
6530
+ }
6531
+ </div>
6532
+ `, 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 });
6533
+ }
6534
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: PhoneFieldComponent, decorators: [{
6535
+ type: Component,
6536
+ args: [{
6537
+ changeDetection: ChangeDetectionStrategy.OnPush,
6538
+ selector: 'ngx-phone-field',
6539
+ standalone: true,
6540
+ imports: [ReactiveFormsModule],
6541
+ template: `
6542
+ <div
6543
+ class="ngx-field ngx-field--phone"
6544
+ [attr.data-testid]="'field-' + field.id"
6545
+ [attr.data-field-type]="field.type"
6546
+ [class.ngx-field--readonly]="readonly"
6547
+ [class.ngx-field--masked]="masked"
6548
+ [class.ngx-field--invalid]="control && control.invalid && control.touched"
6549
+ >
6550
+ <div class="ngx-field__label-row">
6551
+ <label class="ngx-field__label" [attr.for]="domId()">
6552
+ {{ label }}
6553
+ @if (field.validators?.required) {
6554
+ <span class="ngx-field__req">*</span>
6555
+ }
6556
+ </label>
6557
+ @if (hint) {
6558
+ <span class="ngx-field__hint-wrap">
6559
+ <span class="ngx-field__hint-icon" aria-hidden="true">i</span>
6560
+ <span
6561
+ class="ngx-field__hint"
6562
+ role="tooltip"
6563
+ [attr.data-testid]="'field-' + field.id + '-hint'"
6564
+ [id]="domId('-hint')"
6565
+ >{{ hint }}</span
6566
+ >
6567
+ </span>
6568
+ }
6569
+ </div>
6570
+ @if (masked) {
6571
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
6572
+ maskedText
6573
+ }}</span>
6574
+ } @else if (readonly) {
6575
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">
6576
+ @if (dialHref) {
6577
+ <a [href]="dialHref" class="ngx-field__link">{{ control.value }}</a>
6578
+ } @else {
6579
+ {{ control.value || '—' }}
6580
+ }
6581
+ </span>
6582
+ } @else {
6583
+ <input
6584
+ [id]="domId()"
6585
+ class="ngx-field__input"
6586
+ [attr.data-testid]="'field-' + field.id + '-input'"
6587
+ type="tel"
6588
+ inputmode="tel"
6589
+ autocomplete="tel"
6590
+ [formControl]="$any(control)"
6591
+ [attr.aria-invalid]="control.invalid && control.touched"
6592
+ [attr.aria-describedby]="describedBy()"
6593
+ [placeholder]="placeholder"
6594
+ [attr.disabled]="field.disabled ? true : null"
6595
+ />
6596
+ @if (errorMessage) {
6597
+ <span
6598
+ class="ngx-field__error"
6599
+ [attr.data-testid]="'field-' + field.id + '-error'"
6600
+ [id]="domId('-error')"
6601
+ role="alert"
6602
+ >{{ errorMessage }}</span
6603
+ >
6604
+ }
6605
+ }
6606
+ </div>
6607
+ `,
6608
+ }]
6609
+ }], propDecorators: { field: [{
6610
+ type: Input
6611
+ }], control: [{
6612
+ type: Input
6613
+ }], language: [{
6614
+ type: Input
6615
+ }], readonly: [{
6616
+ type: Input
6617
+ }], masked: [{
6618
+ type: Input
6619
+ }] } });
6620
+
6621
+ /**
6622
+ * Slider field: a range input over `validators.min`–`validators.max` in steps of `field.step`.
6623
+ *
6624
+ * A range input cannot show that it is empty. With no value it puts the thumb at the midpoint,
6625
+ * which looks exactly like a chosen value. The readout beside it is therefore the value itself,
6626
+ * and shows a dash until someone moves the thumb. Without it, a required slider nobody touched
6627
+ * would look answered and still fail at save.
6628
+ */
6629
+ class SliderFieldComponent {
6630
+ /**
6631
+ * Unique to this component instance: an `array` renders the same field once per row, and a
6632
+ * DOM id may not repeat. See `field-dom-id.ts`.
6633
+ */
6634
+ instanceId = nextFieldInstanceId();
6635
+ domId(suffix = '') {
6636
+ return fieldDomId(this.field, this.instanceId, suffix);
6637
+ }
6638
+ /** Overridable via MASKED_PLACEHOLDER; the default is the historic literal. */
6639
+ maskedText = inject(MASKED_PLACEHOLDER, { optional: true }) ?? 'XXXXXXXXX';
6640
+ messages = inject(ValidationMessagesService);
6641
+ field;
6642
+ control;
6643
+ language = 'en';
6644
+ readonly = false;
6645
+ masked = false;
6646
+ get label() {
6647
+ return resolveLabel(this.field?.label, this.language);
6648
+ }
6649
+ /** Author help text, shown under the control and named by `aria-describedby`. */
6650
+ get hint() {
6651
+ return resolveLabel(this.field?.hint, this.language);
6652
+ }
6653
+ get bounds() {
6654
+ return sliderBounds(this.field);
6655
+ }
6656
+ /** Whether the control holds a number. The thumb's position cannot say. */
6657
+ get hasValue() {
6658
+ const value = this.control?.value;
6659
+ return value !== null && value !== undefined && value !== '' && Number.isFinite(Number(value));
6660
+ }
6661
+ get display() {
6662
+ return this.hasValue ? String(this.control.value) : '—';
6663
+ }
6664
+ /** The ids of whatever is describing this control right now. */
6665
+ describedBy() {
6666
+ return fieldDescribedBy(s => this.domId(s), { hint: !!this.hint, error: !!this.errorMessage });
6667
+ }
6668
+ get errorMessage() {
6669
+ if (!this.control?.errors || !this.control.touched)
6670
+ return '';
6671
+ return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
6672
+ }
6673
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SliderFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
6674
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: SliderFieldComponent, isStandalone: true, selector: "ngx-slider-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
6675
+ <div
6676
+ class="ngx-field ngx-field--slider"
6677
+ [attr.data-testid]="'field-' + field.id"
6678
+ [attr.data-field-type]="field.type"
6679
+ [class.ngx-field--readonly]="readonly"
6680
+ [class.ngx-field--masked]="masked"
6681
+ [class.ngx-field--invalid]="control && control.invalid && control.touched"
6682
+ >
6683
+ <div class="ngx-field__label-row">
6684
+ <label class="ngx-field__label" [attr.for]="domId()">
6685
+ {{ label }}
6686
+ @if (field.validators?.required) {
6687
+ <span class="ngx-field__req">*</span>
6688
+ }
6689
+ </label>
6690
+ @if (hint) {
6691
+ <span class="ngx-field__hint-wrap">
6692
+ <span class="ngx-field__hint-icon" aria-hidden="true">i</span>
6693
+ <span
6694
+ class="ngx-field__hint"
6695
+ role="tooltip"
6696
+ [attr.data-testid]="'field-' + field.id + '-hint'"
6697
+ [id]="domId('-hint')"
6698
+ >{{ hint }}</span
6699
+ >
6700
+ </span>
6701
+ }
6702
+ </div>
6703
+ @if (masked) {
6704
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
6705
+ maskedText
6706
+ }}</span>
6707
+ } @else if (readonly) {
6708
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ display }}</span>
6709
+ } @else {
6710
+ <div class="ngx-field__slider-wrap">
6711
+ <input
6712
+ [id]="domId()"
6713
+ type="range"
6714
+ class="ngx-field__slider"
6715
+ [class.ngx-field__slider--unset]="!hasValue"
6716
+ [attr.data-testid]="'field-' + field.id + '-input'"
6717
+ [min]="bounds.min"
6718
+ [max]="bounds.max"
6719
+ [step]="bounds.step"
6720
+ [formControl]="$any(control)"
6721
+ [attr.aria-valuetext]="hasValue ? null : display"
6722
+ [attr.aria-invalid]="control.invalid && control.touched"
6723
+ [attr.aria-describedby]="describedBy()"
6724
+ [attr.disabled]="field.disabled ? true : null"
6725
+ />
6726
+ <output
6727
+ class="ngx-field__slider-value"
6728
+ [attr.for]="domId()"
6729
+ [attr.data-testid]="'field-' + field.id + '-readout'"
6730
+ aria-hidden="true"
6731
+ >{{ display }}</output
6732
+ >
6733
+ </div>
6734
+ @if (errorMessage) {
6735
+ <span
6736
+ class="ngx-field__error"
6737
+ [attr.data-testid]="'field-' + field.id + '-error'"
6738
+ [id]="domId('-error')"
6739
+ role="alert"
6740
+ >{{ errorMessage }}</span
6741
+ >
6742
+ }
6743
+ }
6744
+ </div>
6745
+ `, 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.RangeValueAccessor, selector: "input[type=range][formControlName],input[type=range][formControl],input[type=range][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 });
6746
+ }
6747
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SliderFieldComponent, decorators: [{
6748
+ type: Component,
6749
+ args: [{
6750
+ changeDetection: ChangeDetectionStrategy.OnPush,
6751
+ selector: 'ngx-slider-field',
6752
+ standalone: true,
6753
+ imports: [ReactiveFormsModule],
6754
+ template: `
6755
+ <div
6756
+ class="ngx-field ngx-field--slider"
6757
+ [attr.data-testid]="'field-' + field.id"
6758
+ [attr.data-field-type]="field.type"
6759
+ [class.ngx-field--readonly]="readonly"
6760
+ [class.ngx-field--masked]="masked"
6761
+ [class.ngx-field--invalid]="control && control.invalid && control.touched"
6762
+ >
6763
+ <div class="ngx-field__label-row">
6764
+ <label class="ngx-field__label" [attr.for]="domId()">
6765
+ {{ label }}
6766
+ @if (field.validators?.required) {
6767
+ <span class="ngx-field__req">*</span>
6768
+ }
6769
+ </label>
6770
+ @if (hint) {
6771
+ <span class="ngx-field__hint-wrap">
6772
+ <span class="ngx-field__hint-icon" aria-hidden="true">i</span>
6773
+ <span
6774
+ class="ngx-field__hint"
6775
+ role="tooltip"
6776
+ [attr.data-testid]="'field-' + field.id + '-hint'"
6777
+ [id]="domId('-hint')"
6778
+ >{{ hint }}</span
6779
+ >
6780
+ </span>
6781
+ }
6782
+ </div>
6783
+ @if (masked) {
6784
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
6785
+ maskedText
6786
+ }}</span>
6787
+ } @else if (readonly) {
6788
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">{{ display }}</span>
6789
+ } @else {
6790
+ <div class="ngx-field__slider-wrap">
6791
+ <input
6792
+ [id]="domId()"
6793
+ type="range"
6794
+ class="ngx-field__slider"
6795
+ [class.ngx-field__slider--unset]="!hasValue"
6796
+ [attr.data-testid]="'field-' + field.id + '-input'"
6797
+ [min]="bounds.min"
6798
+ [max]="bounds.max"
6799
+ [step]="bounds.step"
6800
+ [formControl]="$any(control)"
6801
+ [attr.aria-valuetext]="hasValue ? null : display"
6802
+ [attr.aria-invalid]="control.invalid && control.touched"
6803
+ [attr.aria-describedby]="describedBy()"
6804
+ [attr.disabled]="field.disabled ? true : null"
6805
+ />
6806
+ <output
6807
+ class="ngx-field__slider-value"
6808
+ [attr.for]="domId()"
6809
+ [attr.data-testid]="'field-' + field.id + '-readout'"
6810
+ aria-hidden="true"
6811
+ >{{ display }}</output
6812
+ >
6813
+ </div>
6814
+ @if (errorMessage) {
6815
+ <span
6816
+ class="ngx-field__error"
6817
+ [attr.data-testid]="'field-' + field.id + '-error'"
6818
+ [id]="domId('-error')"
6819
+ role="alert"
6820
+ >{{ errorMessage }}</span
6821
+ >
6822
+ }
6823
+ }
6824
+ </div>
6825
+ `,
6826
+ }]
6827
+ }], propDecorators: { field: [{
6828
+ type: Input
6829
+ }], control: [{
6830
+ type: Input
6831
+ }], language: [{
6832
+ type: Input
6833
+ }], readonly: [{
6834
+ type: Input
6835
+ }], masked: [{
6836
+ type: Input
6837
+ }] } });
6838
+
6839
+ /**
6840
+ * Rating field: 1 to `validators.max` stars (default 5), stored as a number.
6841
+ *
6842
+ * Built from native radio inputs in a fieldset, not clickable spans. That gives it the
6843
+ * behaviour a radio group already has: arrow keys move the choice, Tab enters and leaves the
6844
+ * group once, and a screen reader announces "3 of 5, radio button, 3 of 5". Each star's
6845
+ * label is text a host can translate (`ratingValue`), because a star glyph read aloud is
6846
+ * "black star".
6847
+ */
6848
+ class RatingFieldComponent {
6849
+ /**
6850
+ * Unique to this component instance: an `array` renders the same field once per row, and a
6851
+ * DOM id may not repeat. See `field-dom-id.ts`.
6852
+ */
6853
+ instanceId = nextFieldInstanceId();
6854
+ domId(suffix = '') {
6855
+ return fieldDomId(this.field, this.instanceId, suffix);
6856
+ }
6857
+ /** Overridable via MASKED_PLACEHOLDER; the default is the historic literal. */
6858
+ maskedText = inject(MASKED_PLACEHOLDER, { optional: true }) ?? 'XXXXXXXXX';
6859
+ messages = inject(ValidationMessagesService);
6860
+ cdr = inject(ChangeDetectorRef);
6861
+ ui = inject(UiTextService);
6862
+ field;
6863
+ language = 'en';
6864
+ readonly = false;
6865
+ masked = false;
6866
+ _control;
6867
+ _controlSub;
6868
+ /**
6869
+ * Subscribed because the radios are not bound through a form directive. A value patched in
6870
+ * from outside (a reset, an autoPatch) would otherwise leave an OnPush view showing the old
6871
+ * stars.
6872
+ */
6873
+ set control(value) {
6874
+ this._control = value;
6875
+ this._controlSub?.unsubscribe();
6876
+ this._controlSub = this._control?.valueChanges?.subscribe(() => this.cdr.markForCheck());
6877
+ }
6878
+ get control() {
6879
+ return this._control;
6880
+ }
6881
+ ngOnDestroy() {
6882
+ this._controlSub?.unsubscribe();
6883
+ }
6884
+ get label() {
6885
+ return resolveLabel(this.field?.label, this.language);
6886
+ }
6887
+ /** Author help text, shown under the control and named by `aria-describedby`. */
6888
+ get hint() {
6889
+ return resolveLabel(this.field?.hint, this.language);
6890
+ }
6891
+ get scale() {
6892
+ return ratingScale(this.field);
6893
+ }
6894
+ get stars() {
6895
+ return Array.from({ length: this.scale }, (_, i) => i + 1);
6896
+ }
6897
+ /** The stored rating, or null when there is none or it is not a star on this scale. */
6898
+ get current() {
6899
+ const value = Number(this.control?.value);
6900
+ const raw = this.control?.value;
6901
+ if (raw === null || raw === undefined || raw === '' || !Number.isInteger(value))
6902
+ return null;
6903
+ return value >= 1 && value <= this.scale ? value : null;
6904
+ }
6905
+ select(star) {
6906
+ this.control?.setValue(star);
6907
+ this.control?.markAsTouched();
6908
+ this.control?.markAsDirty();
6909
+ }
6910
+ clear() {
6911
+ this.control?.setValue(null);
6912
+ this.control?.markAsTouched();
6913
+ this.control?.markAsDirty();
6914
+ }
6915
+ /** The ids of whatever is describing this control right now. */
6916
+ describedBy() {
6917
+ return fieldDescribedBy(s => this.domId(s), { hint: !!this.hint, error: !!this.errorMessage });
6918
+ }
6919
+ get errorMessage() {
6920
+ if (!this.control?.errors || !this.control.touched)
6921
+ return '';
6922
+ return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
6923
+ }
6924
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: RatingFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
6925
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: RatingFieldComponent, isStandalone: true, selector: "ngx-rating-field", inputs: { field: "field", language: "language", readonly: "readonly", masked: "masked", control: "control" }, ngImport: i0, template: `
6926
+ <div
6927
+ class="ngx-field ngx-field--rating"
6928
+ [attr.data-testid]="'field-' + field.id"
6929
+ [attr.data-field-type]="field.type"
6930
+ [class.ngx-field--readonly]="readonly"
6931
+ [class.ngx-field--masked]="masked"
6932
+ [class.ngx-field--invalid]="control && control.invalid && control.touched"
6933
+ >
6934
+ <fieldset class="ngx-field__fieldset">
6935
+ <legend class="ngx-field__label">
6936
+ {{ label }}
6937
+ @if (field.validators?.required) {
6938
+ <span class="ngx-field__req">*</span>
6939
+ }
6940
+ @if (hint) {
6941
+ <!-- Inside the legend: a fieldset allows one legend, and it must come first. -->
6942
+ <span class="ngx-field__hint-wrap">
6943
+ <span class="ngx-field__hint-icon" aria-hidden="true">i</span>
6944
+ <span
6945
+ class="ngx-field__hint"
6946
+ role="tooltip"
6947
+ [attr.data-testid]="'field-' + field.id + '-hint'"
6948
+ [id]="domId('-hint')"
6949
+ >{{ hint }}</span
6950
+ >
6951
+ </span>
6952
+ }
6953
+ </legend>
6954
+ @if (masked) {
6955
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
6956
+ maskedText
6957
+ }}</span>
6958
+ } @else if (readonly) {
6959
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">
6960
+ @if (current) {
6961
+ <span class="ngx-field__stars" aria-hidden="true">
6962
+ @for (star of stars; track star) {
6963
+ <span class="ngx-field__star-glyph" [class.ngx-field__star-glyph--on]="star <= current">{{
6964
+ star <= current ? '★' : '☆'
6965
+ }}</span>
6966
+ }
6967
+ </span>
6968
+ {{ ui.text('ratingValue', language, { value: current, max: scale }) }}
6969
+ } @else {
6970
+ —
6971
+ }
6972
+ </span>
6973
+ } @else {
6974
+ <div class="ngx-field__rating">
6975
+ @for (star of stars; track star) {
6976
+ <label class="ngx-field__star" [class.ngx-field__star--on]="current !== null && star <= current">
6977
+ <input
6978
+ type="radio"
6979
+ class="ngx-field__star-input"
6980
+ [id]="domId('-' + star)"
6981
+ [name]="domId()"
6982
+ [value]="star"
6983
+ [checked]="current === star"
6984
+ [attr.data-testid]="'field-' + field.id + '-star-' + star"
6985
+ [attr.aria-describedby]="describedBy()"
6986
+ [attr.disabled]="field.disabled ? true : null"
6987
+ (change)="select(star)"
6988
+ />
6989
+ <!-- Filled and hollow, so the rating is readable without telling colours apart. -->
6990
+ <span class="ngx-field__star-glyph" aria-hidden="true">{{
6991
+ current !== null && star <= current ? '★' : '☆'
6992
+ }}</span>
6993
+ <span class="ngx-field__sr-only">{{ ui.text('ratingValue', language, { value: star, max: scale }) }}</span>
6994
+ </label>
6995
+ }
6996
+ @if (current !== null && !field.disabled) {
6997
+ <button
6998
+ type="button"
6999
+ class="ngx-field__remove-btn"
7000
+ [attr.data-testid]="'field-' + field.id + '-clear'"
7001
+ [attr.aria-label]="ui.text('clearValue', language, { field: label })"
7002
+ (click)="clear()"
7003
+ >
7004
+ ×
7005
+ </button>
7006
+ }
7007
+ </div>
7008
+ @if (errorMessage) {
7009
+ <span
7010
+ class="ngx-field__error"
7011
+ [attr.data-testid]="'field-' + field.id + '-error'"
7012
+ [id]="domId('-error')"
7013
+ role="alert"
7014
+ >{{ errorMessage }}</span
7015
+ >
7016
+ }
7017
+ }
7018
+ </fieldset>
7019
+ </div>
7020
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
7021
+ }
7022
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: RatingFieldComponent, decorators: [{
7023
+ type: Component,
7024
+ args: [{
7025
+ changeDetection: ChangeDetectionStrategy.OnPush,
7026
+ selector: 'ngx-rating-field',
7027
+ standalone: true,
7028
+ imports: [ReactiveFormsModule],
7029
+ template: `
7030
+ <div
7031
+ class="ngx-field ngx-field--rating"
7032
+ [attr.data-testid]="'field-' + field.id"
7033
+ [attr.data-field-type]="field.type"
7034
+ [class.ngx-field--readonly]="readonly"
7035
+ [class.ngx-field--masked]="masked"
7036
+ [class.ngx-field--invalid]="control && control.invalid && control.touched"
7037
+ >
7038
+ <fieldset class="ngx-field__fieldset">
7039
+ <legend class="ngx-field__label">
7040
+ {{ label }}
7041
+ @if (field.validators?.required) {
7042
+ <span class="ngx-field__req">*</span>
7043
+ }
7044
+ @if (hint) {
7045
+ <!-- Inside the legend: a fieldset allows one legend, and it must come first. -->
7046
+ <span class="ngx-field__hint-wrap">
7047
+ <span class="ngx-field__hint-icon" aria-hidden="true">i</span>
7048
+ <span
7049
+ class="ngx-field__hint"
7050
+ role="tooltip"
7051
+ [attr.data-testid]="'field-' + field.id + '-hint'"
7052
+ [id]="domId('-hint')"
7053
+ >{{ hint }}</span
7054
+ >
7055
+ </span>
7056
+ }
7057
+ </legend>
7058
+ @if (masked) {
7059
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
7060
+ maskedText
7061
+ }}</span>
7062
+ } @else if (readonly) {
7063
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">
7064
+ @if (current) {
7065
+ <span class="ngx-field__stars" aria-hidden="true">
7066
+ @for (star of stars; track star) {
7067
+ <span class="ngx-field__star-glyph" [class.ngx-field__star-glyph--on]="star <= current">{{
7068
+ star <= current ? '★' : '☆'
7069
+ }}</span>
7070
+ }
7071
+ </span>
7072
+ {{ ui.text('ratingValue', language, { value: current, max: scale }) }}
7073
+ } @else {
7074
+ —
7075
+ }
7076
+ </span>
7077
+ } @else {
7078
+ <div class="ngx-field__rating">
7079
+ @for (star of stars; track star) {
7080
+ <label class="ngx-field__star" [class.ngx-field__star--on]="current !== null && star <= current">
7081
+ <input
7082
+ type="radio"
7083
+ class="ngx-field__star-input"
7084
+ [id]="domId('-' + star)"
7085
+ [name]="domId()"
7086
+ [value]="star"
7087
+ [checked]="current === star"
7088
+ [attr.data-testid]="'field-' + field.id + '-star-' + star"
7089
+ [attr.aria-describedby]="describedBy()"
7090
+ [attr.disabled]="field.disabled ? true : null"
7091
+ (change)="select(star)"
7092
+ />
7093
+ <!-- Filled and hollow, so the rating is readable without telling colours apart. -->
7094
+ <span class="ngx-field__star-glyph" aria-hidden="true">{{
7095
+ current !== null && star <= current ? '★' : '☆'
7096
+ }}</span>
7097
+ <span class="ngx-field__sr-only">{{ ui.text('ratingValue', language, { value: star, max: scale }) }}</span>
7098
+ </label>
7099
+ }
7100
+ @if (current !== null && !field.disabled) {
7101
+ <button
7102
+ type="button"
7103
+ class="ngx-field__remove-btn"
7104
+ [attr.data-testid]="'field-' + field.id + '-clear'"
7105
+ [attr.aria-label]="ui.text('clearValue', language, { field: label })"
7106
+ (click)="clear()"
7107
+ >
7108
+ ×
7109
+ </button>
7110
+ }
7111
+ </div>
7112
+ @if (errorMessage) {
7113
+ <span
7114
+ class="ngx-field__error"
7115
+ [attr.data-testid]="'field-' + field.id + '-error'"
7116
+ [id]="domId('-error')"
7117
+ role="alert"
7118
+ >{{ errorMessage }}</span
7119
+ >
7120
+ }
7121
+ }
7122
+ </fieldset>
7123
+ </div>
7124
+ `,
7125
+ }]
7126
+ }], propDecorators: { field: [{
7127
+ type: Input
7128
+ }], language: [{
7129
+ type: Input
7130
+ }], readonly: [{
7131
+ type: Input
7132
+ }], masked: [{
7133
+ type: Input
7134
+ }], control: [{
7135
+ type: Input
7136
+ }] } });
7137
+
7138
+ /**
7139
+ * Colour field: a native colour picker, storing `#rrggbb`.
7140
+ *
7141
+ * Like a range input, a colour input has no empty state and shows black. The code beside it is
7142
+ * therefore the real value, a dash until a colour is chosen, and a clear button sets the value
7143
+ * back to nothing. Without them, "no colour" and "black" would look the same.
7144
+ *
7145
+ * Only a value that passes `normalizeHexColor` reaches a `style` binding. A record can hold
7146
+ * any string, and a colour swatch is no place to interpret one.
7147
+ */
7148
+ class ColorFieldComponent {
7149
+ /**
7150
+ * Unique to this component instance: an `array` renders the same field once per row, and a
7151
+ * DOM id may not repeat. See `field-dom-id.ts`.
7152
+ */
7153
+ instanceId = nextFieldInstanceId();
7154
+ domId(suffix = '') {
7155
+ return fieldDomId(this.field, this.instanceId, suffix);
7156
+ }
7157
+ /** Overridable via MASKED_PLACEHOLDER; the default is the historic literal. */
7158
+ maskedText = inject(MASKED_PLACEHOLDER, { optional: true }) ?? 'XXXXXXXXX';
7159
+ messages = inject(ValidationMessagesService);
7160
+ ui = inject(UiTextService);
7161
+ field;
7162
+ control;
7163
+ language = 'en';
7164
+ readonly = false;
7165
+ masked = false;
7166
+ get label() {
7167
+ return resolveLabel(this.field?.label, this.language);
7168
+ }
7169
+ /** Author help text, shown under the control and named by `aria-describedby`. */
7170
+ get hint() {
7171
+ return resolveLabel(this.field?.hint, this.language);
7172
+ }
7173
+ /** The stored colour as `#rrggbb`, or null when there is none or it is not a colour. */
7174
+ get hex() {
7175
+ return normalizeHexColor(this.control?.value);
7176
+ }
7177
+ clear() {
7178
+ this.control?.setValue(null);
7179
+ this.control?.markAsTouched();
7180
+ this.control?.markAsDirty();
7181
+ }
7182
+ /** The ids of whatever is describing this control right now. */
7183
+ describedBy() {
7184
+ return fieldDescribedBy(s => this.domId(s), { hint: !!this.hint, error: !!this.errorMessage });
7185
+ }
7186
+ get errorMessage() {
7187
+ if (!this.control?.errors || !this.control.touched)
7188
+ return '';
7189
+ return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
7190
+ }
7191
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: ColorFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
7192
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: ColorFieldComponent, isStandalone: true, selector: "ngx-color-field", inputs: { field: "field", control: "control", language: "language", readonly: "readonly", masked: "masked" }, ngImport: i0, template: `
7193
+ <div
7194
+ class="ngx-field ngx-field--color"
7195
+ [attr.data-testid]="'field-' + field.id"
7196
+ [attr.data-field-type]="field.type"
7197
+ [class.ngx-field--readonly]="readonly"
7198
+ [class.ngx-field--masked]="masked"
7199
+ [class.ngx-field--invalid]="control && control.invalid && control.touched"
7200
+ >
7201
+ <div class="ngx-field__label-row">
7202
+ <label class="ngx-field__label" [attr.for]="domId()">
7203
+ {{ label }}
7204
+ @if (field.validators?.required) {
7205
+ <span class="ngx-field__req">*</span>
7206
+ }
7207
+ </label>
7208
+ @if (hint) {
7209
+ <span class="ngx-field__hint-wrap">
7210
+ <span class="ngx-field__hint-icon" aria-hidden="true">i</span>
7211
+ <span
7212
+ class="ngx-field__hint"
7213
+ role="tooltip"
7214
+ [attr.data-testid]="'field-' + field.id + '-hint'"
7215
+ [id]="domId('-hint')"
7216
+ >{{ hint }}</span
7217
+ >
7218
+ </span>
7219
+ }
7220
+ </div>
7221
+ @if (masked) {
7222
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
7223
+ maskedText
7224
+ }}</span>
7225
+ } @else if (readonly) {
7226
+ <span class="ngx-field__value ngx-field__color-value" [attr.data-testid]="'field-' + field.id + '-value'">
7227
+ @if (hex) {
7228
+ <span class="ngx-field__swatch" [style.background-color]="hex" aria-hidden="true"></span>
7229
+ {{ hex }}
7230
+ } @else {
7231
+ {{ control.value || '—' }}
7232
+ }
7233
+ </span>
7234
+ } @else {
7235
+ <div class="ngx-field__color-wrap">
7236
+ <input
7237
+ [id]="domId()"
7238
+ type="color"
7239
+ class="ngx-field__color"
7240
+ [attr.data-testid]="'field-' + field.id + '-input'"
7241
+ [formControl]="$any(control)"
7242
+ [attr.aria-invalid]="control.invalid && control.touched"
7243
+ [attr.aria-describedby]="describedBy()"
7244
+ [attr.disabled]="field.disabled ? true : null"
7245
+ />
7246
+ <span class="ngx-field__color-code" [attr.data-testid]="'field-' + field.id + '-code'">{{ hex || '—' }}</span>
7247
+ @if (hex && !field.disabled) {
7248
+ <button
7249
+ type="button"
7250
+ class="ngx-field__remove-btn"
7251
+ [attr.data-testid]="'field-' + field.id + '-clear'"
7252
+ [attr.aria-label]="ui.text('clearValue', language, { field: label })"
7253
+ (click)="clear()"
7254
+ >
7255
+ ×
7256
+ </button>
7257
+ }
7258
+ </div>
7259
+ @if (errorMessage) {
7260
+ <span
7261
+ class="ngx-field__error"
7262
+ [attr.data-testid]="'field-' + field.id + '-error'"
7263
+ [id]="domId('-error')"
7264
+ role="alert"
7265
+ >{{ errorMessage }}</span
7266
+ >
7267
+ }
7268
+ }
7269
+ </div>
7270
+ `, 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 });
7271
+ }
7272
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: ColorFieldComponent, decorators: [{
7273
+ type: Component,
7274
+ args: [{
7275
+ changeDetection: ChangeDetectionStrategy.OnPush,
7276
+ selector: 'ngx-color-field',
7277
+ standalone: true,
7278
+ imports: [ReactiveFormsModule],
7279
+ template: `
7280
+ <div
7281
+ class="ngx-field ngx-field--color"
7282
+ [attr.data-testid]="'field-' + field.id"
7283
+ [attr.data-field-type]="field.type"
7284
+ [class.ngx-field--readonly]="readonly"
7285
+ [class.ngx-field--masked]="masked"
7286
+ [class.ngx-field--invalid]="control && control.invalid && control.touched"
7287
+ >
7288
+ <div class="ngx-field__label-row">
7289
+ <label class="ngx-field__label" [attr.for]="domId()">
7290
+ {{ label }}
7291
+ @if (field.validators?.required) {
7292
+ <span class="ngx-field__req">*</span>
7293
+ }
7294
+ </label>
7295
+ @if (hint) {
7296
+ <span class="ngx-field__hint-wrap">
7297
+ <span class="ngx-field__hint-icon" aria-hidden="true">i</span>
7298
+ <span
7299
+ class="ngx-field__hint"
7300
+ role="tooltip"
7301
+ [attr.data-testid]="'field-' + field.id + '-hint'"
7302
+ [id]="domId('-hint')"
7303
+ >{{ hint }}</span
7304
+ >
7305
+ </span>
7306
+ }
7307
+ </div>
7308
+ @if (masked) {
7309
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
7310
+ maskedText
7311
+ }}</span>
7312
+ } @else if (readonly) {
7313
+ <span class="ngx-field__value ngx-field__color-value" [attr.data-testid]="'field-' + field.id + '-value'">
7314
+ @if (hex) {
7315
+ <span class="ngx-field__swatch" [style.background-color]="hex" aria-hidden="true"></span>
7316
+ {{ hex }}
7317
+ } @else {
7318
+ {{ control.value || '—' }}
7319
+ }
7320
+ </span>
7321
+ } @else {
7322
+ <div class="ngx-field__color-wrap">
7323
+ <input
7324
+ [id]="domId()"
7325
+ type="color"
7326
+ class="ngx-field__color"
7327
+ [attr.data-testid]="'field-' + field.id + '-input'"
7328
+ [formControl]="$any(control)"
7329
+ [attr.aria-invalid]="control.invalid && control.touched"
7330
+ [attr.aria-describedby]="describedBy()"
7331
+ [attr.disabled]="field.disabled ? true : null"
7332
+ />
7333
+ <span class="ngx-field__color-code" [attr.data-testid]="'field-' + field.id + '-code'">{{ hex || '—' }}</span>
7334
+ @if (hex && !field.disabled) {
7335
+ <button
7336
+ type="button"
7337
+ class="ngx-field__remove-btn"
7338
+ [attr.data-testid]="'field-' + field.id + '-clear'"
7339
+ [attr.aria-label]="ui.text('clearValue', language, { field: label })"
7340
+ (click)="clear()"
7341
+ >
7342
+ ×
7343
+ </button>
7344
+ }
7345
+ </div>
7346
+ @if (errorMessage) {
7347
+ <span
7348
+ class="ngx-field__error"
7349
+ [attr.data-testid]="'field-' + field.id + '-error'"
7350
+ [id]="domId('-error')"
7351
+ role="alert"
7352
+ >{{ errorMessage }}</span
7353
+ >
7354
+ }
7355
+ }
7356
+ </div>
7357
+ `,
7358
+ }]
7359
+ }], propDecorators: { field: [{
7360
+ type: Input
7361
+ }], control: [{
7362
+ type: Input
7363
+ }], language: [{
7364
+ type: Input
7365
+ }], readonly: [{
7366
+ type: Input
7367
+ }], masked: [{
7368
+ type: Input
7369
+ }] } });
7370
+
7371
+ /** What ends a tag while typing, or separates several in pasted text. */
7372
+ const TAG_SEPARATOR = /[,;\n]/;
7373
+ /**
7374
+ * Tags field: free text entered as a list of chips, stored as `string[]`.
7375
+ *
7376
+ * Unlike `multiSelect`, the values are whatever the user types, so there is no option list to
7377
+ * keep in step. Enter or a comma ends a tag, Backspace in an empty box removes the last one,
7378
+ * and pasted text is split on commas, semicolons and line breaks. Leaving the box commits
7379
+ * what is in it, so a tag someone typed and then tabbed away from is not silently lost.
7380
+ *
7381
+ * The control holds the array. The text box is only a draft, which is why it is not bound
7382
+ * with a form directive.
7383
+ */
7384
+ class TagsFieldComponent {
7385
+ /**
7386
+ * Unique to this component instance: an `array` renders the same field once per row, and a
7387
+ * DOM id may not repeat. See `field-dom-id.ts`.
7388
+ */
7389
+ instanceId = nextFieldInstanceId();
7390
+ domId(suffix = '') {
7391
+ return fieldDomId(this.field, this.instanceId, suffix);
7392
+ }
7393
+ /** Overridable via MASKED_PLACEHOLDER; the default is the historic literal. */
7394
+ maskedText = inject(MASKED_PLACEHOLDER, { optional: true }) ?? 'XXXXXXXXX';
7395
+ messages = inject(ValidationMessagesService);
7396
+ cdr = inject(ChangeDetectorRef);
7397
+ ui = inject(UiTextService);
7398
+ field;
7399
+ language = 'en';
7400
+ readonly = false;
7401
+ masked = false;
7402
+ _control;
7403
+ _controlSub;
7404
+ /** Subscribed for the same reason as the rating field: nothing here is a form directive. */
7405
+ set control(value) {
7406
+ this._control = value;
7407
+ this._controlSub?.unsubscribe();
7408
+ this._controlSub = this._control?.valueChanges?.subscribe(() => this.cdr.markForCheck());
7409
+ }
7410
+ get control() {
7411
+ return this._control;
7412
+ }
7413
+ ngOnDestroy() {
7414
+ this._controlSub?.unsubscribe();
7415
+ }
7416
+ get label() {
7417
+ return resolveLabel(this.field?.label, this.language);
7418
+ }
7419
+ get placeholder() {
7420
+ return resolveLabel(this.field?.placeholder, this.language);
7421
+ }
7422
+ /** Author help text, shown under the control and named by `aria-describedby`. */
7423
+ get hint() {
7424
+ return resolveLabel(this.field?.hint, this.language);
7425
+ }
7426
+ /** The stored tags, read leniently. A record may hold a single string or anything else. */
7427
+ get tags() {
7428
+ return normalizeTags(this.control?.value);
7429
+ }
7430
+ onKeydown(event, entry) {
7431
+ if (event.key === 'Enter' || event.key === ',' || event.key === ';') {
7432
+ // Enter inside a form submits it. Adding a tag is what the user meant.
7433
+ event.preventDefault();
7434
+ this.commit(entry);
7435
+ }
7436
+ else if (event.key === 'Backspace' && entry.value === '' && this.tags.length) {
7437
+ this.remove(this.tags[this.tags.length - 1]);
7438
+ }
7439
+ }
7440
+ onPaste(event, entry) {
7441
+ const text = event.clipboardData?.getData('text') ?? '';
7442
+ if (!TAG_SEPARATOR.test(text))
7443
+ return;
7444
+ event.preventDefault();
7445
+ // Split here, not by writing the text into the box first: a single-line input strips line
7446
+ // breaks on assignment, which would join "z\nw" into one tag.
7447
+ const draft = entry.value;
7448
+ entry.value = '';
7449
+ this.add([draft, ...text.split(TAG_SEPARATOR)]);
7450
+ }
7451
+ onBlur(entry) {
7452
+ this.commit(entry);
7453
+ this.control?.markAsTouched();
7454
+ }
7455
+ /** Add whatever is in the draft box, split on separators, and empty it. */
7456
+ commit(entry) {
7457
+ const added = entry.value.split(TAG_SEPARATOR);
7458
+ entry.value = '';
7459
+ this.add(added);
7460
+ }
7461
+ add(candidates) {
7462
+ const next = normalizeTags([...this.tags, ...candidates]);
7463
+ if (next.length !== this.tags.length)
7464
+ this.write(next);
7465
+ }
7466
+ remove(tag) {
7467
+ this.write(this.tags.filter(t => t !== tag));
7468
+ }
7469
+ /**
7470
+ * An empty list is stored as `[]`, not `null`. `required` treats both as empty, and `[]`
7471
+ * stays the same shape as every other value this field holds.
7472
+ */
7473
+ write(tags) {
7474
+ this.control?.setValue(tags);
7475
+ this.control?.markAsTouched();
7476
+ this.control?.markAsDirty();
7477
+ }
7478
+ /** The ids of whatever is describing this control right now. */
7479
+ describedBy() {
7480
+ return fieldDescribedBy(s => this.domId(s), { hint: !!this.hint, error: !!this.errorMessage });
7481
+ }
7482
+ get errorMessage() {
7483
+ if (!this.control?.errors || !this.control.touched)
7484
+ return '';
7485
+ return this.messages.resolveForField(this.control.errors, this.language, this.field.type);
7486
+ }
7487
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: TagsFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
7488
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: TagsFieldComponent, isStandalone: true, selector: "ngx-tags-field", inputs: { field: "field", language: "language", readonly: "readonly", masked: "masked", control: "control" }, ngImport: i0, template: `
7489
+ <div
7490
+ class="ngx-field ngx-field--tags"
7491
+ [attr.data-testid]="'field-' + field.id"
7492
+ [attr.data-field-type]="field.type"
7493
+ [class.ngx-field--readonly]="readonly"
7494
+ [class.ngx-field--masked]="masked"
7495
+ [class.ngx-field--invalid]="control && control.invalid && control.touched"
7496
+ >
7497
+ <div class="ngx-field__label-row">
7498
+ <label class="ngx-field__label" [attr.for]="domId()">
7499
+ {{ label }}
7500
+ @if (field.validators?.required) {
7501
+ <span class="ngx-field__req">*</span>
7502
+ }
7503
+ </label>
7504
+ @if (hint) {
7505
+ <span class="ngx-field__hint-wrap">
7506
+ <span class="ngx-field__hint-icon" aria-hidden="true">i</span>
7507
+ <span
7508
+ class="ngx-field__hint"
7509
+ role="tooltip"
7510
+ [attr.data-testid]="'field-' + field.id + '-hint'"
7511
+ [id]="domId('-hint')"
7512
+ >{{ hint }}</span
7513
+ >
7514
+ </span>
7515
+ }
7516
+ </div>
7517
+ @if (masked) {
7518
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
7519
+ maskedText
7520
+ }}</span>
7521
+ } @else if (readonly) {
7522
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">
7523
+ @if (tags.length) {
7524
+ <span class="ngx-field__tags ngx-field__tags--readonly">
7525
+ @for (tag of tags; track tag) {
7526
+ <span class="ngx-field__tag">{{ tag }}</span>
7527
+ }
7528
+ </span>
7529
+ } @else {
7530
+ —
7531
+ }
7532
+ </span>
7533
+ } @else {
7534
+ <div class="ngx-field__tags" [class.ngx-field__tags--disabled]="field.disabled">
7535
+ @for (tag of tags; track tag) {
7536
+ <span class="ngx-field__tag" [attr.data-testid]="'field-' + field.id + '-tag'">
7537
+ {{ tag }}
7538
+ @if (!field.disabled) {
7539
+ <button
7540
+ type="button"
7541
+ class="ngx-field__tag-remove"
7542
+ [attr.aria-label]="ui.text('removeTag', language, { tag: tag })"
7543
+ (click)="remove(tag)"
7544
+ >
7545
+ ×
7546
+ </button>
7547
+ }
7548
+ </span>
7549
+ }
7550
+ <input
7551
+ #entry
7552
+ [id]="domId()"
7553
+ type="text"
7554
+ class="ngx-field__tags-input"
7555
+ [attr.data-testid]="'field-' + field.id + '-input'"
7556
+ [attr.aria-invalid]="control.invalid && control.touched"
7557
+ [attr.aria-describedby]="describedBy()"
7558
+ [placeholder]="placeholder || ui.text('addTagPlaceholder', language)"
7559
+ [attr.disabled]="field.disabled ? true : null"
7560
+ (keydown)="onKeydown($event, entry)"
7561
+ (paste)="onPaste($event, entry)"
7562
+ (blur)="onBlur(entry)"
7563
+ />
7564
+ </div>
7565
+ @if (errorMessage) {
7566
+ <span
7567
+ class="ngx-field__error"
7568
+ [attr.data-testid]="'field-' + field.id + '-error'"
7569
+ [id]="domId('-error')"
7570
+ role="alert"
7571
+ >{{ errorMessage }}</span
7572
+ >
7573
+ }
7574
+ }
7575
+ </div>
7576
+ `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
7577
+ }
7578
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: TagsFieldComponent, decorators: [{
7579
+ type: Component,
7580
+ args: [{
7581
+ changeDetection: ChangeDetectionStrategy.OnPush,
7582
+ selector: 'ngx-tags-field',
7583
+ standalone: true,
7584
+ template: `
7585
+ <div
7586
+ class="ngx-field ngx-field--tags"
7587
+ [attr.data-testid]="'field-' + field.id"
7588
+ [attr.data-field-type]="field.type"
7589
+ [class.ngx-field--readonly]="readonly"
7590
+ [class.ngx-field--masked]="masked"
7591
+ [class.ngx-field--invalid]="control && control.invalid && control.touched"
7592
+ >
7593
+ <div class="ngx-field__label-row">
7594
+ <label class="ngx-field__label" [attr.for]="domId()">
7595
+ {{ label }}
7596
+ @if (field.validators?.required) {
7597
+ <span class="ngx-field__req">*</span>
7598
+ }
7599
+ </label>
7600
+ @if (hint) {
7601
+ <span class="ngx-field__hint-wrap">
7602
+ <span class="ngx-field__hint-icon" aria-hidden="true">i</span>
7603
+ <span
7604
+ class="ngx-field__hint"
7605
+ role="tooltip"
7606
+ [attr.data-testid]="'field-' + field.id + '-hint'"
7607
+ [id]="domId('-hint')"
7608
+ >{{ hint }}</span
7609
+ >
7610
+ </span>
7611
+ }
7612
+ </div>
7613
+ @if (masked) {
7614
+ <span class="ngx-field__value ngx-field__value--masked" [attr.data-testid]="'field-' + field.id + '-masked'">{{
7615
+ maskedText
7616
+ }}</span>
7617
+ } @else if (readonly) {
7618
+ <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">
7619
+ @if (tags.length) {
7620
+ <span class="ngx-field__tags ngx-field__tags--readonly">
7621
+ @for (tag of tags; track tag) {
7622
+ <span class="ngx-field__tag">{{ tag }}</span>
7623
+ }
7624
+ </span>
7625
+ } @else {
7626
+ —
7627
+ }
7628
+ </span>
7629
+ } @else {
7630
+ <div class="ngx-field__tags" [class.ngx-field__tags--disabled]="field.disabled">
7631
+ @for (tag of tags; track tag) {
7632
+ <span class="ngx-field__tag" [attr.data-testid]="'field-' + field.id + '-tag'">
7633
+ {{ tag }}
7634
+ @if (!field.disabled) {
7635
+ <button
7636
+ type="button"
7637
+ class="ngx-field__tag-remove"
7638
+ [attr.aria-label]="ui.text('removeTag', language, { tag: tag })"
7639
+ (click)="remove(tag)"
7640
+ >
7641
+ ×
7642
+ </button>
7643
+ }
7644
+ </span>
7645
+ }
7646
+ <input
7647
+ #entry
7648
+ [id]="domId()"
7649
+ type="text"
7650
+ class="ngx-field__tags-input"
7651
+ [attr.data-testid]="'field-' + field.id + '-input'"
7652
+ [attr.aria-invalid]="control.invalid && control.touched"
7653
+ [attr.aria-describedby]="describedBy()"
7654
+ [placeholder]="placeholder || ui.text('addTagPlaceholder', language)"
7655
+ [attr.disabled]="field.disabled ? true : null"
7656
+ (keydown)="onKeydown($event, entry)"
7657
+ (paste)="onPaste($event, entry)"
7658
+ (blur)="onBlur(entry)"
7659
+ />
7660
+ </div>
7661
+ @if (errorMessage) {
7662
+ <span
7663
+ class="ngx-field__error"
7664
+ [attr.data-testid]="'field-' + field.id + '-error'"
7665
+ [id]="domId('-error')"
7666
+ role="alert"
7667
+ >{{ errorMessage }}</span
7668
+ >
7669
+ }
7670
+ }
7671
+ </div>
7672
+ `,
7673
+ }]
7674
+ }], propDecorators: { field: [{
7675
+ type: Input
7676
+ }], language: [{
7677
+ type: Input
7678
+ }], readonly: [{
7679
+ type: Input
7680
+ }], masked: [{
7681
+ type: Input
7682
+ }], control: [{
7683
+ type: Input
7684
+ }] } });
7685
+
7686
+ /**
7687
+ * Field-type registration — the tree-shaking seam.
7688
+ *
7689
+ * `FieldRegistryService` imports no components. Whatever you register here is what gets
7690
+ * bundled, so an app that only uses text/number/dropdown pays for three components and
7691
+ * their imports, not twenty-seven.
7692
+ *
7693
+ * ```ts
7694
+ * // Everything (convenient; bundles all built-ins):
7695
+ * provideNgxDynamicEntity(), provideBuiltInFieldTypes()
7696
+ *
7697
+ * // Only what this app renders:
7698
+ * provideFieldTypes({ text: TextFieldComponent, number: NumberFieldComponent })
7699
+ * ```
7700
+ *
7701
+ * Both merge into `FIELD_TYPE_REGISTRY`; later registrations win on key collision.
7702
+ */
7703
+ /** Map of every built-in field type key → component. Referencing this pulls in all of them. */
7704
+ function builtInFieldTypes() {
7705
+ return {
7706
+ text: TextFieldComponent,
7707
+ textarea: TextareaFieldComponent,
7708
+ markdown: MarkdownFieldComponent,
7709
+ number: NumberFieldComponent,
7710
+ currency: CurrencyFieldComponent,
7711
+ email: EmailFieldComponent,
7712
+ password: PasswordFieldComponent,
7713
+ checkbox: CheckboxFieldComponent,
7714
+ boolean: BooleanFieldComponent,
7715
+ date: DateFieldComponent,
7716
+ datetime: DateTimeFieldComponent,
7717
+ time: TimeFieldComponent,
7718
+ monthYear: MonthYearFieldComponent,
7719
+ dropdown: DropdownFieldComponent,
7720
+ radio: RadioFieldComponent,
7721
+ multiSelect: MultiSelectFieldComponent,
7722
+ 'entity-ref': EntityRefFieldComponent,
7723
+ group: GroupFieldComponent,
7724
+ array: ArrayFieldComponent,
7725
+ image: ImageFieldComponent,
7726
+ file: FileFieldComponent,
7727
+ url: UrlFieldComponent,
7728
+ phone: PhoneFieldComponent,
7729
+ slider: SliderFieldComponent,
7730
+ rating: RatingFieldComponent,
7731
+ color: ColorFieldComponent,
7732
+ tags: TagsFieldComponent,
7733
+ };
7734
+ }
7735
+ /** Register a specific set of field types. Only the components you name are bundled. */
7736
+ function provideFieldTypes(types) {
7737
+ return makeEnvironmentProviders([
7738
+ { provide: FIELD_TYPE_SETS, useValue: types, multi: true },
7739
+ ]);
7740
+ }
7741
+ /** Register all 27 built-in field type keys. Convenience for apps that use most of them. */
7742
+ function provideBuiltInFieldTypes() {
7743
+ return provideFieldTypes(builtInFieldTypes());
7744
+ }
7745
+
7746
+ /**
7747
+ * HookRegistryService — stores and runs Angular-side pre/post hooks.
7748
+ * Consumers register hooks via provideNgxDynamicEntity({ hooks: { 'clients:beforeSave': fn } }).
7749
+ */
7750
+ class HookRegistryService {
7751
+ registry = inject(HOOK_REGISTRY, { optional: true }) ?? new Map();
7752
+ has(key) {
7753
+ return this.registry.has(key);
7754
+ }
7755
+ async run(key, data, context = {}) {
7756
+ const hook = this.registry.get(key);
7757
+ if (!hook)
7758
+ return data;
7759
+ return hook(data, context);
7760
+ }
7761
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: HookRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
7762
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: HookRegistryService, providedIn: 'root' });
7763
+ }
7764
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: HookRegistryService, decorators: [{
7765
+ type: Injectable,
7766
+ args: [{ providedIn: 'root' }]
7767
+ }] });
7768
+
7769
+ /**
7770
+ * RulesEvaluationService — evaluates rules, and owns what a result *means*.
7771
+ *
7772
+ * Precedence lives in one method that both the render filter and the validity sync go
7773
+ * through, so what is on screen and what counts toward validity cannot disagree. Stateless —
7774
+ * every method takes the result it is interpreting, so there is nothing to go stale against a
7775
+ * rebuilt form and nothing to reset between configs.
7776
+ */
7777
+ class RulesEvaluationService {
7778
+ /**
7779
+ * Evaluate rules against form values and optional original baseline values.
7780
+ */
7781
+ evaluate(rules, values, originalValues, options) {
7782
+ return evaluateFormRules(rules, values, originalValues, options);
7783
+ }
7784
+ /**
7785
+ * Filter rules pertinent to a single tab.
7786
+ */
7787
+ filterForTab(rules, tabId, config) {
7788
+ return filterRulesForTab(rules, tabId, config);
7789
+ }
7790
+ /**
7791
+ * Whether a field renders, under the precedence documented on `RuleEvaluationResult`.
7792
+ *
7793
+ * An explicit hide beats everything. A show beats static hiding — `visibility: false`, or a
7794
+ * `showWhen` that does not currently hold — because a rule that could only hide what the
7795
+ * config already hides would have nothing to say, and `{ visibility: true }` was a
7796
+ * documented no-op until it did. Otherwise the config's own visibility decides.
7797
+ */
7798
+ isFieldVisible(result, field, names, values) {
7799
+ const sets = this.nameSets(result);
7800
+ if (names.some(name => sets.hidden.has(name)))
7801
+ return false;
7802
+ if (names.some(name => sets.shown.has(name)))
7803
+ return true;
7804
+ return evaluateFieldVisibility(field, values);
7805
+ }
7806
+ /** The same precedence for a tab. */
7807
+ isTabVisible(result, tab) {
7808
+ if (result.hiddenTabs.includes(tab.id))
7809
+ return false;
7810
+ return tab.visibility !== false || result.shownTabs.includes(tab.id);
7811
+ }
7812
+ /**
7813
+ * Keep a hidden field's control out of form validity.
7814
+ *
7815
+ * A field hidden by a rule or a `showWhen` is filtered out of the render, but its
7816
+ * validators stay attached — so a hidden *required* field holds `form.invalid` true
7817
+ * forever, permanently disabling Save with nothing on screen to explain why.
7818
+ *
7819
+ * Disabling is the fix rather than stripping validators: Angular excludes disabled controls
7820
+ * from validity, and the field's own validators survive intact for when it comes back.
7821
+ * Values are untouched — a disabled control still carries its value, so rule evaluation and
7822
+ * the emitted record see exactly what they saw before. That also keeps this from feeding
7823
+ * back on itself: hiding a field cannot change the values the rules are evaluated over.
7824
+ *
7825
+ * The predicate is `isFieldVisible`, the same one the render filter uses, so what is on
7826
+ * screen and what counts toward validity cannot drift apart.
7827
+ *
7828
+ * Entries must arrive parent-first, which is what `collectFieldScopes` yields: a container's
7829
+ * children are skipped once the container itself is hidden, because Angular's `disable()`
7830
+ * cascades to descendants and a child's `enable()` walks back up recalculating ancestors.
7831
+ * Toggling a child inside a hidden group would fight its parent.
7832
+ */
7833
+ syncHiddenFieldState(entries, ctx) {
7834
+ const hiddenContainers = [];
7835
+ for (const entry of entries) {
7836
+ const field = entry.field;
7837
+ if (!field?.id)
7838
+ continue;
7839
+ const address = ctx.addressOf(field);
7840
+ if (hiddenContainers.some(prefix => address.startsWith(`${prefix}.`)))
7841
+ continue;
7842
+ const hidden = !this.isFieldVisible(ctx.result, field, ctx.namesOf(field), ctx.values);
7843
+ if (hidden && (field.type === 'group' || field.type === 'array'))
7844
+ hiddenContainers.push(address);
7845
+ const ctrl = ctx.controlFor(entry);
7846
+ if (!ctrl)
7847
+ continue;
7848
+ if (hidden) {
7849
+ if (ctrl.enabled)
7850
+ ctrl.disable({ emitEvent: false });
7851
+ }
7852
+ else if (ctrl.disabled && !field.disabled) {
7853
+ ctrl.enable({ emitEvent: false });
7854
+ }
7855
+ }
7856
+ }
7857
+ /**
7858
+ * The hidden and shown names as sets, memoised against the result they came from.
7859
+ *
7860
+ * `isFieldVisible` runs once per field per change-detection pass; building two sets from
7861
+ * two arrays each time turns a rule result into allocation proportional to the form. A
7862
+ * `WeakMap` keyed by the result means it is built once per evaluation and collected with it.
7863
+ */
7864
+ nameSets(result) {
7865
+ let sets = this.nameSetCache.get(result);
7866
+ if (!sets) {
7867
+ sets = { hidden: new Set(result.hiddenFields), shown: new Set(result.shownFields) };
7868
+ this.nameSetCache.set(result, sets);
7869
+ }
7870
+ return sets;
7871
+ }
7872
+ nameSetCache = new WeakMap();
7873
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: RulesEvaluationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
7874
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: RulesEvaluationService, providedIn: 'root' });
7875
+ }
7876
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: RulesEvaluationService, decorators: [{
7877
+ type: Injectable,
7878
+ args: [{ providedIn: 'root' }]
7879
+ }] });
7880
+
7881
+ class ConfigSourceService {
7882
+ cache = new Map();
7883
+ inFlight = new Map();
7884
+ handler;
7885
+ constructor(handler) {
7886
+ this.handler = handler;
7887
+ }
7888
+ /** Retrieve an entity config by key, with caching and in-flight deduplication. */
7889
+ async getConfig(entityKey) {
7890
+ if (!entityKey || !this.handler)
7891
+ return undefined;
7892
+ if (this.cache.has(entityKey))
7893
+ return this.cache.get(entityKey);
7894
+ if (this.inFlight.has(entityKey))
7895
+ return this.inFlight.get(entityKey);
7896
+ const promise = (async () => {
7897
+ try {
7898
+ const raw = this.handler(entityKey);
7899
+ let resolved;
7900
+ if (isObservable(raw)) {
7901
+ resolved = await firstValueFrom(raw);
6480
7902
  }
6481
7903
  else {
6482
7904
  resolved = await Promise.resolve(raw);
@@ -6506,10 +7928,10 @@ class ConfigSourceService {
6506
7928
  this.cache.clear();
6507
7929
  }
6508
7930
  }
6509
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: ConfigSourceService, deps: [{ token: CONFIG_SOURCE, optional: true }], target: i0.ɵɵFactoryTarget.Injectable });
6510
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: ConfigSourceService, providedIn: 'root' });
7931
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: ConfigSourceService, deps: [{ token: CONFIG_SOURCE, optional: true }], target: i0.ɵɵFactoryTarget.Injectable });
7932
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: ConfigSourceService, providedIn: 'root' });
6511
7933
  }
6512
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: ConfigSourceService, decorators: [{
7934
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: ConfigSourceService, decorators: [{
6513
7935
  type: Injectable,
6514
7936
  args: [{ providedIn: 'root' }]
6515
7937
  }], ctorParameters: () => [{ type: undefined, decorators: [{
@@ -6524,9 +7946,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
6524
7946
  *
6525
7947
  * The one question this answers is *where a value lives*: which control a field owns, how
6526
7948
  * the tab tree nests into groups, how a record is read out of the tree and patched back in.
6527
- * It was half of `DynamicFormComponent`, mixed in with tab state, focus management, rule
6528
- * evaluation and RBAC — and the cost of that was not length but ownership: the bug Phase 2
6529
- * fixed was two methods forty lines apart disagreeing about what "every field" meant.
7949
+ * Anything that has to find a field's control goes through here, so there is one answer.
6530
7950
  *
6531
7951
  * Deliberately stateless. Every method takes the form and the config it is operating on, so
6532
7952
  * there is no cached `this.form` to go stale against a rebuilt one, and every method is
@@ -6618,6 +8038,44 @@ class FormStructureService {
6618
8038
  }
6619
8039
  return curr instanceof FormGroup ? curr : null;
6620
8040
  }
8041
+ /**
8042
+ * The control a scope entry addresses, found by its path rather than its bare id.
8043
+ *
8044
+ * The control tree mirrors the scope path exactly — a tab nests by its id and a `group`
8045
+ * under its own — so `form.get(path)` names exactly one control. A bare-id lookup falls
8046
+ * back to a first-match search and, with `address` on two tabs, finds the wrong one.
8047
+ *
8048
+ * A field inside an `array` has no static path: its controls live in `FormArray` rows
8049
+ * built per row. `form.get()` returns null for those, and callers leave them alone.
8050
+ */
8051
+ controlAt(form, entry) {
8052
+ return form?.get(fieldRefFor(entry.scope, entry.field.id)) ?? null;
8053
+ }
8054
+ /**
8055
+ * The value map rules and `showWhen` are evaluated against.
8056
+ *
8057
+ * Every field appears under two keys: its bare id, and its ref wrapped in brackets. The
8058
+ * bare id is what most configs use, but ids are unique only per scope, so when two scopes
8059
+ * define one the last field walked wins. `[personal.address]` names one field and cannot
8060
+ * be ambiguous, which is why a rule that has to tell them apart uses the ref.
8061
+ *
8062
+ * Both live in one flat map on purpose: `evaluateFormRules` takes a `Record<string,
8063
+ * unknown>` and needs no knowledge of refs at all — the extra keys simply resolve.
8064
+ */
8065
+ flattenValues(form, entries) {
8066
+ const out = {};
8067
+ for (const entry of entries) {
8068
+ const field = entry.field;
8069
+ if (!field?.id)
8070
+ continue;
8071
+ const ctrl = this.controlAt(form, entry);
8072
+ if (!ctrl)
8073
+ continue;
8074
+ out[field.id] = ctrl.value;
8075
+ out[toRefToken(refOf(field, entry.scope))] = ctrl.value;
8076
+ }
8077
+ return out;
8078
+ }
6621
8079
  /**
6622
8080
  * Find a control by reference: a bracketed path, or a bare field id.
6623
8081
  *
@@ -6761,10 +8219,10 @@ class FormStructureService {
6761
8219
  fieldsById.has(key) &&
6762
8220
  !patchedFieldIds.has(key));
6763
8221
  }
6764
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: FormStructureService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
6765
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: FormStructureService });
8222
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: FormStructureService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
8223
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: FormStructureService });
6766
8224
  }
6767
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: FormStructureService, decorators: [{
8225
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: FormStructureService, decorators: [{
6768
8226
  type: Injectable
6769
8227
  }] });
6770
8228
 
@@ -6796,6 +8254,12 @@ const AUTO_COL_SPAN = {
6796
8254
  boolean: 4,
6797
8255
  checkbox: 4,
6798
8256
  'entity-ref': 6,
8257
+ url: 6,
8258
+ phone: 6,
8259
+ slider: 6,
8260
+ rating: 4,
8261
+ color: 4,
8262
+ tags: 6,
6799
8263
  // Left at the full width deliberately — textarea, markdown, image, file, group and array
6800
8264
  // each hold something a half-row would crop.
6801
8265
  };
@@ -6937,9 +8401,8 @@ class DynamicFormComponent {
6937
8401
  // ─── Computed ─────────────────────────────────────────────────────────────
6938
8402
  ruleResult = computed(() => this.rulesEvaluation.evaluate(this.rules, this.formValues(), this.baseline(), {
6939
8403
  // The engine reports rather than logs, because it also runs on a server. This is the
6940
- // client's answer: say it once per distinct problem, in dev only. A rule the author
6941
- // half-finished used to throw out of change detection; now it is skipped, and a
6942
- // silently skipped rule is exactly as hard to diagnose as a thrown one.
8404
+ // client's answer: say it once per distinct problem, in dev only — a silently skipped
8405
+ // rule is as hard to diagnose as a thrown one.
6943
8406
  onProblem: message => this.warnOnce(message),
6944
8407
  }), ...(ngDevMode ? [{ debugName: "ruleResult" }] : /* istanbul ignore next */ []));
6945
8408
  /** Warned-about rule problems, per component instance — see `warnedAmbiguousIds`. */
@@ -7056,8 +8519,7 @@ class DynamicFormComponent {
7056
8519
  /**
7057
8520
  * Whether the current roles may see this record at all.
7058
8521
  *
7059
- * `permissions.view` used to be computed and thrown away — a user whose roles failed it
7060
- * still received the complete form with every value in the DOM. It is honoured now.
8522
+ * A user whose roles fail `permissions.view` gets no form, so no value reaches the DOM.
7061
8523
  *
7062
8524
  * As with masking, this is presentational: it stops the browser rendering data, it does
7063
8525
  * not stop the data reaching the browser. Authorize on the server.
@@ -7115,13 +8577,11 @@ class DynamicFormComponent {
7115
8577
  /**
7116
8578
  * Whether the Save button is unavailable.
7117
8579
  *
7118
- * This used to be `submitBlocked`, so an invalid form greyed Save out — and a greyed button
7119
- * is the worst possible answer to "why can't I save?". It cannot be clicked, so there is no
7120
- * moment at which the form gets to say which field is at fault or which tab it is on; the
7121
- * user is left comparing a disabled button against a form that looks, to them, filled in.
7122
- * On a tabbed form the offending field is usually not even on screen.
8580
+ * Deliberately not `submitBlocked`: a greyed button is the worst answer to "why can't I
8581
+ * save?". It cannot be clicked, so the form never gets to say which field is at fault or
8582
+ * which tab it is on — and on a tabbed form that field is usually not on screen.
7123
8583
  *
7124
- * Save now stays available while the form is merely invalid. Clicking it still saves
8584
+ * Save stays available while the form is merely invalid. Clicking it still saves
7125
8585
  * nothing — `submit()` checks `submitBlocked` and refuses — but the refusal is where the
7126
8586
  * error summary, the tab badges and the jump to the first bad field come from, so pressing
7127
8587
  * Save produces an explanation instead of silence.
@@ -7156,10 +8616,9 @@ class DynamicFormComponent {
7156
8616
  this.buildForm();
7157
8617
  // Clear *before* the guard below, not after it.
7158
8618
  //
7159
- // The reset used to sit behind `if (visibleTabs.length === 0) return`, so a swap that
7160
- // arrived while no tab was visible — a config still being applied, or every tab hidden
7161
- // by a rule at that instant — skipped it entirely and the previous record's tab
7162
- // survived into the next one. Clearing is unconditional and cheap, and safe on its own
8619
+ // A swap can arrive while no tab is visible — a config still being applied, or every
8620
+ // tab hidden by a rule at that instant — and the previous record's tab must not survive
8621
+ // into the next one. Clearing is unconditional and cheap, and safe on its own
7163
8622
  // because `activeTabConfig` already falls back to the first visible tab when nothing
7164
8623
  // is selected. Selecting the tab is what needs tabs to exist; forgetting the old one
7165
8624
  // does not.
@@ -7169,14 +8628,9 @@ class DynamicFormComponent {
7169
8628
  }
7170
8629
  if (this.visibleTabs.length === 0)
7171
8630
  return;
7172
- // The first tab used to be chosen only when no tab was active yet, and nothing ever
7173
- // cleared `activeTab` — so a form that stays mounted while `initialData` is swapped
7174
- // opened the next record on the tab the previous one was left on. The demo escaped it
7175
- // by destroying the form between records; a host that keeps it mounted did not.
7176
- //
7177
- // Resetting here is consistent with what already happens: `buildForm` assigns a brand
7178
- // new FormGroup, so a changed `initialData` has already discarded every control and
7179
- // all validation state. The tab was the one thing pretending nothing had changed.
8631
+ // A form that stays mounted while `initialData` is swapped must open the next record
8632
+ // on its first tab, not the tab the previous one was left on. `buildForm` has already
8633
+ // discarded every control and all validation state; the active tab resets with them.
7180
8634
  if (!this.activeTab()) {
7181
8635
  // No focus steal: the panel gains focus when a *user* picks a tab, not when a host
7182
8636
  // swaps the record underneath them.
@@ -7610,25 +9064,10 @@ class DynamicFormComponent {
7610
9064
  /**
7611
9065
  * Ids already warned about, per component instance.
7612
9066
  *
7613
- * This was `private static`, so it was module-global mutable state that nothing ever
7614
- * cleared: under SSR every render of every form in the process accumulated into one set
7615
- * that lived as long as the server did, and the first request to render a config
7616
- * suppressed the warning for every request after it. Per instance, the warning is once per
7617
- * form — which is what "warn once" was meant to mean.
9067
+ * Per instance, never `static`: under SSR a module-global set would outlive every request,
9068
+ * and the first render of a config would suppress the warning for every render after it.
7618
9069
  */
7619
9070
  warnedAmbiguousIds = new Set();
7620
- /**
7621
- * The value map rules and `showWhen` are evaluated against.
7622
- *
7623
- * Every field appears under two keys: its bare id, and its `refererField` path wrapped in brackets.
7624
- * The bare id is what every config written so far uses and is kept exactly as it was — but
7625
- * ids are unique per scope, so when two scopes define one the last field walked wins and
7626
- * the rule reads whichever that is. `[personal.address]` names one field and cannot be
7627
- * ambiguous, which is why a rule that has to distinguish them uses the ref.
7628
- *
7629
- * Both live in one flat map on purpose: `evaluateFormRules` takes a `Record<string,
7630
- * unknown>` and needs no knowledge of refs at all — the extra keys simply resolve.
7631
- */
7632
9071
  /**
7633
9072
  * Every name a rule may address this field by.
7634
9073
  *
@@ -7662,9 +9101,8 @@ class DynamicFormComponent {
7662
9101
  /**
7663
9102
  * Every field in the config with the scope its value is stored under, cached per config.
7664
9103
  *
7665
- * `collectFieldScopes` walks the whole tree. It was called once per field from `pathFor`,
7666
- * which `namesField` calls, which the render filter calls for every field on the active
7667
- * tab — a quadratic walk on every change-detection pass. Keyed by config identity, so a
9104
+ * `collectFieldScopes` walks the whole tree, and `pathFor` runs once per rendered field on
9105
+ * every change-detection pass — uncached, that is quadratic. Keyed by config identity, so a
7668
9106
  * new config invalidates it without an explicit lifecycle hook to forget.
7669
9107
  */
7670
9108
  fieldScopes() {
@@ -7683,35 +9121,13 @@ class DynamicFormComponent {
7683
9121
  return this.scopeCache?.byField.get(field);
7684
9122
  }
7685
9123
  scopeCache;
7686
- /**
7687
- * The control a scope entry addresses, named by path rather than by bare id.
7688
- *
7689
- * `getControl(field.id)` with no tab falls through to a first-match recursive search of the
7690
- * whole form, so with `address` on two tabs, hiding one disabled the other. The control
7691
- * tree mirrors the scope path exactly — `buildTabControls` nests by tab id and
7692
- * `buildFieldControl` nests a `group` under its own id — so a `[path]` ref resolves through
7693
- * `form.get()` on `getControl`'s first branch and names exactly one control.
7694
- *
7695
- * A field inside an `array` has no static path: its controls live in `FormArray` rows built
7696
- * per row. `form.get()` returns null for those and they are left alone, which is correct —
7697
- * a row's controls are created and destroyed with the row.
7698
- */
9124
+ /** The control a scope entry addresses — see `FormStructureService.controlAt`. */
7699
9125
  controlForEntry(entry) {
7700
- return this.getControl(toRefToken(fieldRefFor(entry.scope, entry.field.id)));
9126
+ return this.structure.controlAt(this.form, entry);
7701
9127
  }
9128
+ /** The value map rules and `showWhen` are evaluated against — see `FormStructureService.flattenValues`. */
7702
9129
  flattenFormValues() {
7703
- const out = {};
7704
- for (const entry of collectFieldScopes(this.config)) {
7705
- const field = entry.field;
7706
- if (!field?.id)
7707
- continue;
7708
- const ctrl = this.getControl(field.id, entry.scope.split('.').pop());
7709
- if (!ctrl)
7710
- continue;
7711
- out[field.id] = ctrl.value;
7712
- out[toRefToken(refOf(field, entry.scope))] = ctrl.value;
7713
- }
7714
- return out;
9130
+ return this.structure.flattenValues(this.form, this.fieldScopes());
7715
9131
  }
7716
9132
  // ─── autoPatch / patchOnTrue ──────────────────────────────────────────────
7717
9133
  /**
@@ -7738,22 +9154,46 @@ class DynamicFormComponent {
7738
9154
  /**
7739
9155
  * `patchOnTrue` — when a boolean/checkbox field transitions to `true`,
7740
9156
  * copy `from` → `to` within the current record.
9157
+ *
9158
+ * The trigger is read by its ref, and a bare `from`/`to` resolves to a sibling in the
9159
+ * trigger's own scope before falling back to a form-wide lookup — so two groups that each
9160
+ * carry `home`, `billing` and a "same as" flag patch within themselves, not across.
7741
9161
  */
7742
9162
  runPatchOnTrue(values) {
7743
- for (const field of this.allFields()) {
7744
- const mappings = field.patchOnTrue;
7745
- if (!mappings?.length)
9163
+ for (const entry of this.fieldScopes()) {
9164
+ const field = entry.field;
9165
+ const mappings = field?.patchOnTrue;
9166
+ if (!field?.id || !mappings?.length)
7746
9167
  continue;
7747
- const wasTrue = this.previousValues[field.id] === true;
7748
- const isTrue = values[field.id] === true;
7749
- if (!isTrue || wasTrue)
9168
+ const trigger = toRefToken(refOf(field, entry.scope));
9169
+ if (values[trigger] !== true || this.previousValues[trigger] === true)
7750
9170
  continue;
7751
- const patch = applyPatchOnTrue(mappings, values);
7752
- for (const [targetId, value] of Object.entries(patch)) {
7753
- this.getControl(targetId)?.patchValue(value, { emitEvent: false });
9171
+ for (const mapping of mappings) {
9172
+ if (!mapping || isUnsafePath(mapping.to))
9173
+ continue;
9174
+ const from = this.resolveInScope(mapping.from, entry.scope);
9175
+ if (!from || !(from.key in values))
9176
+ continue;
9177
+ this.resolveInScope(mapping.to, entry.scope)?.control?.patchValue(values[from.key], { emitEvent: false });
7754
9178
  }
7755
9179
  }
7756
9180
  }
9181
+ /**
9182
+ * A field named by a mapping, preferring a sibling in `scope` when the name is a bare id.
9183
+ *
9184
+ * `key` is the field's entry in the flattened value map; `control` is the control it owns.
9185
+ */
9186
+ resolveInScope(name, scope) {
9187
+ if (typeof name !== 'string' || !name)
9188
+ return null;
9189
+ const parsed = parseFieldRef(name);
9190
+ if (parsed.kind !== 'ref') {
9191
+ const sibling = this.fieldScopes().find(e => e.scope === scope && e.field?.id === parsed.value);
9192
+ if (sibling)
9193
+ return { key: toRefToken(refOf(sibling.field, sibling.scope)), control: this.controlForEntry(sibling) };
9194
+ }
9195
+ return { key: name, control: this.getControl(name) };
9196
+ }
7757
9197
  /**
7758
9198
  * Prefer a control declared on the configured target tab; fall back to a top-level control.
7759
9199
  *
@@ -7889,11 +9329,9 @@ class DynamicFormComponent {
7889
9329
  async submit() {
7890
9330
  if (!this.canSubmit || this.submitBlocked) {
7891
9331
  /*
7892
- * A refused save used to mark every control touched and stop there.
7893
- *
7894
- * On a tabbed form that is indistinguishable from a broken button: the errors appear on
7895
- * whichever tabs hold them, the user is looking at a different one, and nothing on
7896
- * screen changes. Three things fix it, and all three are needed — the summary says how
9332
+ * Marking every control touched is not enough on its own: on a tabbed form the errors
9333
+ * appear on whichever tabs hold them, the user is looking at a different one, and
9334
+ * nothing on screen changes. Three things are needed — the summary says how
7897
9335
  * many and which, the tab badges say where, and this jump takes the user to the first.
7898
9336
  */
7899
9337
  this.markAllTouched();
@@ -7946,10 +9384,10 @@ class DynamicFormComponent {
7946
9384
  this.submitAttempted.set(false);
7947
9385
  this.formReset.emit();
7948
9386
  }
7949
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: DynamicFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
7950
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", 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", layout: "layout" }, outputs: { formSubmit: "formSubmit", formChange: "formChange", formReset: "formReset", activeTabChange: "activeTabChange", saveRejected: "saveRejected" }, host: { listeners: { "window:keydown": "handleKeyboardEvent($event)" } }, providers: [EntityRefSelectionService, FormStructureService], 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\" [class.ngx-form--readonly]=\"readonly\" novalidate>\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 <!--\n Why the last Save did nothing.\n Only after a refused submit \u2014 see `submitAttempted`. Each entry jumps to its field,\n switching tabs on the way, because the field is usually not the one on screen.\n -->\n @if (submitAttempted() && invalidFields().length > 0) {\n <div class=\"ngx-form__error-summary\" role=\"alert\" data-testid=\"error-summary\" tabindex=\"-1\">\n <!--\n `error-summary-heading`, not `-title`: the links below are `error-summary-<fieldId>`,\n and a field whose id happens to be `title` would then share a test id with this line.\n The `extensions` entity has exactly such a field.\n -->\n <p class=\"ngx-form__error-summary-title\" data-testid=\"error-summary-heading\">\n {{ ui.text('errorSummaryTitle', language, { count: invalidFields().length }) }}\n </p>\n <ul class=\"ngx-form__error-summary-list\">\n @for (entry of invalidFields(); track entry.field.id) {\n <li>\n <!--\n The field, and what is wrong with it \u2014 the same sentence the field renders\n under itself, so the summary is not a second opinion. Naming only the field\n left the user to visit each one to find out what it wanted.\n -->\n <button\n type=\"button\"\n class=\"ngx-form__error-summary-link\"\n [attr.data-testid]=\"'error-summary-' + entry.field.id\"\n (click)=\"jumpToField(entry.field.id)\"\n >\n <span class=\"ngx-form__error-summary-field\">{{ resolveFieldLabel(entry.field) }}</span>\n @if (entry.message) {\n <span class=\"ngx-form__error-summary-reason\">{{ entry.message }}</span>\n }\n </button>\n </li>\n }\n </ul>\n </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 {{ ui.text('criticalFieldChanged', language, { fields: changedCriticalFieldLabels() }) }}\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 <!--\n How many fields on this tab are stopping the save.\n The count is in the accessible name rather than the badge, because \"3\" read out\n after a tab name says nothing about what it counts.\n -->\n @if (tabErrorCount(tab); as errors) {\n <span\n class=\"ngx-form__tab-badge\"\n [attr.data-testid]=\"'tab-errors-' + tab.id\"\n [attr.aria-label]=\"ui.text('tabErrorCount', language, { count: errors })\"\n >{{ errors }}</span\n >\n }\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\n class=\"ngx-form__tabs ngx-form__tabs--sub\"\n role=\"tablist\"\n data-testid=\"subtab-strip\"\n style=\"margin-top: -12px; margin-bottom: 12px; font-size: 13px\"\n >\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 @if (subTabErrorCount(subTab); as errors) {\n <span\n class=\"ngx-form__tab-badge\"\n [attr.data-testid]=\"'subtab-errors-' + subTab.id\"\n [attr.aria-label]=\"ui.text('tabErrorCount', language, { count: errors })\"\n >{{ errors }}</span\n >\n }\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 role=\"tabpanel\" tabindex=\"-1\" #formPanel class=\"ngx-form__tabpanel\" [attr.aria-label]=\"activeTabLabel\">\n @if (activeTabModuleComponent) {\n <div class=\"ngx-form__module-panel\" data-testid=\"module-panel\">\n <ng-container\n *ngComponentOutlet=\"activeTabModuleComponent; inputs: (activeSubTabConfig || activeTabConfig)?.moduleInputs\"\n ></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 tabindex=\"-1\"\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 }\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 ui.text(isFieldLocked(field) ? 'unlockField' : 'lockField', language, {\n field: resolveFieldLabel(field),\n })\n \"\n [title]=\"ui.text(isFieldLocked(field) ? 'criticalFieldUnlockHint' : 'criticalFieldLockHint', language)\"\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 type=\"button\" class=\"ngx-form__reset\" data-testid=\"form-reset\" (click)=\"reset()\" [disabled]=\"isSaving()\">\n {{ ui.text('reset', language) }}\n </button>\n <!--\n Two different \"unavailable\" states, and they are not the same thing.\n\n `disabled` is for what a retry cannot fix \u2014 a save in flight, an async check still\n outstanding. `data-blocked` is for validation: the button stays fully operable,\n because pressing it is how the user gets the error summary, but it should not look\n ready either. The stylesheet mutes it without taking the click away.\n\n Deliberately not `aria-disabled`. That announces a control as present-but-inoperable,\n and this one is operable and useful \u2014 pressing it raises a `role=\"alert\"` naming\n every field at fault, which is more than a disabled button could ever say. Tooling\n agrees: Playwright refuses to click through `aria-disabled`, and it would be right\n to, because the attribute would be claiming something untrue.\n -->\n <button\n type=\"submit\"\n class=\"ngx-form__submit\"\n data-testid=\"form-submit\"\n [disabled]=\"submitDisabled\"\n [attr.data-blocked]=\"submitBlocked ? '' : null\"\n [title]=\"ui.text('saveShortcutHint', language)\"\n >\n @if (isSaving() || loading) {\n {{ ui.text('saving', language) }}\n } @else {\n {{ ui.text('save', language) }}\n }\n </button>\n </div>\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 {{ ui.text('accessDenied', language) }}\n </div>\n}\n", styles: [".ngx-form__panel{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:var(--ngx-gap, 18px);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:var(--ngx-radius-sm, 6px);cursor:pointer;font-size:14px;line-height:1;padding:4px 6px}.ngx-form__lock:hover{border-color:var(--ngx-color-border, #d1d5db);background:var(--ngx-color-surface-alt, #f9fafb)}@media(max-width:640px){.ngx-form__panel{grid-template-columns:minmax(0,1fr)}.ngx-form__field-slot{grid-column:1 / -1!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],[formArray],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", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
9387
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: DynamicFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
9388
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", 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", layout: "layout" }, outputs: { formSubmit: "formSubmit", formChange: "formChange", formReset: "formReset", activeTabChange: "activeTabChange", saveRejected: "saveRejected" }, host: { listeners: { "window:keydown": "handleKeyboardEvent($event)" } }, providers: [EntityRefSelectionService, FormStructureService], 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\" [class.ngx-form--readonly]=\"readonly\" novalidate>\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 <!--\n Why the last Save did nothing.\n Only after a refused submit \u2014 see `submitAttempted`. Each entry jumps to its field,\n switching tabs on the way, because the field is usually not the one on screen.\n -->\n @if (submitAttempted() && invalidFields().length > 0) {\n <div class=\"ngx-form__error-summary\" role=\"alert\" data-testid=\"error-summary\" tabindex=\"-1\">\n <!--\n `error-summary-heading`, not `-title`: the links below are `error-summary-<fieldId>`,\n and a field whose id happens to be `title` would then share a test id with this line.\n The `extensions` entity has exactly such a field.\n -->\n <p class=\"ngx-form__error-summary-title\" data-testid=\"error-summary-heading\">\n {{ ui.text('errorSummaryTitle', language, { count: invalidFields().length }) }}\n </p>\n <ul class=\"ngx-form__error-summary-list\">\n @for (entry of invalidFields(); track entry.field.id) {\n <li>\n <!--\n The field, and what is wrong with it \u2014 the same sentence the field renders\n under itself, so the summary is not a second opinion. Naming only the field\n left the user to visit each one to find out what it wanted.\n -->\n <button\n type=\"button\"\n class=\"ngx-form__error-summary-link\"\n [attr.data-testid]=\"'error-summary-' + entry.field.id\"\n (click)=\"jumpToField(entry.field.id)\"\n >\n <span class=\"ngx-form__error-summary-field\">{{ resolveFieldLabel(entry.field) }}</span>\n @if (entry.message) {\n <span class=\"ngx-form__error-summary-reason\">{{ entry.message }}</span>\n }\n </button>\n </li>\n }\n </ul>\n </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 {{ ui.text('criticalFieldChanged', language, { fields: changedCriticalFieldLabels() }) }}\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 <!--\n How many fields on this tab are stopping the save.\n The count is in the accessible name rather than the badge, because \"3\" read out\n after a tab name says nothing about what it counts.\n -->\n @if (tabErrorCount(tab); as errors) {\n <span\n class=\"ngx-form__tab-badge\"\n [attr.data-testid]=\"'tab-errors-' + tab.id\"\n [attr.aria-label]=\"ui.text('tabErrorCount', language, { count: errors })\"\n >{{ errors }}</span\n >\n }\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\n class=\"ngx-form__tabs ngx-form__tabs--sub\"\n role=\"tablist\"\n data-testid=\"subtab-strip\"\n style=\"margin-top: -12px; margin-bottom: 12px; font-size: 13px\"\n >\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 @if (subTabErrorCount(subTab); as errors) {\n <span\n class=\"ngx-form__tab-badge\"\n [attr.data-testid]=\"'subtab-errors-' + subTab.id\"\n [attr.aria-label]=\"ui.text('tabErrorCount', language, { count: errors })\"\n >{{ errors }}</span\n >\n }\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 role=\"tabpanel\" tabindex=\"-1\" #formPanel class=\"ngx-form__tabpanel\" [attr.aria-label]=\"activeTabLabel\">\n @if (activeTabModuleComponent) {\n <div class=\"ngx-form__module-panel\" data-testid=\"module-panel\">\n <ng-container\n *ngComponentOutlet=\"activeTabModuleComponent; inputs: (activeSubTabConfig || activeTabConfig)?.moduleInputs\"\n ></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 tabindex=\"-1\"\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 }\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 ui.text(isFieldLocked(field) ? 'unlockField' : 'lockField', language, {\n field: resolveFieldLabel(field),\n })\n \"\n [title]=\"ui.text(isFieldLocked(field) ? 'criticalFieldUnlockHint' : 'criticalFieldLockHint', language)\"\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 type=\"button\" class=\"ngx-form__reset\" data-testid=\"form-reset\" (click)=\"reset()\" [disabled]=\"isSaving()\">\n {{ ui.text('reset', language) }}\n </button>\n <!--\n Two different \"unavailable\" states, and they are not the same thing.\n\n `disabled` is for what a retry cannot fix \u2014 a save in flight, an async check still\n outstanding. `data-blocked` is for validation: the button stays fully operable,\n because pressing it is how the user gets the error summary, but it should not look\n ready either. The stylesheet mutes it without taking the click away.\n\n Deliberately not `aria-disabled`. That announces a control as present-but-inoperable,\n and this one is operable and useful \u2014 pressing it raises a `role=\"alert\"` naming\n every field at fault, which is more than a disabled button could ever say. Tooling\n agrees: Playwright refuses to click through `aria-disabled`, and it would be right\n to, because the attribute would be claiming something untrue.\n -->\n <button\n type=\"submit\"\n class=\"ngx-form__submit\"\n data-testid=\"form-submit\"\n [disabled]=\"submitDisabled\"\n [attr.data-blocked]=\"submitBlocked ? '' : null\"\n [title]=\"ui.text('saveShortcutHint', language)\"\n >\n @if (isSaving() || loading) {\n {{ ui.text('saving', language) }}\n } @else {\n {{ ui.text('save', language) }}\n }\n </button>\n </div>\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 {{ ui.text('accessDenied', language) }}\n </div>\n}\n", styles: [".ngx-form__panel{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:var(--ngx-gap, 18px);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:var(--ngx-radius-sm, 6px);cursor:pointer;font-size:14px;line-height:1;padding:4px 6px}.ngx-form__lock:hover{border-color:var(--ngx-color-border, #d1d5db);background:var(--ngx-color-surface-alt, #f9fafb)}@media(max-width:640px){.ngx-form__panel{grid-template-columns:minmax(0,1fr)}.ngx-form__field-slot{grid-column:1 / -1!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],[formArray],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", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
7951
9389
  }
7952
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: DynamicFormComponent, decorators: [{
9390
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: DynamicFormComponent, decorators: [{
7953
9391
  type: Component,
7954
9392
  args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'ngx-dynamic-form', standalone: true, imports: [ReactiveFormsModule, DynamicFieldComponent, NgComponentOutlet], providers: [EntityRefSelectionService, FormStructureService], template: "@if (canView) {\n <form [formGroup]=\"form\" (ngSubmit)=\"submit()\" class=\"ngx-form\" [class.ngx-form--readonly]=\"readonly\" novalidate>\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 <!--\n Why the last Save did nothing.\n Only after a refused submit \u2014 see `submitAttempted`. Each entry jumps to its field,\n switching tabs on the way, because the field is usually not the one on screen.\n -->\n @if (submitAttempted() && invalidFields().length > 0) {\n <div class=\"ngx-form__error-summary\" role=\"alert\" data-testid=\"error-summary\" tabindex=\"-1\">\n <!--\n `error-summary-heading`, not `-title`: the links below are `error-summary-<fieldId>`,\n and a field whose id happens to be `title` would then share a test id with this line.\n The `extensions` entity has exactly such a field.\n -->\n <p class=\"ngx-form__error-summary-title\" data-testid=\"error-summary-heading\">\n {{ ui.text('errorSummaryTitle', language, { count: invalidFields().length }) }}\n </p>\n <ul class=\"ngx-form__error-summary-list\">\n @for (entry of invalidFields(); track entry.field.id) {\n <li>\n <!--\n The field, and what is wrong with it \u2014 the same sentence the field renders\n under itself, so the summary is not a second opinion. Naming only the field\n left the user to visit each one to find out what it wanted.\n -->\n <button\n type=\"button\"\n class=\"ngx-form__error-summary-link\"\n [attr.data-testid]=\"'error-summary-' + entry.field.id\"\n (click)=\"jumpToField(entry.field.id)\"\n >\n <span class=\"ngx-form__error-summary-field\">{{ resolveFieldLabel(entry.field) }}</span>\n @if (entry.message) {\n <span class=\"ngx-form__error-summary-reason\">{{ entry.message }}</span>\n }\n </button>\n </li>\n }\n </ul>\n </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 {{ ui.text('criticalFieldChanged', language, { fields: changedCriticalFieldLabels() }) }}\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 <!--\n How many fields on this tab are stopping the save.\n The count is in the accessible name rather than the badge, because \"3\" read out\n after a tab name says nothing about what it counts.\n -->\n @if (tabErrorCount(tab); as errors) {\n <span\n class=\"ngx-form__tab-badge\"\n [attr.data-testid]=\"'tab-errors-' + tab.id\"\n [attr.aria-label]=\"ui.text('tabErrorCount', language, { count: errors })\"\n >{{ errors }}</span\n >\n }\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\n class=\"ngx-form__tabs ngx-form__tabs--sub\"\n role=\"tablist\"\n data-testid=\"subtab-strip\"\n style=\"margin-top: -12px; margin-bottom: 12px; font-size: 13px\"\n >\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 @if (subTabErrorCount(subTab); as errors) {\n <span\n class=\"ngx-form__tab-badge\"\n [attr.data-testid]=\"'subtab-errors-' + subTab.id\"\n [attr.aria-label]=\"ui.text('tabErrorCount', language, { count: errors })\"\n >{{ errors }}</span\n >\n }\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 role=\"tabpanel\" tabindex=\"-1\" #formPanel class=\"ngx-form__tabpanel\" [attr.aria-label]=\"activeTabLabel\">\n @if (activeTabModuleComponent) {\n <div class=\"ngx-form__module-panel\" data-testid=\"module-panel\">\n <ng-container\n *ngComponentOutlet=\"activeTabModuleComponent; inputs: (activeSubTabConfig || activeTabConfig)?.moduleInputs\"\n ></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 tabindex=\"-1\"\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 }\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 ui.text(isFieldLocked(field) ? 'unlockField' : 'lockField', language, {\n field: resolveFieldLabel(field),\n })\n \"\n [title]=\"ui.text(isFieldLocked(field) ? 'criticalFieldUnlockHint' : 'criticalFieldLockHint', language)\"\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 type=\"button\" class=\"ngx-form__reset\" data-testid=\"form-reset\" (click)=\"reset()\" [disabled]=\"isSaving()\">\n {{ ui.text('reset', language) }}\n </button>\n <!--\n Two different \"unavailable\" states, and they are not the same thing.\n\n `disabled` is for what a retry cannot fix \u2014 a save in flight, an async check still\n outstanding. `data-blocked` is for validation: the button stays fully operable,\n because pressing it is how the user gets the error summary, but it should not look\n ready either. The stylesheet mutes it without taking the click away.\n\n Deliberately not `aria-disabled`. That announces a control as present-but-inoperable,\n and this one is operable and useful \u2014 pressing it raises a `role=\"alert\"` naming\n every field at fault, which is more than a disabled button could ever say. Tooling\n agrees: Playwright refuses to click through `aria-disabled`, and it would be right\n to, because the attribute would be claiming something untrue.\n -->\n <button\n type=\"submit\"\n class=\"ngx-form__submit\"\n data-testid=\"form-submit\"\n [disabled]=\"submitDisabled\"\n [attr.data-blocked]=\"submitBlocked ? '' : null\"\n [title]=\"ui.text('saveShortcutHint', language)\"\n >\n @if (isSaving() || loading) {\n {{ ui.text('saving', language) }}\n } @else {\n {{ ui.text('save', language) }}\n }\n </button>\n </div>\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 {{ ui.text('accessDenied', language) }}\n </div>\n}\n", styles: [".ngx-form__panel{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:var(--ngx-gap, 18px);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:var(--ngx-radius-sm, 6px);cursor:pointer;font-size:14px;line-height:1;padding:4px 6px}.ngx-form__lock:hover{border-color:var(--ngx-color-border, #d1d5db);background:var(--ngx-color-surface-alt, #f9fafb)}@media(max-width:640px){.ngx-form__panel{grid-template-columns:minmax(0,1fr)}.ngx-form__field-slot{grid-column:1 / -1!important}}\n"] }]
7955
9393
  }], propDecorators: { config: [{
@@ -8367,10 +9805,8 @@ class DynamicRecordFormComponent {
8367
9805
  /**
8368
9806
  * What this record is called, as opposed to what kind of thing it is.
8369
9807
  *
8370
- * The header used to show the entity's name twice — "clients", with "Entity: clients"
8371
- * underneath — so the one question a record header exists to answer, *which* record am I
8372
- * looking at, was the one thing it did not say. The entity keeps the subtitle; this takes
8373
- * the heading.
9808
+ * A record header exists to answer *which* record am I looking at, so this takes the
9809
+ * heading and the entity name keeps the subtitle.
8374
9810
  *
8375
9811
  * The order is deliberate. `showOnMinimize` is the author saying "this is what identifies a
8376
9812
  * record" — it is what the summary panel and the collapsed view already show — so it is
@@ -8552,11 +9988,6 @@ class DynamicRecordFormComponent {
8552
9988
  formatFieldValue(field) {
8553
9989
  return formatDisplayValue(field.type, field.options, this.fieldValue(field.id), this.language);
8554
9990
  }
8555
- /**
8556
- * Finds which tab — and, where the field lives one level down, which sub-tab — owns a
8557
- * field. The walk used to check top-level `fields` only, so every field in a sub-tab was
8558
- * simply not found and the jump did nothing at all.
8559
- */
8560
9991
  /**
8561
9992
  * Switches to the tab holding `fieldId`, then scrolls it into view and moves focus to it.
8562
9993
  *
@@ -8569,10 +10000,10 @@ class DynamicRecordFormComponent {
8569
10000
  jumpToField(fieldId) {
8570
10001
  this.dynamicFormComp?.jumpToField(fieldId);
8571
10002
  }
8572
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: DynamicRecordFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
8573
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", 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", layout: "layout", viewMode: "viewMode" }, outputs: { formSubmit: "formSubmit", formChange: "formChange", formReset: "formReset", sectionSave: "sectionSave", saveRejected: "saveRejected" }, 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]=\"recordHeading\"\n />\n } @else {\n <div class=\"ngx-record-editor__avatar\" data-testid=\"header-avatar-initial\">\n {{ avatarLetter }}\n </div>\n }\n <div>\n <!--\n The record's own name, with the entity underneath. These were the other way round \u2014\n both showing the entity \u2014 so the header never said which record was open.\n -->\n <h2 class=\"ngx-record-editor__title\" data-testid=\"record-title\">\n {{ recordHeading }}\n </h2>\n <span class=\"ngx-record-editor__subtitle\">{{ ui.text('entityLabel', language, { entity: recordTitle }) }}</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\">{{ ui.text(headerToggleValue ? 'yes' : 'no', language) }}</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]=\"ui.text('dismissBanner', language, { message: banner.message })\"\n (click)=\"dismissInfoBanner(banner.fieldId)\"\n >\n \u2715\n </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 {{ ui.text('recordModified', language) }}\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]=\"ui.text('jumpToField', language, { field: 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\">{{ ui.text('editingSection', language) }}</span>\n <button type=\"button\" class=\"ngx-form__reset\" data-testid=\"cancel-section\" (click)=\"cancelSection()\">\n {{ ui.text('cancel', language) }}\n </button>\n <button type=\"button\" class=\"ngx-form__submit\" data-testid=\"save-section\" (click)=\"saveSection()\">\n {{ ui.text('saveSection', language) }}\n </button>\n } @else {\n <button type=\"button\" class=\"ngx-form__reset\" data-testid=\"edit-section\" (click)=\"editSection()\">\n {{ ui.text('editSection', language) }}\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 >\n {{ ui.text('addRow', language) }}\n </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\n type=\"button\"\n class=\"ngx-record-editor__row-btn\"\n [attr.data-testid]=\"'edit-row-' + arrayField.id + '-' + $index\"\n (click)=\"openEditRow(arrayField, $index)\"\n >\n {{ ui.text('edit', language) }}\n </button>\n <button\n type=\"button\"\n class=\"ngx-record-editor__row-btn\"\n [attr.data-testid]=\"'delete-row-' + arrayField.id + '-' + $index\"\n (click)=\"deleteRow(arrayField, $index)\"\n >\n {{ ui.text('delete', language) }}\n </button>\n }\n </div>\n } @empty {\n <p class=\"ngx-record-editor__row-empty\">{{ ui.text('noRows', language) }}</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>{{ rowDrawerTitle(rowField) }}</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()\">\n {{ ui.text('cancel', language) }}\n </button>\n <button type=\"button\" class=\"ngx-form__submit\" data-testid=\"save-row\" (click)=\"saveRow()\">\n {{ ui.text('saveRow', language) }}\n </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 [layout]=\"layout\"\n [showInfoBanners]=\"false\"\n [loading]=\"loading\"\n [error]=\"error\"\n (formChange)=\"onFormChange($event)\"\n (formSubmit)=\"onFormSubmit($event)\"\n (saveRejected)=\"saveRejected.emit($event)\"\n (formReset)=\"onFormReset()\"\n (activeTabChange)=\"onActiveTabChange($event)\"\n />\n</div>\n", styles: [".ngx-record-editor{display:flex;flex-direction:column;gap:var(--ngx-gap, 16px);background:var(--ngx-color-surface, #ffffff);border:1px solid var(--ngx-color-border, #e5e7eb);border-radius:var(--ngx-radius, 12px);padding:20px}.ngx-record-editor__header{display:flex;align-items:center;justify-content:space-between;gap:14px;padding-bottom:var(--ngx-gap-sm, 16px);border-bottom:1px solid var(--ngx-color-border, #f3f4f6)}.ngx-record-editor__title-group{display:flex;align-items:center;gap:12px;min-width:0}.ngx-record-editor__avatar{width:44px;height:44px;border-radius:50%;object-fit:cover;background:var(--ngx-color-accent-soft, #e0e7ff);color:var(--ngx-color-accent, #4f46e5);border:1px solid var(--ngx-color-border, transparent);display:flex;align-items:center;justify-content:center;font-weight:700;font-size:17px;flex:none}.ngx-record-editor__title{font-size:19px;font-weight:700;color:var(--ngx-color-text, #111827);margin:0;letter-spacing:-.01em}.ngx-record-editor__subtitle{font-size:var(--ngx-hint-size, 13px);color:var(--ngx-color-muted, #6b7280)}.ngx-record-editor__header-toggle{display:flex;align-items:center;gap:8px;font-size:var(--ngx-label-size, 13px);font-weight:var(--ngx-label-weight, 600);color:var(--ngx-color-text, #374151)}.ngx-record-editor__header-toggle-text{font-weight:600}.ngx-record-editor__banner{padding:11px 14px;border-radius:var(--ngx-radius-sm, 8px);font-size:var(--ngx-font-size, 13px);border:1px solid transparent;border-left-width:3px}.ngx-record-editor__banner--info{background:var(--ngx-color-info-soft, #eff6ff);border-color:var(--ngx-color-border, #dbeafe);border-left-color:var(--ngx-color-info, #1d4ed8);color:var(--ngx-color-info, #1d4ed8);display:flex;align-items:center;justify-content:space-between;gap:12px}.ngx-record-editor__banner--error{background:var(--ngx-color-error-soft, #fef2f2);border-color:var(--ngx-color-border, #fecaca);border-left-color:var(--ngx-color-error, #b91c1c);color:var(--ngx-color-error, #b91c1c)}.ngx-record-editor__banner--warning{background:var(--ngx-color-warning-soft, #fffbeb);border-color:var(--ngx-color-border, #fef3c7);border-left-color:var(--ngx-color-warning, #b45309);color:var(--ngx-color-warning, #b45309)}.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__section-bar{display:flex;align-items:center;justify-content:flex-end;flex-wrap:wrap;gap:8px}.ngx-record-editor__section-hint{margin-right:auto;font-size:var(--ngx-hint-size, 12px);color:var(--ngx-color-muted, #6b7280)}.ngx-record-editor__rows{border:1px solid var(--ngx-color-border, #f3f4f6);border-radius:var(--ngx-radius-sm, 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:8px 10px;border-radius:var(--ngx-radius-sm, 6px);background:var(--ngx-color-surface-alt, #f9fafb)}.ngx-record-editor__row-text{flex:1;font-size:var(--ngx-font-size, 13px);overflow-wrap:anywhere}.ngx-record-editor__row-btn{background:var(--ngx-color-surface, none);border:1px solid var(--ngx-color-border, #e5e7eb);border-radius:var(--ngx-radius-sm, 6px);cursor:pointer;font:inherit;font-size:var(--ngx-hint-size, 12px);font-weight:var(--ngx-label-weight, 600);color:inherit;padding:4px 10px}.ngx-record-editor__row-btn:hover{border-color:var(--ngx-color-accent, #e5e7eb);color:var(--ngx-color-accent, inherit)}.ngx-record-editor__row-empty{font-size:var(--ngx-hint-size, 12px);color:var(--ngx-color-muted, #6b7280);margin:0}.ngx-record-editor__drawer{border:1px solid var(--ngx-color-border, #dbeafe);background:var(--ngx-color-surface-alt, #f8fafc);border-radius:var(--ngx-radius, 8px);padding:16px;display:flex;flex-direction:column;gap:12px}.ngx-record-editor__summary-panel{background:var(--ngx-color-surface-alt, #f9fafb);border:1px solid var(--ngx-color-border, #f3f4f6);border-radius:var(--ngx-radius, 8px);padding:12px 16px;display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:4px var(--ngx-gap, 12px)}.ngx-record-editor__summary-item{display:flex;flex-direction:column;cursor:pointer;padding:6px 10px;border-radius:var(--ngx-radius-sm, 6px);min-width:0;transition:background .15s}.ngx-record-editor__summary-item:hover{background:var(--ngx-color-accent-soft, #eff6ff)}.ngx-record-editor__summary-label{font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:var(--ngx-color-muted, #6b7280);font-weight:600}.ngx-record-editor__summary-val{font-size:var(--ngx-font-size, 14px);font-weight:600;color:var(--ngx-color-text, #1f2937);overflow-wrap:anywhere}@media(prefers-reduced-motion:reduce){.ngx-record-editor__summary-item{transition:none}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],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", "layout"], outputs: ["formSubmit", "formChange", "formReset", "activeTabChange", "saveRejected"] }, { kind: "component", type: DynamicFieldComponent, selector: "ngx-dynamic-field", inputs: ["field", "control", "config", "currentTabId", "language", "readonly", "userRoles"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
10003
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: DynamicRecordFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
10004
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", 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", layout: "layout", viewMode: "viewMode" }, outputs: { formSubmit: "formSubmit", formChange: "formChange", formReset: "formReset", sectionSave: "sectionSave", saveRejected: "saveRejected" }, 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]=\"recordHeading\"\n />\n } @else {\n <div class=\"ngx-record-editor__avatar\" data-testid=\"header-avatar-initial\">\n {{ avatarLetter }}\n </div>\n }\n <div>\n <!--\n The record's own name, with the entity underneath. These were the other way round \u2014\n both showing the entity \u2014 so the header never said which record was open.\n -->\n <h2 class=\"ngx-record-editor__title\" data-testid=\"record-title\">\n {{ recordHeading }}\n </h2>\n <span class=\"ngx-record-editor__subtitle\">{{ ui.text('entityLabel', language, { entity: recordTitle }) }}</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\">{{ ui.text(headerToggleValue ? 'yes' : 'no', language) }}</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]=\"ui.text('dismissBanner', language, { message: banner.message })\"\n (click)=\"dismissInfoBanner(banner.fieldId)\"\n >\n \u2715\n </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 {{ ui.text('recordModified', language) }}\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]=\"ui.text('jumpToField', language, { field: 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\">{{ ui.text('editingSection', language) }}</span>\n <button type=\"button\" class=\"ngx-form__reset\" data-testid=\"cancel-section\" (click)=\"cancelSection()\">\n {{ ui.text('cancel', language) }}\n </button>\n <button type=\"button\" class=\"ngx-form__submit\" data-testid=\"save-section\" (click)=\"saveSection()\">\n {{ ui.text('saveSection', language) }}\n </button>\n } @else {\n <button type=\"button\" class=\"ngx-form__reset\" data-testid=\"edit-section\" (click)=\"editSection()\">\n {{ ui.text('editSection', language) }}\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 >\n {{ ui.text('addRow', language) }}\n </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\n type=\"button\"\n class=\"ngx-record-editor__row-btn\"\n [attr.data-testid]=\"'edit-row-' + arrayField.id + '-' + $index\"\n (click)=\"openEditRow(arrayField, $index)\"\n >\n {{ ui.text('edit', language) }}\n </button>\n <button\n type=\"button\"\n class=\"ngx-record-editor__row-btn\"\n [attr.data-testid]=\"'delete-row-' + arrayField.id + '-' + $index\"\n (click)=\"deleteRow(arrayField, $index)\"\n >\n {{ ui.text('delete', language) }}\n </button>\n }\n </div>\n } @empty {\n <p class=\"ngx-record-editor__row-empty\">{{ ui.text('noRows', language) }}</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>{{ rowDrawerTitle(rowField) }}</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()\">\n {{ ui.text('cancel', language) }}\n </button>\n <button type=\"button\" class=\"ngx-form__submit\" data-testid=\"save-row\" (click)=\"saveRow()\">\n {{ ui.text('saveRow', language) }}\n </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 [layout]=\"layout\"\n [showInfoBanners]=\"false\"\n [loading]=\"loading\"\n [error]=\"error\"\n (formChange)=\"onFormChange($event)\"\n (formSubmit)=\"onFormSubmit($event)\"\n (saveRejected)=\"saveRejected.emit($event)\"\n (formReset)=\"onFormReset()\"\n (activeTabChange)=\"onActiveTabChange($event)\"\n />\n</div>\n", styles: [".ngx-record-editor{display:flex;flex-direction:column;gap:var(--ngx-gap, 16px);background:var(--ngx-color-surface, #ffffff);border:1px solid var(--ngx-color-border, #e5e7eb);border-radius:var(--ngx-radius, 12px);padding:20px}.ngx-record-editor__header{display:flex;align-items:center;justify-content:space-between;gap:14px;padding-bottom:var(--ngx-gap-sm, 16px);border-bottom:1px solid var(--ngx-color-border, #f3f4f6)}.ngx-record-editor__title-group{display:flex;align-items:center;gap:12px;min-width:0}.ngx-record-editor__avatar{width:44px;height:44px;border-radius:50%;object-fit:cover;background:var(--ngx-color-accent-soft, #e0e7ff);color:var(--ngx-color-accent, #4f46e5);border:1px solid var(--ngx-color-border, transparent);display:flex;align-items:center;justify-content:center;font-weight:700;font-size:17px;flex:none}.ngx-record-editor__title{font-size:19px;font-weight:700;color:var(--ngx-color-text, #111827);margin:0;letter-spacing:-.01em}.ngx-record-editor__subtitle{font-size:var(--ngx-hint-size, 13px);color:var(--ngx-color-muted, #6b7280)}.ngx-record-editor__header-toggle{display:flex;align-items:center;gap:8px;font-size:var(--ngx-label-size, 13px);font-weight:var(--ngx-label-weight, 600);color:var(--ngx-color-text, #374151)}.ngx-record-editor__header-toggle-text{font-weight:600}.ngx-record-editor__banner{padding:11px 14px;border-radius:var(--ngx-radius-sm, 8px);font-size:var(--ngx-font-size, 13px);border:1px solid transparent;border-left-width:3px}.ngx-record-editor__banner--info{background:var(--ngx-color-info-soft, #eff6ff);border-color:var(--ngx-color-border, #dbeafe);border-left-color:var(--ngx-color-info, #1d4ed8);color:var(--ngx-color-info, #1d4ed8);display:flex;align-items:center;justify-content:space-between;gap:12px}.ngx-record-editor__banner--error{background:var(--ngx-color-error-soft, #fef2f2);border-color:var(--ngx-color-border, #fecaca);border-left-color:var(--ngx-color-error, #b91c1c);color:var(--ngx-color-error, #b91c1c)}.ngx-record-editor__banner--warning{background:var(--ngx-color-warning-soft, #fffbeb);border-color:var(--ngx-color-border, #fef3c7);border-left-color:var(--ngx-color-warning, #b45309);color:var(--ngx-color-warning, #b45309)}.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__section-bar{display:flex;align-items:center;justify-content:flex-end;flex-wrap:wrap;gap:8px}.ngx-record-editor__section-hint{margin-right:auto;font-size:var(--ngx-hint-size, 12px);color:var(--ngx-color-muted, #6b7280)}.ngx-record-editor__rows{border:1px solid var(--ngx-color-border, #f3f4f6);border-radius:var(--ngx-radius-sm, 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:8px 10px;border-radius:var(--ngx-radius-sm, 6px);background:var(--ngx-color-surface-alt, #f9fafb)}.ngx-record-editor__row-text{flex:1;font-size:var(--ngx-font-size, 13px);overflow-wrap:anywhere}.ngx-record-editor__row-btn{background:var(--ngx-color-surface, none);border:1px solid var(--ngx-color-border, #e5e7eb);border-radius:var(--ngx-radius-sm, 6px);cursor:pointer;font:inherit;font-size:var(--ngx-hint-size, 12px);font-weight:var(--ngx-label-weight, 600);color:inherit;padding:4px 10px}.ngx-record-editor__row-btn:hover{border-color:var(--ngx-color-accent, #e5e7eb);color:var(--ngx-color-accent, inherit)}.ngx-record-editor__row-empty{font-size:var(--ngx-hint-size, 12px);color:var(--ngx-color-muted, #6b7280);margin:0}.ngx-record-editor__drawer{border:1px solid var(--ngx-color-border, #dbeafe);background:var(--ngx-color-surface-alt, #f8fafc);border-radius:var(--ngx-radius, 8px);padding:16px;display:flex;flex-direction:column;gap:12px}.ngx-record-editor__summary-panel{background:var(--ngx-color-surface-alt, #f9fafb);border:1px solid var(--ngx-color-border, #f3f4f6);border-radius:var(--ngx-radius, 8px);padding:12px 16px;display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:4px var(--ngx-gap, 12px)}.ngx-record-editor__summary-item{display:flex;flex-direction:column;cursor:pointer;padding:6px 10px;border-radius:var(--ngx-radius-sm, 6px);min-width:0;transition:background .15s}.ngx-record-editor__summary-item:hover{background:var(--ngx-color-accent-soft, #eff6ff)}.ngx-record-editor__summary-label{font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:var(--ngx-color-muted, #6b7280);font-weight:600}.ngx-record-editor__summary-val{font-size:var(--ngx-font-size, 14px);font-weight:600;color:var(--ngx-color-text, #1f2937);overflow-wrap:anywhere}@media(prefers-reduced-motion:reduce){.ngx-record-editor__summary-item{transition:none}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],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", "layout"], outputs: ["formSubmit", "formChange", "formReset", "activeTabChange", "saveRejected"] }, { kind: "component", type: DynamicFieldComponent, selector: "ngx-dynamic-field", inputs: ["field", "control", "config", "currentTabId", "language", "readonly", "userRoles"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
8574
10005
  }
8575
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: DynamicRecordFormComponent, decorators: [{
10006
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: DynamicRecordFormComponent, decorators: [{
8576
10007
  type: Component,
8577
10008
  args: [{ changeDetection: ChangeDetectionStrategy.OnPush, 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]=\"recordHeading\"\n />\n } @else {\n <div class=\"ngx-record-editor__avatar\" data-testid=\"header-avatar-initial\">\n {{ avatarLetter }}\n </div>\n }\n <div>\n <!--\n The record's own name, with the entity underneath. These were the other way round \u2014\n both showing the entity \u2014 so the header never said which record was open.\n -->\n <h2 class=\"ngx-record-editor__title\" data-testid=\"record-title\">\n {{ recordHeading }}\n </h2>\n <span class=\"ngx-record-editor__subtitle\">{{ ui.text('entityLabel', language, { entity: recordTitle }) }}</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\">{{ ui.text(headerToggleValue ? 'yes' : 'no', language) }}</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]=\"ui.text('dismissBanner', language, { message: banner.message })\"\n (click)=\"dismissInfoBanner(banner.fieldId)\"\n >\n \u2715\n </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 {{ ui.text('recordModified', language) }}\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]=\"ui.text('jumpToField', language, { field: 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\">{{ ui.text('editingSection', language) }}</span>\n <button type=\"button\" class=\"ngx-form__reset\" data-testid=\"cancel-section\" (click)=\"cancelSection()\">\n {{ ui.text('cancel', language) }}\n </button>\n <button type=\"button\" class=\"ngx-form__submit\" data-testid=\"save-section\" (click)=\"saveSection()\">\n {{ ui.text('saveSection', language) }}\n </button>\n } @else {\n <button type=\"button\" class=\"ngx-form__reset\" data-testid=\"edit-section\" (click)=\"editSection()\">\n {{ ui.text('editSection', language) }}\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 >\n {{ ui.text('addRow', language) }}\n </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\n type=\"button\"\n class=\"ngx-record-editor__row-btn\"\n [attr.data-testid]=\"'edit-row-' + arrayField.id + '-' + $index\"\n (click)=\"openEditRow(arrayField, $index)\"\n >\n {{ ui.text('edit', language) }}\n </button>\n <button\n type=\"button\"\n class=\"ngx-record-editor__row-btn\"\n [attr.data-testid]=\"'delete-row-' + arrayField.id + '-' + $index\"\n (click)=\"deleteRow(arrayField, $index)\"\n >\n {{ ui.text('delete', language) }}\n </button>\n }\n </div>\n } @empty {\n <p class=\"ngx-record-editor__row-empty\">{{ ui.text('noRows', language) }}</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>{{ rowDrawerTitle(rowField) }}</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()\">\n {{ ui.text('cancel', language) }}\n </button>\n <button type=\"button\" class=\"ngx-form__submit\" data-testid=\"save-row\" (click)=\"saveRow()\">\n {{ ui.text('saveRow', language) }}\n </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 [layout]=\"layout\"\n [showInfoBanners]=\"false\"\n [loading]=\"loading\"\n [error]=\"error\"\n (formChange)=\"onFormChange($event)\"\n (formSubmit)=\"onFormSubmit($event)\"\n (saveRejected)=\"saveRejected.emit($event)\"\n (formReset)=\"onFormReset()\"\n (activeTabChange)=\"onActiveTabChange($event)\"\n />\n</div>\n", styles: [".ngx-record-editor{display:flex;flex-direction:column;gap:var(--ngx-gap, 16px);background:var(--ngx-color-surface, #ffffff);border:1px solid var(--ngx-color-border, #e5e7eb);border-radius:var(--ngx-radius, 12px);padding:20px}.ngx-record-editor__header{display:flex;align-items:center;justify-content:space-between;gap:14px;padding-bottom:var(--ngx-gap-sm, 16px);border-bottom:1px solid var(--ngx-color-border, #f3f4f6)}.ngx-record-editor__title-group{display:flex;align-items:center;gap:12px;min-width:0}.ngx-record-editor__avatar{width:44px;height:44px;border-radius:50%;object-fit:cover;background:var(--ngx-color-accent-soft, #e0e7ff);color:var(--ngx-color-accent, #4f46e5);border:1px solid var(--ngx-color-border, transparent);display:flex;align-items:center;justify-content:center;font-weight:700;font-size:17px;flex:none}.ngx-record-editor__title{font-size:19px;font-weight:700;color:var(--ngx-color-text, #111827);margin:0;letter-spacing:-.01em}.ngx-record-editor__subtitle{font-size:var(--ngx-hint-size, 13px);color:var(--ngx-color-muted, #6b7280)}.ngx-record-editor__header-toggle{display:flex;align-items:center;gap:8px;font-size:var(--ngx-label-size, 13px);font-weight:var(--ngx-label-weight, 600);color:var(--ngx-color-text, #374151)}.ngx-record-editor__header-toggle-text{font-weight:600}.ngx-record-editor__banner{padding:11px 14px;border-radius:var(--ngx-radius-sm, 8px);font-size:var(--ngx-font-size, 13px);border:1px solid transparent;border-left-width:3px}.ngx-record-editor__banner--info{background:var(--ngx-color-info-soft, #eff6ff);border-color:var(--ngx-color-border, #dbeafe);border-left-color:var(--ngx-color-info, #1d4ed8);color:var(--ngx-color-info, #1d4ed8);display:flex;align-items:center;justify-content:space-between;gap:12px}.ngx-record-editor__banner--error{background:var(--ngx-color-error-soft, #fef2f2);border-color:var(--ngx-color-border, #fecaca);border-left-color:var(--ngx-color-error, #b91c1c);color:var(--ngx-color-error, #b91c1c)}.ngx-record-editor__banner--warning{background:var(--ngx-color-warning-soft, #fffbeb);border-color:var(--ngx-color-border, #fef3c7);border-left-color:var(--ngx-color-warning, #b45309);color:var(--ngx-color-warning, #b45309)}.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__section-bar{display:flex;align-items:center;justify-content:flex-end;flex-wrap:wrap;gap:8px}.ngx-record-editor__section-hint{margin-right:auto;font-size:var(--ngx-hint-size, 12px);color:var(--ngx-color-muted, #6b7280)}.ngx-record-editor__rows{border:1px solid var(--ngx-color-border, #f3f4f6);border-radius:var(--ngx-radius-sm, 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:8px 10px;border-radius:var(--ngx-radius-sm, 6px);background:var(--ngx-color-surface-alt, #f9fafb)}.ngx-record-editor__row-text{flex:1;font-size:var(--ngx-font-size, 13px);overflow-wrap:anywhere}.ngx-record-editor__row-btn{background:var(--ngx-color-surface, none);border:1px solid var(--ngx-color-border, #e5e7eb);border-radius:var(--ngx-radius-sm, 6px);cursor:pointer;font:inherit;font-size:var(--ngx-hint-size, 12px);font-weight:var(--ngx-label-weight, 600);color:inherit;padding:4px 10px}.ngx-record-editor__row-btn:hover{border-color:var(--ngx-color-accent, #e5e7eb);color:var(--ngx-color-accent, inherit)}.ngx-record-editor__row-empty{font-size:var(--ngx-hint-size, 12px);color:var(--ngx-color-muted, #6b7280);margin:0}.ngx-record-editor__drawer{border:1px solid var(--ngx-color-border, #dbeafe);background:var(--ngx-color-surface-alt, #f8fafc);border-radius:var(--ngx-radius, 8px);padding:16px;display:flex;flex-direction:column;gap:12px}.ngx-record-editor__summary-panel{background:var(--ngx-color-surface-alt, #f9fafb);border:1px solid var(--ngx-color-border, #f3f4f6);border-radius:var(--ngx-radius, 8px);padding:12px 16px;display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:4px var(--ngx-gap, 12px)}.ngx-record-editor__summary-item{display:flex;flex-direction:column;cursor:pointer;padding:6px 10px;border-radius:var(--ngx-radius-sm, 6px);min-width:0;transition:background .15s}.ngx-record-editor__summary-item:hover{background:var(--ngx-color-accent-soft, #eff6ff)}.ngx-record-editor__summary-label{font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:var(--ngx-color-muted, #6b7280);font-weight:600}.ngx-record-editor__summary-val{font-size:var(--ngx-font-size, 14px);font-weight:600;color:var(--ngx-color-text, #1f2937);overflow-wrap:anywhere}@media(prefers-reduced-motion:reduce){.ngx-record-editor__summary-item{transition:none}}\n"] }]
8578
10009
  }], propDecorators: { config: [{
@@ -8738,10 +10169,10 @@ class LocalImportTransport {
8738
10169
  const csv = toCsv(spec.columns.map(column => column.header));
8739
10170
  return new Blob([csv], { type: 'text/csv;charset=utf-8' });
8740
10171
  }
8741
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: LocalImportTransport, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
8742
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: LocalImportTransport, providedIn: 'root' });
10172
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: LocalImportTransport, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
10173
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: LocalImportTransport, providedIn: 'root' });
8743
10174
  }
8744
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: LocalImportTransport, decorators: [{
10175
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: LocalImportTransport, decorators: [{
8745
10176
  type: Injectable,
8746
10177
  args: [{ providedIn: 'root' }]
8747
10178
  }] });
@@ -8965,8 +10396,8 @@ class ImportErrorsComponent {
8965
10396
  .sort((a, b) => a[0] - b[0])
8966
10397
  .map(([row, problems]) => ({ row, problems }));
8967
10398
  }
8968
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: ImportErrorsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
8969
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: ImportErrorsComponent, isStandalone: true, selector: "ngx-import-errors", inputs: { errors: "errors", language: "language" }, ngImport: i0, template: `
10399
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: ImportErrorsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
10400
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: ImportErrorsComponent, isStandalone: true, selector: "ngx-import-errors", inputs: { errors: "errors", language: "language" }, ngImport: i0, template: `
8970
10401
  @if (errors.length) {
8971
10402
  <section class="ngx-import-errors" data-testid="import-errors">
8972
10403
  <h4 class="ngx-import-errors__heading">{{ ui.text('importErrorsHeading', language) }}</h4>
@@ -8998,7 +10429,7 @@ class ImportErrorsComponent {
8998
10429
  }
8999
10430
  `, isInline: true, styles: [".ngx-import-errors__heading{margin:0 0 8px;font-size:var(--ngx-font-size, 14px);color:var(--ngx-color-error, #b91c1c)}.ngx-import-errors__table{width:100%;border-collapse:collapse}.ngx-import-errors__table th,.ngx-import-errors__table td{text-align:left;padding:6px 10px;border-bottom:1px solid var(--ngx-color-border, #e5e7eb);vertical-align:top}.ngx-import-errors__table thead th{color:var(--ngx-color-muted, #6b7280)}.ngx-import-errors__table tbody th{white-space:nowrap;color:var(--ngx-color-error, #b91c1c)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
9000
10431
  }
9001
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: ImportErrorsComponent, decorators: [{
10432
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: ImportErrorsComponent, decorators: [{
9002
10433
  type: Component,
9003
10434
  args: [{ selector: 'ngx-import-errors', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [], template: `
9004
10435
  @if (errors.length) {
@@ -9109,8 +10540,8 @@ class ImportMapperComponent {
9109
10540
  entries,
9110
10541
  };
9111
10542
  }
9112
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: ImportMapperComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
9113
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: ImportMapperComponent, isStandalone: true, selector: "ngx-import-mapper", inputs: { config: "config", headers: "headers", plan: "plan", language: "language" }, outputs: { planChange: "planChange" }, usesOnChanges: true, ngImport: i0, template: `
10543
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: ImportMapperComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
10544
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: ImportMapperComponent, isStandalone: true, selector: "ngx-import-mapper", inputs: { config: "config", headers: "headers", plan: "plan", language: "language" }, outputs: { planChange: "planChange" }, usesOnChanges: true, ngImport: i0, template: `
9114
10545
  <section class="ngx-import-mapper" data-testid="import-mapper">
9115
10546
  <p class="ngx-import-mapper__summary" data-testid="import-mapped-count">
9116
10547
  {{ ui.text('importMappedCount', language, { mapped: mappedCount(), total: rows().length }) }}
@@ -9168,7 +10599,7 @@ class ImportMapperComponent {
9168
10599
  </section>
9169
10600
  `, isInline: true, styles: [".ngx-import-mapper__summary,.ngx-import-mapper__warning,.ngx-import-mapper__note{margin:0 0 10px}.ngx-import-mapper__warning{color:var(--ngx-color-warning, #b45309);background:var(--ngx-color-warning-soft, #fffbeb);border:1px solid var(--ngx-color-border, #fef3c7);border-radius:var(--ngx-radius-sm, 6px);padding:8px 12px}.ngx-import-mapper__note{color:var(--ngx-color-muted, #6b7280)}.ngx-import-mapper__table{width:100%;border-collapse:collapse}.ngx-import-mapper__table th,.ngx-import-mapper__table td{text-align:left;padding:8px 10px;border-bottom:1px solid var(--ngx-color-border, #e5e7eb);vertical-align:middle}.ngx-import-mapper__table thead th{color:var(--ngx-color-muted, #6b7280);font-size:var(--ngx-font-size, 13px);text-transform:uppercase;letter-spacing:.04em}.ngx-import-mapper__required{color:var(--ngx-color-error, #b91c1c);margin-left:2px}.ngx-import-mapper__select{min-width:220px;max-width:100%;padding:6px 8px;border:1px solid var(--ngx-color-border, #d1d5db);border-radius:var(--ngx-radius-sm, 6px);background:var(--ngx-color-surface, #ffffff);color:inherit;font:inherit}.ngx-import-mapper__guess{margin-left:8px;padding:2px 8px;border-radius:999px;font-size:12px;color:var(--ngx-color-info, #1d4ed8);background:var(--ngx-color-info-soft, #eff6ff)}@media(max-width:640px){.ngx-import-mapper__select{min-width:0;width:100%}}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { 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.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
9170
10601
  }
9171
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: ImportMapperComponent, decorators: [{
10602
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: ImportMapperComponent, decorators: [{
9172
10603
  type: Component,
9173
10604
  args: [{ selector: 'ngx-import-mapper', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [FormsModule], template: `
9174
10605
  <section class="ngx-import-mapper" data-testid="import-mapper">
@@ -9310,8 +10741,8 @@ class ImportPreviewComponent {
9310
10741
  }),
9311
10742
  })));
9312
10743
  }
9313
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: ImportPreviewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
9314
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: ImportPreviewComponent, isStandalone: true, selector: "ngx-import-preview", inputs: { config: "config", plan: "plan", rows: "rows", lookups: "lookups", rules: "rules", language: "language" }, usesOnChanges: true, ngImport: i0, template: `
10744
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: ImportPreviewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
10745
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: ImportPreviewComponent, isStandalone: true, selector: "ngx-import-preview", inputs: { config: "config", plan: "plan", rows: "rows", lookups: "lookups", rules: "rules", language: "language" }, usesOnChanges: true, ngImport: i0, template: `
9315
10746
  <section class="ngx-import-preview" data-testid="import-preview">
9316
10747
  <h4 class="ngx-import-preview__heading">
9317
10748
  {{ ui.text('importPreviewHeading', language, { count: rows.length }) }}
@@ -9349,7 +10780,7 @@ class ImportPreviewComponent {
9349
10780
  </section>
9350
10781
  `, isInline: true, styles: [".ngx-import-preview__heading{margin:0 0 8px;font-size:var(--ngx-font-size, 14px)}.ngx-import-preview__scroll{overflow-x:auto;border:1px solid var(--ngx-color-border, #e5e7eb);border-radius:var(--ngx-radius-sm, 6px)}.ngx-import-preview__table{width:100%;border-collapse:collapse;white-space:nowrap}.ngx-import-preview__table th,.ngx-import-preview__table td{text-align:left;padding:6px 10px;border-bottom:1px solid var(--ngx-color-border, #f3f4f6)}.ngx-import-preview__table thead th{background:var(--ngx-color-surface-alt, #f9fafb);color:var(--ngx-color-muted, #6b7280)}.ngx-import-preview__cell--error{color:var(--ngx-color-error, #b91c1c);background:var(--ngx-color-error-soft, #fef2f2)}.ngx-import-preview__row--rejected td{opacity:.65}.ngx-import-preview__reason td{white-space:normal;color:var(--ngx-color-error, #b91c1c);background:var(--ngx-color-error-soft, #fef2f2)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
9351
10782
  }
9352
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: ImportPreviewComponent, decorators: [{
10783
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: ImportPreviewComponent, decorators: [{
9353
10784
  type: Component,
9354
10785
  args: [{ selector: 'ngx-import-preview', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [], template: `
9355
10786
  <section class="ngx-import-preview" data-testid="import-preview">
@@ -9458,8 +10889,8 @@ class ImportTemplateComponent {
9458
10889
  selectAll(selected) {
9459
10890
  this.choices.update(choices => choices.map(choice => ({ ...choice, selected })));
9460
10891
  }
9461
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: ImportTemplateComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
9462
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: ImportTemplateComponent, isStandalone: true, selector: "ngx-import-template", inputs: { config: "config", language: "language" }, outputs: { download: "download" }, usesOnChanges: true, ngImport: i0, template: `
10892
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: ImportTemplateComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
10893
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: ImportTemplateComponent, isStandalone: true, selector: "ngx-import-template", inputs: { config: "config", language: "language" }, outputs: { download: "download" }, usesOnChanges: true, ngImport: i0, template: `
9463
10894
  <section class="ngx-import-template" data-testid="import-template">
9464
10895
  <h4 class="ngx-import-template__heading">{{ ui.text('importTemplateHeading', language) }}</h4>
9465
10896
  <p class="ngx-import-template__explain">{{ ui.text('importTemplateExplain', language) }}</p>
@@ -9510,7 +10941,7 @@ class ImportTemplateComponent {
9510
10941
  </section>
9511
10942
  `, isInline: true, styles: [".ngx-import-template{padding:14px;border:1px solid var(--ngx-color-border, #e5e7eb);border-radius:var(--ngx-radius-sm, 6px);background:var(--ngx-color-surface-alt, #f9fafb)}.ngx-import-template__heading{margin:0 0 4px;font-size:var(--ngx-font-size, 14px)}.ngx-import-template__explain{margin:0 0 10px;color:var(--ngx-color-muted, #6b7280)}.ngx-import-template__bulk{display:flex;gap:8px;margin-bottom:10px}.ngx-import-template__list{list-style:none;margin:0 0 10px;padding:0;max-height:240px;overflow-y:auto;display:grid;gap:4px}.ngx-import-template__item{display:flex;flex-wrap:wrap;align-items:baseline;gap:8px}.ngx-import-template__item label{display:flex;align-items:center;gap:6px;cursor:pointer}.ngx-import-template__note{color:var(--ngx-color-muted, #6b7280);font-size:12px}.ngx-import-template__unsupported{margin:0 0 10px;color:var(--ngx-color-muted, #6b7280)}.ngx-import-template button{padding:6px 12px;border:1px solid var(--ngx-color-border, #d1d5db);border-radius:var(--ngx-radius-sm, 6px);background:var(--ngx-color-surface, #ffffff);color:inherit;font:inherit;cursor:pointer}.ngx-import-template button:disabled{opacity:.55;cursor:not-allowed}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
9512
10943
  }
9513
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: ImportTemplateComponent, decorators: [{
10944
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: ImportTemplateComponent, decorators: [{
9514
10945
  type: Component,
9515
10946
  args: [{ selector: 'ngx-import-template', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [], template: `
9516
10947
  <section class="ngx-import-template" data-testid="import-template">
@@ -9796,8 +11227,8 @@ class EntityImportComponent {
9796
11227
  this.problem.set(null);
9797
11228
  this.step.set('upload');
9798
11229
  }
9799
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: EntityImportComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
9800
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: EntityImportComponent, isStandalone: true, selector: "ngx-entity-import", inputs: { config: "config", rules: "rules", lookups: "lookups", language: "language", templateFormat: "templateFormat" }, outputs: { importComplete: "importComplete", templateReady: "templateReady" }, ngImport: i0, template: `
11230
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: EntityImportComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
11231
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: EntityImportComponent, isStandalone: true, selector: "ngx-entity-import", inputs: { config: "config", rules: "rules", lookups: "lookups", language: "language", templateFormat: "templateFormat" }, outputs: { importComplete: "importComplete", templateReady: "templateReady" }, ngImport: i0, template: `
9801
11232
  <div class="ngx-import" data-testid="entity-import">
9802
11233
  <ol class="ngx-import__steps" data-testid="import-steps">
9803
11234
  @for (name of steps; track name) {
@@ -9915,7 +11346,7 @@ class EntityImportComponent {
9915
11346
  </div>
9916
11347
  `, isInline: true, styles: [".ngx-import{display:flex;flex-direction:column;gap:var(--ngx-gap, 16px);color:var(--ngx-color-text, #1f2937);font-size:var(--ngx-font-size, 14px)}.ngx-import__steps{display:flex;flex-wrap:wrap;gap:8px;list-style:none;margin:0;padding:0}.ngx-import__step{padding:4px 12px;border:1px solid var(--ngx-color-border, #e5e7eb);border-radius:var(--ngx-radius-sm, 6px);color:var(--ngx-color-muted, #6b7280);background:var(--ngx-color-surface-alt, #f9fafb)}.ngx-import__step--current{color:var(--ngx-color-accent, #4f46e5);border-color:var(--ngx-color-accent, #4f46e5);background:var(--ngx-color-accent-soft, #e0e7ff);font-weight:600}.ngx-import__problem{margin:0;padding:10px 12px;border:1px solid var(--ngx-color-border, #fecaca);border-radius:var(--ngx-radius-sm, 6px);background:var(--ngx-color-error-soft, #fef2f2);color:var(--ngx-color-error, #b91c1c);overflow-wrap:anywhere}.ngx-import__file{display:flex;flex-direction:column;gap:6px;padding:14px;border:1px dashed var(--ngx-color-border, #d1d5db);border-radius:var(--ngx-radius-sm, 6px);background:var(--ngx-color-surface, #ffffff)}.ngx-import button{align-self:flex-start;padding:8px 16px;border:1px solid var(--ngx-color-border, #d1d5db);border-radius:var(--ngx-radius-sm, 6px);background:var(--ngx-color-surface, #ffffff);color:inherit;font:inherit;cursor:pointer}.ngx-import button:hover:not(:disabled){border-color:var(--ngx-color-accent, #4f46e5);background:var(--ngx-color-accent-soft, #e0e7ff)}.ngx-import button:disabled{opacity:.55;cursor:not-allowed}\n"], dependencies: [{ kind: "component", type: ImportTemplateComponent, selector: "ngx-import-template", inputs: ["config", "language"], outputs: ["download"] }, { kind: "component", type: ImportMapperComponent, selector: "ngx-import-mapper", inputs: ["config", "headers", "plan", "language"], outputs: ["planChange"] }, { kind: "component", type: ImportPreviewComponent, selector: "ngx-import-preview", inputs: ["config", "plan", "rows", "lookups", "rules", "language"] }, { kind: "component", type: ImportErrorsComponent, selector: "ngx-import-errors", inputs: ["errors", "language"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
9917
11348
  }
9918
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: EntityImportComponent, decorators: [{
11349
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: EntityImportComponent, decorators: [{
9919
11350
  type: Component,
9920
11351
  args: [{ selector: 'ngx-entity-import', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [
9921
11352
  ImportTemplateComponent,
@@ -10085,5 +11516,5 @@ function saveBlob(blob, filename) {
10085
11516
  * Generated bundle index. Do not edit.
10086
11517
  */
10087
11518
 
10088
- export { ASYNC_VALIDATOR_REGISTRY, ArrayFieldComponent, BooleanFieldComponent, COMMON_MODULES_REGISTRY, CONFIG_SOURCE, CascadeDataService, CheckboxFieldComponent, ConfigSourceService, CurrencyFieldComponent, DEFAULT_UI_TEXT, DEFAULT_VALIDATION_MESSAGES, DateFieldComponent, DateTimeFieldComponent, DropdownFieldComponent, DynamicFieldComponent, DynamicFormComponent, DynamicRecordFormComponent, ENTITY_REF_CACHE_STORE, ENTITY_REF_REGISTRY, EmailFieldComponent, EntityImportComponent, EntityRefFieldComponent, EntityRefRegistryService, EntityRefSelectionService, EntityReferenceService, FIELD_TYPE_REGISTRY, FIELD_TYPE_SETS, FieldRegistryService, FileFieldComponent, GroupFieldComponent, HOOK_REGISTRY, HookRegistryService, HttpImportTransport, IMPORT_TRANSPORT, ImageFieldComponent, ImportErrorsComponent, ImportMapperComponent, ImportPreviewComponent, ImportTemplateComponent, InMemoryEntityRefCacheStore, LOOKUP_REGISTRY, LocalImportTransport, LookupRegistryService, MARKDOWN_RENDERER, MASKED_PLACEHOLDER, MASKED_ROLES, MarkdownFieldComponent, MonthYearFieldComponent, MultiSelectFieldComponent, NumberFieldComponent, PasswordFieldComponent, RECORD_MIGRATIONS, RadioFieldComponent, RbacService, RulesEvaluationService, SHEET_PARSER, SYSTEM_DEFAULT_CAN_EDIT, TextFieldComponent, TextareaFieldComponent, TimeFieldComponent, UI_TEXT, UPLOAD_HANDLER, UiTextService, VALIDATION_MESSAGES, VALIDATOR_REGISTRY, ValidationMessagesService, ValidatorRegistryService, builtInFieldTypes, defaultSheetParser, fieldDescribedBy, fieldDomId, nextFieldInstanceId, provideBuiltInFieldTypes, provideFieldTypes, provideHttpImportTransport, provideNgxDynamicEntity, refreshChoiceOptions, resolveUiText };
11519
+ export { ASYNC_VALIDATOR_REGISTRY, ArrayFieldComponent, BooleanFieldComponent, COMMON_MODULES_REGISTRY, CONFIG_SOURCE, CascadeDataService, CheckboxFieldComponent, ColorFieldComponent, ConfigSourceService, CurrencyFieldComponent, DEFAULT_UI_TEXT, DEFAULT_VALIDATION_MESSAGES, DateFieldComponent, DateTimeFieldComponent, DropdownFieldComponent, DynamicFieldComponent, DynamicFormComponent, DynamicRecordFormComponent, ENTITY_REF_CACHE_STORE, ENTITY_REF_REGISTRY, EmailFieldComponent, EntityImportComponent, EntityRefFieldComponent, EntityRefRegistryService, EntityRefSelectionService, EntityReferenceService, FIELD_TYPE_REGISTRY, FIELD_TYPE_SETS, FieldRegistryService, FileFieldComponent, GroupFieldComponent, HOOK_REGISTRY, HookRegistryService, HttpImportTransport, IMPORT_TRANSPORT, ImageFieldComponent, ImportErrorsComponent, ImportMapperComponent, ImportPreviewComponent, ImportTemplateComponent, InMemoryEntityRefCacheStore, LOOKUP_REGISTRY, LocalImportTransport, LookupRegistryService, MARKDOWN_RENDERER, MASKED_PLACEHOLDER, MASKED_ROLES, MarkdownFieldComponent, MonthYearFieldComponent, MultiSelectFieldComponent, NumberFieldComponent, PasswordFieldComponent, PhoneFieldComponent, RECORD_MIGRATIONS, RadioFieldComponent, RatingFieldComponent, RbacService, RulesEvaluationService, SHEET_PARSER, SYSTEM_DEFAULT_CAN_EDIT, SliderFieldComponent, TagsFieldComponent, TextFieldComponent, TextareaFieldComponent, TimeFieldComponent, UI_TEXT, UPLOAD_HANDLER, UiTextService, UrlFieldComponent, VALIDATION_MESSAGES, VALIDATOR_REGISTRY, ValidationMessagesService, ValidatorRegistryService, builtInFieldTypes, defaultSheetParser, fieldDescribedBy, fieldDomId, nextFieldInstanceId, phoneValidator, provideBuiltInFieldTypes, provideFieldTypes, provideHttpImportTransport, provideNgxDynamicEntity, refreshChoiceOptions, resolveUiText, urlValidator };
10089
11520
  //# sourceMappingURL=ngx-dynamic-entity.mjs.map