ngx-dynamic-entity 1.14.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ngx-dynamic-entity",
3
- "version": "1.14.0",
3
+ "version": "2.0.1",
4
4
  "description": "Angular standalone UI library for rendering dynamic entity forms.",
5
5
  "//typings": "Local workspace resolution only: ng-packagr 21 writes types/ngx-dynamic-entity.d.ts into dist/package.json. Do NOT add a 'main' field here — ng-packagr does not generate one, so it would be copied verbatim and resolve wrongly in the published package.",
6
6
  "typings": "types/ngx-dynamic-entity.d.ts",
@@ -8,7 +8,7 @@
8
8
  "@angular/common": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0 || ^22.0.0",
9
9
  "@angular/core": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0 || ^22.0.0",
10
10
  "@angular/forms": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0 || ^22.0.0",
11
- "@dynamic-entity/core": "^1.14.0",
11
+ "@dynamic-entity/core": "^2.0.1",
12
12
  "rxjs": "^7.0.0"
13
13
  },
14
14
  "keywords": [
package/styles.css CHANGED
@@ -224,6 +224,7 @@
224
224
  flex-direction: column;
225
225
  gap: var(--ngx-field-gap);
226
226
  min-width: 0;
227
+ width: 100%;
227
228
  }
228
229
 
229
230
  .ngx-field__label {
@@ -1057,18 +1058,7 @@ textarea.ngx-field__input:focus {
1057
1058
  }
1058
1059
 
1059
1060
  /*
1060
- * A read-only record is a table of values, not a form.
1061
- *
1062
- * With every field at `span 12` the record view spent a full screen on six values — and no
1063
- * control is being sized here, so the width a text input needs is not the width a value
1064
- * needs. `auto-fit` packs them, and a long note still takes its own row when the content
1065
- * demands one.
1066
- */
1067
- /*
1068
- * Nothing is being asked of the reader, so nothing is required of them.
1069
- *
1070
- * The matching `.ngx-form--readonly` *grid* rule lives in the component's own styles, next to
1071
- * the grid it overrides — see the Layout note above.
1061
+ * In read-only mode, required markers are suppressed since no user input is expected.
1072
1062
  */
1073
1063
  .ngx-form--readonly .ngx-field__req {
1074
1064
  display: none;
@@ -1,8 +1,8 @@
1
1
  import * as i0 from '@angular/core';
2
2
  import { InjectionToken, Type, EnvironmentProviders, WritableSignal, OnInit, OnChanges, OnDestroy, EventEmitter, SimpleChanges, Provider } from '@angular/core';
3
- import { ReferenceOption, LocalizedText, EntityFormConfig, FormRule, ImportLookups, MappingPlan, ImportResult, TemplateSpec, SheetData, CommonModuleEntry, EntityReferenceLoader, LookupListSource, RecordMigration, FileUploadHandler, FieldValidators, NestedFieldConfig, ReferenceLoaderContext, DropdownOption, LookupListValue, NestedTabConfig, RuleEvaluationResult, ImportColumn, UnsupportedColumn, ImportRowError } from '@dynamic-entity/core';
3
+ import { ReferenceOption, LocalizedText, EntityFormConfig, FormRule, ImportLookups, MappingPlan, ImportResult, TemplateSpec, SheetData, CommonModuleEntry, EntityReferenceLoader, LookupListSource, RecordMigration, FileUploadHandler, FieldValidators, NestedFieldConfig, ReferenceLoaderContext, DropdownOption, LookupListValue, NestedTabConfig, RuleEvaluationResult, FieldScopeEntry, EvaluateFormRulesOptions, ImportColumn, UnsupportedColumn, ImportRowError } from '@dynamic-entity/core';
4
4
  export { AutoPatchConfig, CommonModuleEntry, ComponentClass, DropdownOption, EntityFormConfig, EntityPermissions, EntityReferenceConfig, EntityReferenceLoader, FieldTableConfig, FieldValidators, FileRef, FormRule, LocalizedText, NestedFieldConfig, NestedTabConfig, PatchOnTrueMapping, RbacContext, ReferenceLoaderContext, ReferenceOption, RichFieldType, RuleAction, RuleCondition, RuleEvaluationResult, VersionedRecord, formatDisplayValue, normalizeConfig, normalizeOption, resolveLabel, resolveOptionLabel, resolveOptionValue, valuesMatch } from '@dynamic-entity/core';
5
- import { ValidatorFn, AsyncValidatorFn, ValidationErrors, FormGroup, FormArray, AbstractControl } from '@angular/forms';
5
+ import { ValidatorFn, AsyncValidatorFn, AbstractControl, ValidationErrors, FormGroup, FormArray } from '@angular/forms';
6
6
  import * as rxjs from 'rxjs';
7
7
  import * as ngx_dynamic_entity from 'ngx-dynamic-entity';
8
8
 
@@ -618,7 +618,7 @@ declare const provideNgxDynamicEntity: (config?: NgxDynamicEntityConfig) => Envi
618
618
  *
619
619
  * `FieldRegistryService` imports no components. Whatever you register here is what gets
620
620
  * bundled, so an app that only uses text/number/dropdown pays for three components and
621
- * their imports, not nineteen.
621
+ * their imports, not twenty-one.
622
622
  *
623
623
  * ```ts
624
624
  * // Everything (convenient; bundles all built-ins):
@@ -634,7 +634,7 @@ declare const provideNgxDynamicEntity: (config?: NgxDynamicEntityConfig) => Envi
634
634
  declare function builtInFieldTypes(): Record<string, Type<any>>;
635
635
  /** Register a specific set of field types. Only the components you name are bundled. */
636
636
  declare function provideFieldTypes(types: Record<string, Type<any>>): EnvironmentProviders;
637
- /** Register all 19 built-in field type keys. Convenience for apps that use most of them. */
637
+ /** Register all 21 built-in field type keys. Convenience for apps that use most of them. */
638
638
  declare function provideBuiltInFieldTypes(): EnvironmentProviders;
639
639
 
640
640
  /**
@@ -642,7 +642,7 @@ declare function provideBuiltInFieldTypes(): EnvironmentProviders;
642
642
  *
643
643
  * **Tree-shaking contract:** this service holds *no* static imports of field components.
644
644
  * Nothing is bundled until something registers it, which is what makes the built-ins
645
- * opt-in. Register them with `provideBuiltInFieldTypes()` (all 19 keys) or a narrower
645
+ * opt-in. Register them with `provideBuiltInFieldTypes()` (all 21 keys) or a narrower
646
646
  * subset via `provideFieldTypes({ ... })` — see `providers/provide-field-types.ts`.
647
647
  *
648
648
  * Resolution order: consumer token registry → runtime registrations → nothing.
@@ -706,6 +706,22 @@ declare class ValidatorRegistryService {
706
706
  * An unknown name is skipped rather than throwing, matching the synchronous behaviour.
707
707
  */
708
708
  resolveAsyncFromConfig(config?: FieldValidators | string[]): AsyncValidatorFn[];
709
+ /**
710
+ * Say when a named validator resolved to nothing.
711
+ *
712
+ * It used to be dropped in silence, which is the worst failure mode this library has: the
713
+ * schema says the field is checked, the field is not checked, the form saves, and nothing
714
+ * anywhere said so. For `customAsync` that is typically a uniqueness check, so the duplicate
715
+ * lands in the database.
716
+ *
717
+ * Dev only, once per name — this runs while controls are built, which happens on every
718
+ * config change. `validateConfig` with `knownValidators` is the build-time version of the
719
+ * same check, and it fails CI rather than a console.
720
+ */
721
+ private warnUnresolved;
722
+ /** Per service instance, which is per injector — so a test gets a clean slate. */
723
+ private readonly warned;
724
+ private warnOnce;
709
725
  static ɵfac: i0.ɵɵFactoryDeclaration<ValidatorRegistryService, never>;
710
726
  static ɵprov: i0.ɵɵInjectableDeclaration<ValidatorRegistryService>;
711
727
  }
@@ -963,18 +979,88 @@ declare class RbacService {
963
979
  static ɵprov: i0.ɵɵInjectableDeclaration<RbacService>;
964
980
  }
965
981
 
982
+ /** What `syncHiddenFieldState` needs from the form it is operating on. */
983
+ interface RuleSyncContext {
984
+ result: RuleEvaluationResult;
985
+ /** The values `showWhen` is evaluated against — the flat map the rules engine reads. */
986
+ values: Record<string, unknown>;
987
+ /** Every name a field answers to: its bare id, and its bracketed path. */
988
+ namesOf(field: NestedFieldConfig): readonly string[];
989
+ /**
990
+ * The field's dotted address, unbracketed — `personal.address.city`.
991
+ *
992
+ * Separate from `namesOf` because it is compared as a *prefix*: a container hides its
993
+ * descendants, and `[personal.address]` is not a prefix of `[personal.address.city]` once
994
+ * the brackets are in the way.
995
+ */
996
+ addressOf(field: NestedFieldConfig): string;
997
+ /** The control a scope entry addresses, or `null` when it has no static path. */
998
+ controlFor(entry: FieldScopeEntry): AbstractControl | null;
999
+ }
966
1000
  /**
967
- * RulesEvaluationService — evaluates dynamic form rules against active reactive form state.
1001
+ * RulesEvaluationService — evaluates rules, and owns what a result *means*.
1002
+ *
1003
+ * It used to be two passthrough methods, with the interpretation spread across the component:
1004
+ * the render filter decided one way what a rule result did to a field, and the validity sync
1005
+ * decided again forty lines further down. They disagreed about what "every field" meant, and
1006
+ * the consequence was a required field inside a `group` that a rule had hidden holding
1007
+ * `form.invalid` true forever with nothing on screen to explain it.
1008
+ *
1009
+ * Precedence lives here now, in one method that both callers go through. Still stateless —
1010
+ * every method takes the result it is interpreting, so there is nothing to go stale against a
1011
+ * rebuilt form and nothing to reset between configs.
968
1012
  */
969
1013
  declare class RulesEvaluationService {
970
1014
  /**
971
1015
  * Evaluate rules against form values and optional original baseline values.
972
1016
  */
973
- evaluate(rules: FormRule[] | undefined | null, values: Record<string, unknown>, originalValues?: Record<string, unknown>): RuleEvaluationResult;
1017
+ evaluate(rules: FormRule[] | undefined | null, values: Record<string, unknown>, originalValues?: Record<string, unknown>, options?: EvaluateFormRulesOptions): RuleEvaluationResult;
974
1018
  /**
975
1019
  * Filter rules pertinent to a single tab.
976
1020
  */
977
1021
  filterForTab(rules: FormRule[] | undefined | null, tabId: string, config: EntityFormConfig): FormRule[];
1022
+ /**
1023
+ * Whether a field renders, under the precedence documented on `RuleEvaluationResult`.
1024
+ *
1025
+ * An explicit hide beats everything. A show beats static hiding — `visibility: false`, or a
1026
+ * `showWhen` that does not currently hold — because a rule that could only hide what the
1027
+ * config already hides would have nothing to say, and `{ visibility: true }` was a
1028
+ * documented no-op until it did. Otherwise the config's own visibility decides.
1029
+ */
1030
+ isFieldVisible(result: RuleEvaluationResult, field: NestedFieldConfig, names: readonly string[], values: Record<string, unknown>): boolean;
1031
+ /** The same precedence for a tab. */
1032
+ isTabVisible(result: RuleEvaluationResult, tab: NestedTabConfig): boolean;
1033
+ /**
1034
+ * Keep a hidden field's control out of form validity.
1035
+ *
1036
+ * A field hidden by a rule or a `showWhen` is filtered out of the render, but its
1037
+ * validators stay attached — so a hidden *required* field holds `form.invalid` true
1038
+ * forever, permanently disabling Save with nothing on screen to explain why.
1039
+ *
1040
+ * Disabling is the fix rather than stripping validators: Angular excludes disabled controls
1041
+ * from validity, and the field's own validators survive intact for when it comes back.
1042
+ * Values are untouched — a disabled control still carries its value, so rule evaluation and
1043
+ * the emitted record see exactly what they saw before. That also keeps this from feeding
1044
+ * back on itself: hiding a field cannot change the values the rules are evaluated over.
1045
+ *
1046
+ * The predicate is `isFieldVisible`, the same one the render filter uses, so what is on
1047
+ * screen and what counts toward validity cannot drift apart.
1048
+ *
1049
+ * Entries must arrive parent-first, which is what `collectFieldScopes` yields: a container's
1050
+ * children are skipped once the container itself is hidden, because Angular's `disable()`
1051
+ * cascades to descendants and a child's `enable()` walks back up recalculating ancestors.
1052
+ * Toggling a child inside a hidden group would fight its parent.
1053
+ */
1054
+ syncHiddenFieldState(entries: readonly FieldScopeEntry[], ctx: RuleSyncContext): void;
1055
+ /**
1056
+ * The hidden and shown names as sets, memoised against the result they came from.
1057
+ *
1058
+ * `isFieldVisible` runs once per field per change-detection pass; building two sets from
1059
+ * two arrays each time turns a rule result into allocation proportional to the form. A
1060
+ * `WeakMap` keyed by the result means it is built once per evaluation and collected with it.
1061
+ */
1062
+ private nameSets;
1063
+ private readonly nameSetCache;
978
1064
  static ɵfac: i0.ɵɵFactoryDeclaration<RulesEvaluationService, never>;
979
1065
  static ɵprov: i0.ɵɵInjectableDeclaration<RulesEvaluationService>;
980
1066
  }
@@ -1146,6 +1232,8 @@ declare class DynamicFormComponent implements OnInit, OnChanges, OnDestroy {
1146
1232
  private readonly hookRegistry;
1147
1233
  private readonly rbacService;
1148
1234
  private readonly rulesEvaluation;
1235
+ /** config ⇄ FormGroup: where a value lives. See `FormStructureService`. */
1236
+ private readonly structure;
1149
1237
  /** Resolves what is wrong with a field, for the error summary — see `resolveForField`. */
1150
1238
  private readonly messages;
1151
1239
  private readonly entityRefSelection;
@@ -1176,6 +1264,9 @@ declare class DynamicFormComponent implements OnInit, OnChanges, OnDestroy {
1176
1264
  private previousValues;
1177
1265
  handleKeyboardEvent(event: KeyboardEvent): void;
1178
1266
  readonly ruleResult: i0.Signal<ngx_dynamic_entity.RuleEvaluationResult>;
1267
+ /** Warned-about rule problems, per component instance — see `warnedAmbiguousIds`. */
1268
+ private readonly warnedProblems;
1269
+ private warnOnce;
1179
1270
  /**
1180
1271
  * Critical fields whose value differs from the session baseline.
1181
1272
  * These drive the deferred `VALUE_CHANGED` banner — a critical edit is announced once,
@@ -1184,6 +1275,7 @@ declare class DynamicFormComponent implements OnInit, OnChanges, OnDestroy {
1184
1275
  readonly changedCriticalFields: i0.Signal<NestedFieldConfig[]>;
1185
1276
  get tabs(): NestedTabConfig[];
1186
1277
  get visibleTabs(): NestedTabConfig[];
1278
+ private isTabVisible;
1187
1279
  get activeTabConfig(): NestedTabConfig | null;
1188
1280
  get visibleSubTabs(): NestedTabConfig[];
1189
1281
  get activeSubTabConfig(): NestedTabConfig | null;
@@ -1203,6 +1295,12 @@ declare class DynamicFormComponent implements OnInit, OnChanges, OnDestroy {
1203
1295
  private readonly warnedSelectorModules;
1204
1296
  private warnSelectorModule;
1205
1297
  get fieldsForActiveTab(): NestedFieldConfig[];
1298
+ /**
1299
+ * Whether a field renders. Precedence lives in `RulesEvaluationService`, which is also
1300
+ * what `syncHiddenFieldState` asks — so what is on screen and what counts toward validity
1301
+ * cannot drift apart.
1302
+ */
1303
+ private isFieldVisible;
1206
1304
  /**
1207
1305
  * Entity-level permissions for the current roles.
1208
1306
  *
@@ -1344,6 +1442,14 @@ declare class DynamicFormComponent implements OnInit, OnChanges, OnDestroy {
1344
1442
  * library never reaches for the global `document`.
1345
1443
  */
1346
1444
  jumpToField(fieldId: string): void;
1445
+ /**
1446
+ * Which tab (and sub-tab) holds a field.
1447
+ *
1448
+ * Descends into `group`/`array` children: this compared top-level fields only, so "jump to
1449
+ * the first invalid field" found nothing for a nested one and silently did nothing. Only
1450
+ * sub-tabs one level deep are named, because that is what the tab strip can select —
1451
+ * anything deeper resolves to the ancestor it renders under.
1452
+ */
1347
1453
  private locateField;
1348
1454
  /** A critical field stays read-only until the user deliberately unlocks it. */
1349
1455
  isFieldLocked(field: NestedFieldConfig): boolean;
@@ -1370,8 +1476,6 @@ declare class DynamicFormComponent implements OnInit, OnChanges, OnDestroy {
1370
1476
  */
1371
1477
  changedCriticalFieldLabels(): string;
1372
1478
  private buildForm;
1373
- private buildFieldControl;
1374
- /** One row of an `array` field: a FormGroup when the field declares columns, else a bare control. */
1375
1479
  /**
1376
1480
  * The `FormArray` behind an `array` field, for callers that manage rows themselves —
1377
1481
  * the record editor's inline row drawer.
@@ -1408,9 +1512,8 @@ declare class DynamicFormComponent implements OnInit, OnChanges, OnDestroy {
1408
1512
  * stay empty — with no error and no clue. That silent miss is the most expensive way to
1409
1513
  * lose an hour with this library, so name it in dev builds.
1410
1514
  *
1411
- * Only top-level keys that match a known field id are reported: anything else is assumed
1412
- * to be the consumer's own record metadata (ids, timestamps, `_configVersion`) and is not
1413
- * our business.
1515
+ * Which keys were missed is `FormStructureService`'s answer; warning once per key per form
1516
+ * is this component's, because it is the thing that lives as long as the form does.
1414
1517
  */
1415
1518
  private warnUnconsumedInitialData;
1416
1519
  /** Warn once per key per form instance — patchForm re-runs on every initialData change. */
@@ -1429,7 +1532,16 @@ declare class DynamicFormComponent implements OnInit, OnChanges, OnDestroy {
1429
1532
  * consumers that never call the validator still get a signal in development.
1430
1533
  */
1431
1534
  private warnAmbiguousRuleReferences;
1432
- private static readonly warnedAmbiguousIds;
1535
+ /**
1536
+ * Ids already warned about, per component instance.
1537
+ *
1538
+ * This was `private static`, so it was module-global mutable state that nothing ever
1539
+ * cleared: under SSR every render of every form in the process accumulated into one set
1540
+ * that lived as long as the server did, and the first request to render a config
1541
+ * suppressed the warning for every request after it. Per instance, the warning is once per
1542
+ * form — which is what "warn once" was meant to mean.
1543
+ */
1544
+ private readonly warnedAmbiguousIds;
1433
1545
  /**
1434
1546
  * The value map rules and `showWhen` are evaluated against.
1435
1547
  *
@@ -1443,17 +1555,53 @@ declare class DynamicFormComponent implements OnInit, OnChanges, OnDestroy {
1443
1555
  * unknown>` and needs no knowledge of refs at all — the extra keys simply resolve.
1444
1556
  */
1445
1557
  /**
1446
- * Whether a rule result names this field.
1558
+ * Every name a rule may address this field by.
1447
1559
  *
1448
1560
  * A rule target is either a bare field id — how every config so far addresses a field — or
1449
- * a bracketed path, which is the only way to name one of two fields that share an id.
1450
- * Both have to match, or a rule written either way would fail to hide what it targeted.
1561
+ * a bracketed path, which is the only way to name one of two fields that share an id. Both
1562
+ * have to match, or a rule written either way would fail to hide what it targeted.
1563
+ *
1564
+ * The path comes last, and `syncHiddenFieldState` relies on that: it uses the last entry
1565
+ * as the field's address when deciding whether a hidden container already covers it.
1451
1566
  */
1452
- private namesField;
1567
+ private namesOf;
1568
+ /**
1569
+ * The field's dotted address, unbracketed.
1570
+ *
1571
+ * The positional path rather than an authored `refererField`: this is compared as a prefix
1572
+ * to decide whether a hidden container already covers a field, and containment is a fact
1573
+ * about where the field *is*. A `refererField` is a binding override — it says where the
1574
+ * value goes, not which group renders it.
1575
+ */
1576
+ private addressOf;
1453
1577
  /** The field's declared path, or the one its position implies when it carries none. */
1454
1578
  private pathFor;
1579
+ /**
1580
+ * Every field in the config with the scope its value is stored under, cached per config.
1581
+ *
1582
+ * `collectFieldScopes` walks the whole tree. It was called once per field from `pathFor`,
1583
+ * which `namesField` calls, which the render filter calls for every field on the active
1584
+ * tab — a quadratic walk on every change-detection pass. Keyed by config identity, so a
1585
+ * new config invalidates it without an explicit lifecycle hook to forget.
1586
+ */
1587
+ private fieldScopes;
1588
+ private scopeEntryFor;
1589
+ private scopeCache?;
1590
+ /**
1591
+ * The control a scope entry addresses, named by path rather than by bare id.
1592
+ *
1593
+ * `getControl(field.id)` with no tab falls through to a first-match recursive search of the
1594
+ * whole form, so with `address` on two tabs, hiding one disabled the other. The control
1595
+ * tree mirrors the scope path exactly — `buildTabControls` nests by tab id and
1596
+ * `buildFieldControl` nests a `group` under its own id — so a `[path]` ref resolves through
1597
+ * `form.get()` on `getControl`'s first branch and names exactly one control.
1598
+ *
1599
+ * A field inside an `array` has no static path: its controls live in `FormArray` rows built
1600
+ * per row. `form.get()` returns null for those and they are left alone, which is correct —
1601
+ * a row's controls are created and destroyed with the row.
1602
+ */
1603
+ private controlForEntry;
1455
1604
  private flattenFormValues;
1456
- private getTabGroup;
1457
1605
  /**
1458
1606
  * `autoPatch` — copy mapped fields out of the record the user just selected in an
1459
1607
  * entity-ref field into the configured target tab's controls.
@@ -1464,37 +1612,44 @@ declare class DynamicFormComponent implements OnInit, OnChanges, OnDestroy {
1464
1612
  * copy `from` → `to` within the current record.
1465
1613
  */
1466
1614
  private runPatchOnTrue;
1467
- /** Prefer a control declared on the configured target tab; fall back to a top-level control. */
1468
- private resolveTargetControl;
1469
1615
  /**
1470
- * The fields the render filter operates on — a tab's own fields, at every tab depth.
1471
- * Deliberately not `allFields()`: `group` and `array` children are shown or hidden with
1472
- * their parent, and toggling a child's state independently would fight the parent's.
1616
+ * Prefer a control declared on the configured target tab; fall back to a top-level control.
1617
+ *
1618
+ * "Declared on the tab" now means anywhere under it, sub-tabs and `group` children
1619
+ * included. It meant the tab's own top-level fields only, so an `autoPatch` mapping onto a
1620
+ * field inside a group resolved to `null` and copied nothing, silently.
1473
1621
  */
1474
- private tabFields;
1622
+ private resolveTargetControl;
1475
1623
  /**
1476
- * Keep a hidden field's control out of form validity.
1477
- *
1478
- * A field hidden by a rule or a `showWhen` condition is filtered out of the render, but
1479
- * its validators stay attached — so a hidden *required* field holds `form.invalid` true
1480
- * forever, permanently disabling Save with nothing on screen to explain why.
1481
- *
1482
- * Disabling is the fix rather than stripping validators: Angular excludes disabled
1483
- * controls from validity, and the field's own validators survive intact for when it
1484
- * comes back. Values are untouched — `flattenFormValues` and `extractRecord` both read
1485
- * `control.value`, which a disabled control still carries, so rule evaluation and the
1486
- * emitted record see exactly what they saw before. That also keeps this from feeding
1487
- * back on itself: hiding a field cannot change the values the rules are evaluated over.
1624
+ * Keep a hidden field's control out of form validity — see
1625
+ * `RulesEvaluationService.syncHiddenFieldState`, which owns the rule and the reasoning.
1488
1626
  *
1489
- * The visibility predicate is the same one `fieldsForActiveTab` filters with, so what is
1490
- * rendered and what counts toward validity cannot drift apart.
1627
+ * This supplies the three things it cannot know: which fields exist and in what order
1628
+ * (`collectFieldScopes`, parent-first), what each one is called, and which control each
1629
+ * one addresses.
1491
1630
  */
1492
1631
  private syncHiddenFieldState;
1493
1632
  /** Every field in the config, including `group`/`array` children. */
1494
1633
  private allFields;
1495
1634
  /** An empty `originalData` counts as "not supplied" — fall back to the captured baseline. */
1496
1635
  private baseline;
1636
+ /**
1637
+ * Whether a critical field still holds what it held when the session started.
1638
+ *
1639
+ * The same question `VALUE_CHANGED` asks, so it is answered by the same comparator rather
1640
+ * than by a third one. The local version compared objects through `JSON.stringify`, which
1641
+ * is key-order sensitive — two serialisers writing the same option in a different key
1642
+ * order made the field read as edited — and had its own idea of which values count as
1643
+ * empty. `valuesEqual` is order-independent and treats `null` and `undefined` as the same
1644
+ * absence; the empty-string case is kept here, because a cleared text input is not an edit.
1645
+ */
1497
1646
  private sameValue;
1647
+ /**
1648
+ * Find a control by a bracketed path or a bare field id.
1649
+ *
1650
+ * Public API, so it stays on the component and delegates — see `FormStructureService`,
1651
+ * which owns the resolution rule and is where the behaviour is documented.
1652
+ */
1498
1653
  getControl(fieldId: string, currentTabId?: string): AbstractControl | null;
1499
1654
  /**
1500
1655
  * Move focus into the newly shown panel.
@@ -2300,7 +2455,7 @@ declare function fieldDomId(field: {
2300
2455
  * is read first. `null` rather than an empty string when there is nothing to describe — an
2301
2456
  * empty `aria-describedby` is a dangling reference, not an absent one.
2302
2457
  *
2303
- * Shared rather than written into each of the nineteen field components, because the rule is
2458
+ * Shared rather than written into each of the twenty-one field components, because the rule is
2304
2459
  * the same everywhere and getting it wrong is invisible until somebody uses a screen reader.
2305
2460
  */
2306
2461
  declare function fieldDescribedBy(domId: (suffix?: string) => string, parts: {
@@ -2431,7 +2586,14 @@ declare class MarkdownFieldComponent implements OnChanges {
2431
2586
  * A renderer is consumer code and may throw on input it does not like. Letting that
2432
2587
  * escape would take down the whole form over one malformed field, so it falls back to the
2433
2588
  * source text — which is what the field would have shown with no renderer at all.
2589
+ *
2590
+ * Memoised on the source, because this is called from the template: without it the
2591
+ * consumer's markdown parser ran on every change-detection pass, for every markdown field
2592
+ * on screen, over the whole document — which on a form holding a few long ones is a parse
2593
+ * per keystroke *elsewhere* in the form. The cache is keyed by the exact source string,
2594
+ * so a value that comes back to something already rendered is free too.
2434
2595
  */
2596
+ private cached?;
2435
2597
  protected rendered(): string | null;
2436
2598
  /**
2437
2599
  * Resolved through `ValidationMessagesService`, so `provideNgxDynamicEntity({
@@ -2867,11 +3029,20 @@ declare class DropdownFieldComponent {
2867
3029
  readonly: boolean;
2868
3030
  masked: boolean;
2869
3031
  /** Inline `options`, or the field's named list resolved through the registry (§6.3). */
2870
- readonly options: i0.WritableSignal<ngx_dynamic_entity.LocalizedText[]>;
3032
+ readonly options: i0.WritableSignal<DropdownOption[]>;
2871
3033
  readonly compareFn: (o1: unknown, o2: unknown) => boolean;
2872
3034
  isObjectVal(option: DropdownOption): boolean;
2873
3035
  get label(): string;
2874
3036
  get placeholder(): string;
3037
+ /**
3038
+ * The value this option stores — the option object itself.
3039
+ *
3040
+ * Kept as a method rather than binding `option` straight into the template, because it is
3041
+ * the one place that says *why* the two are the same thing. It used to delegate to
3042
+ * `getOptionStoredValue`, an exported identity function whose two branches returned their
3043
+ * argument unchanged; naming a transformation that does not happen is worse than naming
3044
+ * nothing, because a reader has to go and check.
3045
+ */
2875
3046
  getOptStoredVal(option: DropdownOption): unknown;
2876
3047
  getOptLabel(option: DropdownOption): string;
2877
3048
  /**
@@ -2890,7 +3061,7 @@ declare class DropdownFieldComponent {
2890
3061
  }
2891
3062
 
2892
3063
  /** Radio field: a group of radio buttons built from field.options. */
2893
- declare class RadioFieldComponent {
3064
+ declare class RadioFieldComponent implements OnDestroy {
2894
3065
  /**
2895
3066
  * Unique to this component instance: an `array` renders the same field once per row, and a
2896
3067
  * DOM id may not repeat. See `field-dom-id.ts`.
@@ -2901,19 +3072,34 @@ declare class RadioFieldComponent {
2901
3072
  protected readonly maskedText: string;
2902
3073
  private readonly messages;
2903
3074
  private readonly lookups;
3075
+ private readonly cdr;
2904
3076
  private _field;
2905
3077
  private _language;
3078
+ private _control;
3079
+ private _controlSub?;
3080
+ ngOnDestroy(): void;
2906
3081
  /** Setter-based so options resolve however the input is set — see `refreshChoiceOptions`. */
2907
3082
  set field(value: NestedFieldConfig);
2908
3083
  get field(): NestedFieldConfig;
2909
3084
  set language(value: string);
2910
3085
  get language(): string;
2911
- control: AbstractControl;
3086
+ set control(value: AbstractControl);
3087
+ get control(): AbstractControl;
2912
3088
  readonly: boolean;
2913
3089
  masked: boolean;
2914
3090
  /** Inline `options`, or the field's named list resolved through the registry (§6.3). */
2915
- readonly options: i0.WritableSignal<ngx_dynamic_entity.LocalizedText[]>;
3091
+ readonly options: i0.WritableSignal<DropdownOption[]>;
3092
+ onSelect(option: DropdownOption): void;
2916
3093
  get label(): string;
3094
+ /**
3095
+ * The value this option stores — the option object itself.
3096
+ *
3097
+ * Kept as a method rather than binding `option` straight into the template, because it is
3098
+ * the one place that says *why* the two are the same thing. It used to delegate to
3099
+ * `getOptionStoredValue`, an exported identity function whose two branches returned their
3100
+ * argument unchanged; naming a transformation that does not happen is worse than naming
3101
+ * nothing, because a reader has to go and check.
3102
+ */
2917
3103
  getOptStoredVal(option: DropdownOption): unknown;
2918
3104
  /**
2919
3105
  * Stable DOM id for one radio, so the <label for> association works.
@@ -2931,6 +3117,8 @@ declare class RadioFieldComponent {
2931
3117
  optionSlug(option: DropdownOption): string;
2932
3118
  getOptVal(option: DropdownOption): string | number | boolean;
2933
3119
  getOptLabel(option: DropdownOption): string;
3120
+ /** Whether the given option matches the control's current value. */
3121
+ isChecked(option: DropdownOption): boolean;
2934
3122
  /** Read-only display — synchronous, per §6.2. See `DropdownFieldComponent.getLabel`. */
2935
3123
  getSelectedLabel(): string;
2936
3124
  /**
@@ -2970,10 +3158,19 @@ declare class MultiSelectFieldComponent {
2970
3158
  readonly: boolean;
2971
3159
  masked: boolean;
2972
3160
  /** Inline `options`, or the field's named list resolved through the registry (§6.3). */
2973
- readonly options: i0.WritableSignal<ngx_dynamic_entity.LocalizedText[]>;
3161
+ readonly options: i0.WritableSignal<DropdownOption[]>;
2974
3162
  readonly compareFn: (o1: unknown, o2: unknown) => boolean;
2975
3163
  isObjectVal(option: DropdownOption): boolean;
2976
3164
  get label(): string;
3165
+ /**
3166
+ * The value this option stores — the option object itself.
3167
+ *
3168
+ * Kept as a method rather than binding `option` straight into the template, because it is
3169
+ * the one place that says *why* the two are the same thing. It used to delegate to
3170
+ * `getOptionStoredValue`, an exported identity function whose two branches returned their
3171
+ * argument unchanged; naming a transformation that does not happen is worse than naming
3172
+ * nothing, because a reader has to go and check.
3173
+ */
2977
3174
  getOptStoredVal(option: DropdownOption): unknown;
2978
3175
  getOptLabel(option: DropdownOption): string;
2979
3176
  /** Read-only display — synchronous, per §6.2. See `DropdownFieldComponent.getLabel`. */
@@ -3197,4 +3394,4 @@ declare class FileFieldComponent {
3197
3394
  }
3198
3395
 
3199
3396
  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 };
3200
- export type { ChoiceOptionHost, ConfigSourceHandler, DynamicFieldComponentContract, EntityRefCacheStore, EntityRefSelection, HookFn, HttpImportTransportOptions, ImportContext, ImportPreview, ImportTransport, InvalidField, MessageOrderEntry, NgxDynamicEntityConfig, SheetParser, TemplateFormat, UiTextKey, UiTextOverrides, UiTextOverridesFor, UiTextParams, UiTextResolver, UiTextResolverFor, UiTextValue, ValidationMessage };
3397
+ export type { ChoiceOptionHost, ConfigSourceHandler, DynamicFieldComponentContract, EntityRefCacheStore, EntityRefSelection, HookFn, HttpImportTransportOptions, ImportContext, ImportPreview, ImportTransport, InvalidField, MessageOrderEntry, NgxDynamicEntityConfig, RuleSyncContext, SheetParser, TemplateFormat, UiTextKey, UiTextOverrides, UiTextOverridesFor, UiTextParams, UiTextResolver, UiTextResolverFor, UiTextValue, ValidationMessage };