ngx-dynamic-entity 2.0.0 → 2.0.1

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.
@@ -1797,7 +1797,11 @@ class EmailFieldComponent {
1797
1797
  }}</span>
1798
1798
  } @else if (readonly) {
1799
1799
  <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">
1800
- <a [href]="'mailto:' + control.value" class="ngx-field__email-link">{{ control.value || '—' }}</a>
1800
+ @if (control.value) {
1801
+ <a [href]="'mailto:' + control.value" class="ngx-field__email-link">{{ control.value }}</a>
1802
+ } @else {
1803
+ —
1804
+ }
1801
1805
  </span>
1802
1806
  } @else {
1803
1807
  <input
@@ -1860,7 +1864,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
1860
1864
  }}</span>
1861
1865
  } @else if (readonly) {
1862
1866
  <span class="ngx-field__value" [attr.data-testid]="'field-' + field.id + '-value'">
1863
- <a [href]="'mailto:' + control.value" class="ngx-field__email-link">{{ control.value || '—' }}</a>
1867
+ @if (control.value) {
1868
+ <a [href]="'mailto:' + control.value" class="ngx-field__email-link">{{ control.value }}</a>
1869
+ } @else {
1870
+ —
1871
+ }
1864
1872
  </span>
1865
1873
  } @else {
1866
1874
  <input
@@ -3183,7 +3191,7 @@ class MonthYearFieldComponent {
3183
3191
  >
3184
3192
  <option value="">{{ ui.text('month', language) }}</option>
3185
3193
  @for (m of months; track m.value) {
3186
- <option [value]="m.value">{{ m.label }}</option>
3194
+ <option [value]="m.value" [selected]="m.value === selectedMonth">{{ m.label }}</option>
3187
3195
  }
3188
3196
  </select>
3189
3197
  <select
@@ -3198,7 +3206,7 @@ class MonthYearFieldComponent {
3198
3206
  >
3199
3207
  <option value="">{{ ui.text('year', language) }}</option>
3200
3208
  @for (y of years; track y) {
3201
- <option [value]="y">{{ y }}</option>
3209
+ <option [value]="y" [selected]="y.toString() === selectedYear">{{ y }}</option>
3202
3210
  }
3203
3211
  </select>
3204
3212
  </div>
@@ -3269,7 +3277,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
3269
3277
  >
3270
3278
  <option value="">{{ ui.text('month', language) }}</option>
3271
3279
  @for (m of months; track m.value) {
3272
- <option [value]="m.value">{{ m.label }}</option>
3280
+ <option [value]="m.value" [selected]="m.value === selectedMonth">{{ m.label }}</option>
3273
3281
  }
3274
3282
  </select>
3275
3283
  <select
@@ -3284,7 +3292,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
3284
3292
  >
3285
3293
  <option value="">{{ ui.text('year', language) }}</option>
3286
3294
  @for (y of years; track y) {
3287
- <option [value]="y">{{ y }}</option>
3295
+ <option [value]="y" [selected]="y.toString() === selectedYear">{{ y }}</option>
3288
3296
  }
3289
3297
  </select>
3290
3298
  </div>
@@ -3818,8 +3826,14 @@ class RadioFieldComponent {
3818
3826
  maskedText = inject(MASKED_PLACEHOLDER, { optional: true }) ?? 'XXXXXXXXX';
3819
3827
  messages = inject(ValidationMessagesService);
3820
3828
  lookups = inject(LookupRegistryService);
3829
+ cdr = inject(ChangeDetectorRef);
3821
3830
  _field;
3822
3831
  _language = 'en';
3832
+ _control;
3833
+ _controlSub;
3834
+ ngOnDestroy() {
3835
+ this._controlSub?.unsubscribe();
3836
+ }
3823
3837
  /** Setter-based so options resolve however the input is set — see `refreshChoiceOptions`. */
3824
3838
  set field(value) {
3825
3839
  this._field = value;
@@ -3835,11 +3849,25 @@ class RadioFieldComponent {
3835
3849
  get language() {
3836
3850
  return this._language;
3837
3851
  }
3838
- control;
3852
+ set control(value) {
3853
+ this._control = value;
3854
+ this._controlSub?.unsubscribe();
3855
+ this._controlSub = this._control?.valueChanges?.subscribe(() => {
3856
+ this.cdr.markForCheck();
3857
+ });
3858
+ }
3859
+ get control() {
3860
+ return this._control;
3861
+ }
3839
3862
  readonly = false;
3840
3863
  masked = false;
3841
3864
  /** Inline `options`, or the field's named list resolved through the registry (§6.3). */
3842
3865
  options = signal([], ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
3866
+ onSelect(option) {
3867
+ this.control?.setValue(option);
3868
+ this.control?.markAsTouched();
3869
+ this.control?.markAsDirty();
3870
+ }
3843
3871
  get label() {
3844
3872
  return resolveLabel(this.field?.label, this.language);
3845
3873
  }
@@ -3881,6 +3909,10 @@ class RadioFieldComponent {
3881
3909
  getOptLabel(option) {
3882
3910
  return resolveOptionLabel(option, this.language);
3883
3911
  }
3912
+ /** Whether the given option matches the control's current value. */
3913
+ isChecked(option) {
3914
+ return valuesMatch(this.control?.value, option, this.language);
3915
+ }
3884
3916
  /** Read-only display — synchronous, per §6.2. See `DropdownFieldComponent.getLabel`. */
3885
3917
  getSelectedLabel() {
3886
3918
  const value = this.control?.value;
@@ -3959,10 +3991,12 @@ class RadioFieldComponent {
3959
3991
  [attr.data-testid]="'field-' + field.id + '-option-' + optionSlug(option)"
3960
3992
  type="radio"
3961
3993
  class="ngx-field__radio-input"
3962
- [formControl]="$any(control)"
3994
+ [name]="domId()"
3963
3995
  [attr.aria-describedby]="describedBy()"
3964
3996
  [value]="getOptStoredVal(option)"
3997
+ [checked]="isChecked(option)"
3965
3998
  [attr.disabled]="field.disabled ? true : null"
3999
+ (change)="onSelect(option)"
3966
4000
  />
3967
4001
  <span class="ngx-field__radio-label">{{ getOptLabel(option) }}</span>
3968
4002
  </label>
@@ -3976,7 +4010,7 @@ class RadioFieldComponent {
3976
4010
  }
3977
4011
  </fieldset>
3978
4012
  </div>
3979
- `, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.RadioControlValueAccessor, selector: "input[type=radio][formControlName],input[type=radio][formControl],input[type=radio][ngModel]", inputs: ["name", "formControlName", "value"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4013
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3980
4014
  }
3981
4015
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: RadioFieldComponent, decorators: [{
3982
4016
  type: Component,
@@ -4028,10 +4062,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
4028
4062
  [attr.data-testid]="'field-' + field.id + '-option-' + optionSlug(option)"
4029
4063
  type="radio"
4030
4064
  class="ngx-field__radio-input"
4031
- [formControl]="$any(control)"
4065
+ [name]="domId()"
4032
4066
  [attr.aria-describedby]="describedBy()"
4033
4067
  [value]="getOptStoredVal(option)"
4068
+ [checked]="isChecked(option)"
4034
4069
  [attr.disabled]="field.disabled ? true : null"
4070
+ (change)="onSelect(option)"
4035
4071
  />
4036
4072
  <span class="ngx-field__radio-label">{{ getOptLabel(option) }}</span>
4037
4073
  </label>
@@ -5121,16 +5157,11 @@ class DynamicFieldComponent {
5121
5157
  ref.setInput('masked', masked);
5122
5158
  }
5123
5159
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: DynamicFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5124
- 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, changeDetection: i0.ChangeDetectionStrategy.OnPush });
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 });
5125
5161
  }
5126
5162
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: DynamicFieldComponent, decorators: [{
5127
5163
  type: Component,
5128
- args: [{
5129
- changeDetection: ChangeDetectionStrategy.OnPush,
5130
- selector: 'ngx-dynamic-field',
5131
- standalone: true,
5132
- template: `<ng-container #fieldHost></ng-container>`,
5133
- }]
5164
+ 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"] }]
5134
5165
  }], propDecorators: { field: [{
5135
5166
  type: Input
5136
5167
  }], control: [{
@@ -7916,11 +7947,11 @@ class DynamicFormComponent {
7916
7947
  this.formReset.emit();
7917
7948
  }
7918
7949
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: DynamicFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
7919
- 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--readonly .ngx-form__panel{grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:2px var(--ngx-gap, 18px)}.ngx-form--readonly .ngx-form__field-slot{grid-column:auto}.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,.ngx-form--readonly .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 });
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 });
7920
7951
  }
7921
7952
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: DynamicFormComponent, decorators: [{
7922
7953
  type: Component,
7923
- 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--readonly .ngx-form__panel{grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:2px var(--ngx-gap, 18px)}.ngx-form--readonly .ngx-form__field-slot{grid-column:auto}.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,.ngx-form--readonly .ngx-form__panel{grid-template-columns:minmax(0,1fr)}.ngx-form__field-slot{grid-column:1 / -1!important}}\n"] }]
7954
+ 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"] }]
7924
7955
  }], propDecorators: { config: [{
7925
7956
  type: Input
7926
7957
  }], rules: [{