ngx-dynamic-entity 1.7.0 → 1.8.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.
@@ -1263,12 +1263,12 @@ class DateFieldComponent {
1263
1263
  formatDate(value) {
1264
1264
  if (!value)
1265
1265
  return '—';
1266
- try {
1267
- return new Date(value).toLocaleDateString();
1268
- }
1269
- catch {
1270
- return value;
1271
- }
1266
+ // `new Date('nonsense')` does not throw and `toLocaleDateString()` returns the *string*
1267
+ // "Invalid Date", so the try/catch this replaced never fired and readers saw that text
1268
+ // instead of their data. Records outlive schemas — a field retyped from text to date can
1269
+ // hold anything — so an unparseable value is shown as stored.
1270
+ const parsed = new Date(value);
1271
+ return Number.isNaN(parsed.getTime()) ? value : parsed.toLocaleDateString();
1272
1272
  }
1273
1273
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: DateFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1274
1274
  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: `
@@ -2156,7 +2156,11 @@ class RadioFieldComponent {
2156
2156
  * into an id attribute.
2157
2157
  */
2158
2158
  getRadioId(option) {
2159
- const text = String(this.getOptVal(option) ?? 'opt');
2159
+ // `??` cannot fire here: `resolveOptionValue` returns '' for an empty option, not null,
2160
+ // so every valueless option produced the same id — and duplicate ids break the `for`
2161
+ // that ties each label to its input.
2162
+ const raw = String(this.getOptVal(option) ?? '').trim();
2163
+ const text = raw || 'opt';
2160
2164
  return `${this.field.id}-${text.trim().replace(/\s+/g, '_').toLowerCase()}`;
2161
2165
  }
2162
2166
  getOptVal(option) {
@@ -4333,11 +4337,50 @@ class DynamicFormComponent {
4333
4337
  }
4334
4338
  if (changes['config'] || changes['initialData']) {
4335
4339
  this.buildForm();
4336
- if (this.visibleTabs.length > 0 && !this.activeTab()) {
4337
- this.setActiveTab(this.visibleTabs[0].id);
4340
+ // Clear *before* the guard below, not after it.
4341
+ //
4342
+ // The reset used to sit behind `if (visibleTabs.length === 0) return`, so a swap that
4343
+ // arrived while no tab was visible — a config still being applied, or every tab hidden
4344
+ // by a rule at that instant — skipped it entirely and the previous record's tab
4345
+ // survived into the next one. Clearing is unconditional and cheap, and safe on its own
4346
+ // because `activeTabConfig` already falls back to the first visible tab when nothing
4347
+ // is selected. Selecting the tab is what needs tabs to exist; forgetting the old one
4348
+ // does not.
4349
+ if (changes['config'] || this.isRecordSwap(changes['initialData'])) {
4350
+ this.activeTab.set('');
4351
+ this.activeSubTab.set('');
4352
+ }
4353
+ if (this.visibleTabs.length === 0)
4354
+ return;
4355
+ // The first tab used to be chosen only when no tab was active yet, and nothing ever
4356
+ // cleared `activeTab` — so a form that stays mounted while `initialData` is swapped
4357
+ // opened the next record on the tab the previous one was left on. The demo escaped it
4358
+ // by destroying the form between records; a host that keeps it mounted did not.
4359
+ //
4360
+ // Resetting here is consistent with what already happens: `buildForm` assigns a brand
4361
+ // new FormGroup, so a changed `initialData` has already discarded every control and
4362
+ // all validation state. The tab was the one thing pretending nothing had changed.
4363
+ if (!this.activeTab()) {
4364
+ // No focus steal: the panel gains focus when a *user* picks a tab, not when a host
4365
+ // swaps the record underneath them.
4366
+ this.setActiveTab(this.visibleTabs[0].id, { focusPanel: false });
4338
4367
  }
4339
4368
  }
4340
4369
  }
4370
+ /**
4371
+ * Whether an `initialData` change represents loading a different record.
4372
+ *
4373
+ * Not every change does. A template binding like `[initialData]="record() || {}"` yields a
4374
+ * fresh object literal on each evaluation, so `ngOnChanges` fires with two empty objects
4375
+ * and nothing has actually been loaded — resetting there would drag someone back to the
4376
+ * first tab while they were filling in a new record on the third.
4377
+ */
4378
+ isRecordSwap(change) {
4379
+ if (!change || change.isFirstChange())
4380
+ return false;
4381
+ const empty = (v) => v === null || v === undefined || (typeof v === 'object' && Object.keys(v).length === 0);
4382
+ return !(empty(change.previousValue) && empty(change.currentValue));
4383
+ }
4341
4384
  ngOnDestroy() {
4342
4385
  this.valueSub?.unsubscribe();
4343
4386
  this.statusSub?.unsubscribe();
@@ -4950,7 +4993,12 @@ class DynamicFormComponent {
4950
4993
  focusActivePanel() {
4951
4994
  if (typeof requestAnimationFrame !== 'function')
4952
4995
  return;
4953
- requestAnimationFrame(() => this.formPanel?.nativeElement?.focus?.());
4996
+ requestAnimationFrame(() =>
4997
+ // `preventScroll` because this focus exists to tell a screen reader the panel changed,
4998
+ // not to move the page. The default scrolls the panel into view, which on a short
4999
+ // viewport jumps the layout — enough that a Save button below it moves out from under
5000
+ // a tap. The panel sits directly beneath the tab strip and is already on screen.
5001
+ this.formPanel?.nativeElement?.focus?.({ preventScroll: true }));
4954
5002
  }
4955
5003
  /** Accessible name for the field panel — it is the tabpanel for the active tab. */
4956
5004
  get activeTabLabel() {