ngx-dynamic-entity 1.4.0 → 1.5.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, DestroyRef, Injector, isDevMode, ViewContainerRef, ViewChild, forwardRef, ChangeDetectorRef, Optional, Inject, EventEmitter, computed, HostListener, ViewChildren, Output, ElementRef, afterNextRender } from '@angular/core';
3
3
  import * as i1 from '@angular/forms';
4
4
  import { ReactiveFormsModule, Validators, FormBuilder, FormArray, FormGroup } from '@angular/forms';
5
- import { resolveLabel, formatDisplayValue, normalizeLookupValues, lookupValuesToOptions, valuesMatch, getOptionStoredValue, resolveOptionLabel, resolveOptionValue, buildReferenceCacheKey, normalizeReferenceOptions, entityKeyFromCacheKey, applyCascadeFilter, resolveEffectiveMask, shouldMaskField, findTab, fileRefName, evaluateFormRules, filterRulesForTab, evaluateFieldVisibility, normalizeConfigOptions, migrateRecord, getTabData, getValueByPath, setTabData, setValueByPath, normalizeArrayStructures, ambiguousFieldIds, toRefToken, collectFieldScopes, fieldRefFor, refOf, getTabPath, applyAutoPatch, applyPatchOnTrue } from '@dynamic-entity/core';
5
+ import { resolveLabel, formatDisplayValue, normalizeLookupValues, lookupValuesToOptions, valuesMatch, getOptionStoredValue, resolveOptionLabel, resolveOptionValue, buildReferenceCacheKey, normalizeReferenceOptions, entityKeyFromCacheKey, applyCascadeFilter, resolveEffectiveMask, shouldMaskField, findTab, fileRefName, evaluateFormRules, filterRulesForTab, evaluateFieldVisibility, normalizeConfigOptions, migrateRecord, getTabData, getValueByPath, setTabData, setValueByPath, normalizeArrayStructures, ambiguousFieldIds, parseFieldRef, toRefToken, collectFieldScopes, fieldRefFor, refOf, getTabPath, applyAutoPatch, applyPatchOnTrue } 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';
@@ -4096,12 +4096,20 @@ class DynamicFormComponent {
4096
4096
  this.statusSub?.unsubscribe();
4097
4097
  this.selectionSub?.unsubscribe();
4098
4098
  }
4099
- setActiveTab(tabId) {
4099
+ /**
4100
+ * `focusPanel: false` is for a caller that is about to focus something more specific.
4101
+ *
4102
+ * Activating a tab moves focus into its panel, which is right for a keyboard user pressing
4103
+ * a tab. A quick-jump also switches tabs, but then focuses the field it was aiming at — and
4104
+ * the panel focus runs on `requestAnimationFrame`, after `afterNextRender`, so it landed
4105
+ * second and took the focus back every time. Saying so is better than out-timing it.
4106
+ */
4107
+ setActiveTab(tabId, options) {
4100
4108
  const changed = this.activeTab() !== tabId;
4101
4109
  if (changed)
4102
4110
  this.activeTabChange.emit(tabId);
4103
4111
  this.activeTab.set(tabId);
4104
- if (changed)
4112
+ if (changed && options?.focusPanel !== false)
4105
4113
  this.focusActivePanel();
4106
4114
  const parent = findTab(this.tabs, tabId);
4107
4115
  if (parent?.children?.length) {
@@ -4111,10 +4119,10 @@ class DynamicFormComponent {
4111
4119
  this.activeSubTab.set('');
4112
4120
  }
4113
4121
  }
4114
- setActiveSubTab(subTabId) {
4122
+ setActiveSubTab(subTabId, options) {
4115
4123
  const changed = this.activeSubTab() !== subTabId;
4116
4124
  this.activeSubTab.set(subTabId);
4117
- if (changed)
4125
+ if (changed && options?.focusPanel !== false)
4118
4126
  this.focusActivePanel();
4119
4127
  }
4120
4128
  getFieldSpan(field) {
@@ -4411,9 +4419,8 @@ class DynamicFormComponent {
4411
4419
  * `flattenFormValues` keys by bare id, so one of the two silently wins and the rule reads
4412
4420
  * whichever landed there.
4413
4421
  *
4414
- * `validateConfig` catches this for `showWhen` and cascade parents, which live in the
4415
- * config. Rules do not: they arrive as a separate `@Input`, so the config alone cannot be
4416
- * checked for them and this is the only place both are in hand.
4422
+ * `validateConfig({ rules })` catches this statically. The check here remains because
4423
+ * consumers that never call the validator still get a signal in development.
4417
4424
  */
4418
4425
  warnAmbiguousRuleReferences() {
4419
4426
  if (!isDevMode() || !this.rules?.length || !this.config)
@@ -4434,14 +4441,18 @@ class DynamicFormComponent {
4434
4441
  referenced.add(target.id);
4435
4442
  }
4436
4443
  }
4437
- for (const id of referenced) {
4444
+ for (const raw of referenced) {
4445
+ const parsed = parseFieldRef(raw);
4446
+ if (parsed.kind === 'ref')
4447
+ continue;
4448
+ const id = parsed.value;
4438
4449
  const scopes = ambiguous.get(id);
4439
4450
  if (!scopes || DynamicFormComponent.warnedAmbiguousIds.has(id))
4440
4451
  continue;
4441
4452
  DynamicFormComponent.warnedAmbiguousIds.add(id);
4442
4453
  console.warn(`[ngx-dynamic-entity] A rule references field "${id}", which is defined in ${scopes.join(' and ')}. ` +
4443
- `Rules address a field by bare id, so the rule will read whichever one the form finds first. ` +
4444
- `Give one of them a distinct id.`);
4454
+ `A bare id cannot say which one is meant, so the rule will read whichever the form finds first. ` +
4455
+ `Name it by path instead, as [${scopes[0]}.${id}].`);
4445
4456
  }
4446
4457
  }
4447
4458
  static warnedAmbiguousIds = new Set();
@@ -4643,6 +4654,13 @@ class DynamicFormComponent {
4643
4654
  getControl(fieldId, currentTabId) {
4644
4655
  if (!this.form)
4645
4656
  return null;
4657
+ // A bracketed path names exactly one control, and the form nests by tab exactly as the
4658
+ // path does — so `form.get('work.address')` walks straight to it. Anything that names a
4659
+ // field can therefore use a path: `showWhen` keys, `patchOnTrue` mappings and `autoPatch`
4660
+ // targets, not only rules.
4661
+ const parsed = parseFieldRef(fieldId);
4662
+ if (parsed.kind === 'ref')
4663
+ return this.form.get(parsed.value);
4646
4664
  if (currentTabId) {
4647
4665
  const tabGrp = this.getTabGroup(currentTabId);
4648
4666
  const ctrl = tabGrp?.get(fieldId);
@@ -5259,12 +5277,14 @@ class DynamicRecordFormComponent {
5259
5277
  const location = this.locateField(fieldId);
5260
5278
  if (!location)
5261
5279
  return;
5262
- this.dynamicFormComp?.setActiveTab(location.tabId);
5280
+ // The panel must not take the focus back: this jump is going to focus the field itself.
5281
+ this.dynamicFormComp?.setActiveTab(location.tabId, { focusPanel: false });
5263
5282
  // `setActiveTab` resets to a tab's first child, so the sub-tab has to be selected after
5264
5283
  // it, not before. Without this the walk found sub-tab fields and then rendered the wrong
5265
5284
  // panel, so the element the callback below looks for never existed.
5266
- if (location.subTabId)
5267
- this.dynamicFormComp?.setActiveSubTab(location.subTabId);
5285
+ if (location.subTabId) {
5286
+ this.dynamicFormComp?.setActiveSubTab(location.subTabId, { focusPanel: false });
5287
+ }
5268
5288
  afterNextRender(() => {
5269
5289
  // The field id comes from config, so it never goes into a selector string: no
5270
5290
  // escaping to get wrong, and no need for `CSS.escape`, which jsdom and older