ngx-dynamic-entity 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +129 -139
  2. package/esm2022/lib/field-types/array-field.component.mjs +112 -104
  3. package/esm2022/lib/field-types/boolean-field.component.mjs +62 -61
  4. package/esm2022/lib/field-types/checkbox-field.component.mjs +48 -47
  5. package/esm2022/lib/field-types/currency-field.component.mjs +54 -53
  6. package/esm2022/lib/field-types/date-field.component.mjs +46 -45
  7. package/esm2022/lib/field-types/dropdown-field.component.mjs +89 -87
  8. package/esm2022/lib/field-types/email-field.component.mjs +64 -63
  9. package/esm2022/lib/field-types/entity-ref-field.component.mjs +85 -71
  10. package/esm2022/lib/field-types/field-component.contract.mjs +2 -0
  11. package/esm2022/lib/field-types/file-field.component.mjs +88 -87
  12. package/esm2022/lib/field-types/group-field.component.mjs +50 -50
  13. package/esm2022/lib/field-types/image-field.component.mjs +92 -91
  14. package/esm2022/lib/field-types/month-year-field.component.mjs +80 -79
  15. package/esm2022/lib/field-types/multi-select-field.component.mjs +66 -65
  16. package/esm2022/lib/field-types/number-field.component.mjs +71 -75
  17. package/esm2022/lib/field-types/password-field.component.mjs +68 -67
  18. package/esm2022/lib/field-types/radio-field.component.mjs +66 -65
  19. package/esm2022/lib/field-types/text-field.component.mjs +81 -74
  20. package/esm2022/lib/field-types/textarea-field.component.mjs +48 -47
  21. package/esm2022/lib/form/dynamic-field/dynamic-field.component.mjs +53 -6
  22. package/esm2022/lib/form/dynamic-form.component.mjs +309 -20
  23. package/esm2022/lib/form/dynamic-record-form.component.mjs +36 -11
  24. package/esm2022/lib/providers/provide-field-types.mjs +1 -1
  25. package/esm2022/lib/providers/provide-ngx-dynamic-entity.mjs +8 -2
  26. package/esm2022/lib/services/cascade-data.service.mjs +2 -2
  27. package/esm2022/lib/services/entity-ref-cache.mjs +1 -1
  28. package/esm2022/lib/services/entity-ref-registry.service.mjs +2 -2
  29. package/esm2022/lib/services/entity-ref-selection.service.mjs +1 -1
  30. package/esm2022/lib/services/entity-reference.service.mjs +1 -1
  31. package/esm2022/lib/services/field-registry.service.mjs +1 -1
  32. package/esm2022/lib/services/file-upload.service.mjs +1 -1
  33. package/esm2022/lib/services/lookup-registry.service.mjs +2 -2
  34. package/esm2022/lib/services/rbac.service.mjs +1 -1
  35. package/esm2022/lib/services/validation-messages.service.mjs +56 -0
  36. package/esm2022/lib/services/validator-registry.service.mjs +35 -2
  37. package/esm2022/lib/tokens/injection-tokens.mjs +35 -6
  38. package/esm2022/public-api.mjs +23 -2
  39. package/fesm2022/ngx-dynamic-entity.mjs +1754 -1236
  40. package/fesm2022/ngx-dynamic-entity.mjs.map +1 -1
  41. package/lib/field-types/array-field.component.d.ts +7 -1
  42. package/lib/field-types/dropdown-field.component.d.ts +2 -1
  43. package/lib/field-types/entity-ref-field.component.d.ts +12 -2
  44. package/lib/field-types/field-component.contract.d.ts +49 -0
  45. package/lib/field-types/group-field.component.d.ts +1 -1
  46. package/lib/field-types/multi-select-field.component.d.ts +1 -1
  47. package/lib/field-types/number-field.component.d.ts +1 -0
  48. package/lib/field-types/radio-field.component.d.ts +1 -1
  49. package/lib/field-types/text-field.component.d.ts +10 -2
  50. package/lib/form/dynamic-field/dynamic-field.component.d.ts +39 -4
  51. package/lib/form/dynamic-form.component.d.ts +155 -8
  52. package/lib/form/dynamic-record-form.component.d.ts +13 -2
  53. package/lib/providers/provide-ngx-dynamic-entity.d.ts +25 -3
  54. package/lib/services/cascade-data.service.d.ts +1 -1
  55. package/lib/services/entity-ref-registry.service.d.ts +1 -1
  56. package/lib/services/lookup-registry.service.d.ts +1 -1
  57. package/lib/services/validation-messages.service.d.ts +43 -0
  58. package/lib/services/validator-registry.service.d.ts +13 -1
  59. package/lib/tokens/injection-tokens.d.ts +42 -7
  60. package/package.json +17 -18
  61. package/public-api.d.ts +12 -0
  62. package/styles.css +579 -0
@@ -1,8 +1,14 @@
1
1
  import { AbstractControl, FormArray } from '@angular/forms';
2
2
  import type { NestedFieldConfig, EntityFormConfig } from '@dynamic-entity/core';
3
3
  import * as i0 from "@angular/core";
4
- /** ADR-008: ArrayFieldComponent — renders repeating rows for FormArray with + Add Item and Remove */
4
+ /** Renders repeating rows for a FormArray, with Add Item and Remove. */
5
5
  export declare class ArrayFieldComponent {
6
+ /**
7
+ * addItem/removeItem are public and may be called from outside this component's template,
8
+ * which under OnPush would not re-render. The host also watches the control, but this
9
+ * keeps the component correct on its own.
10
+ */
11
+ private readonly cdr;
6
12
  field: NestedFieldConfig;
7
13
  control: AbstractControl;
8
14
  config: EntityFormConfig;
@@ -3,6 +3,7 @@ import type { NestedFieldConfig } from '@dynamic-entity/core';
3
3
  import * as i0 from "@angular/core";
4
4
  export declare class DropdownFieldComponent {
5
5
  private readonly lookups;
6
+ private readonly messages;
6
7
  private _field;
7
8
  private _language;
8
9
  /** Setter-based so options resolve however the input is set — see `refreshChoiceOptions`. */
@@ -14,7 +15,7 @@ export declare class DropdownFieldComponent {
14
15
  readonly: boolean;
15
16
  masked: boolean;
16
17
  /** Inline `options`, or the field's named list resolved through the registry (§6.3). */
17
- readonly options: import("@angular/core").WritableSignal<import("@dynamic-entity/core").LocalizedText[]>;
18
+ readonly options: import("@angular/core").WritableSignal<DropdownOption[]>;
18
19
  readonly compareFn: (o1: any, o2: any) => boolean;
19
20
  isObjectVal(option: any): boolean;
20
21
  get label(): string;
@@ -1,6 +1,6 @@
1
1
  import { OnInit } from '@angular/core';
2
2
  import { AbstractControl } from '@angular/forms';
3
- import type { NestedFieldConfig, ReferenceOption } from '@dynamic-entity/core';
3
+ import type { NestedFieldConfig } from '@dynamic-entity/core';
4
4
  import * as i0 from "@angular/core";
5
5
  /**
6
6
  * EntityRefFieldComponent — a select populated from a consumer-registered loader.
@@ -29,7 +29,17 @@ export declare class EntityRefFieldComponent implements OnInit {
29
29
  ngOnInit(): void;
30
30
  /** Reload options against the parent's current value. */
31
31
  reload(): Promise<void>;
32
- onSelectionChange(): void;
32
+ /**
33
+ * Publish the selected option so autoPatch can copy from its record.
34
+ *
35
+ * Driven from `valueChanges` rather than the select's `change` event: that event is the
36
+ * same one the `formControl` directive listens to, so reading the control inside a
37
+ * template `(change)` handler can see the previous value. `valueChanges` fires after the
38
+ * model has updated — including Playwright `selectOption`, which updates the control
39
+ * through the value accessor.
40
+ */
41
+ onSelectionChange(value?: string): void;
42
+ private publishSelection;
33
43
  getLabel(value: any): string;
34
44
  private parentValue;
35
45
  /** Reload (and drop a stale selection) whenever the parent control changes. */
@@ -0,0 +1,49 @@
1
+ import type { AbstractControl } from '@angular/forms';
2
+ import type { NestedFieldConfig } from '@dynamic-entity/core';
3
+ /**
4
+ * What a field component must accept to be registered with `provideFieldTypes`.
5
+ *
6
+ * The renderer instantiates a field component dynamically and assigns exactly these five
7
+ * inputs — no more, no less — so any component satisfying this interface can stand in for a
8
+ * built-in type or add a new one.
9
+ *
10
+ * Implement it explicitly and TypeScript will tell you when you have missed one:
11
+ *
12
+ * ```typescript
13
+ * @Component({
14
+ * selector: 'app-signature-field',
15
+ * standalone: true,
16
+ * template: `<canvas [attr.aria-label]="field.id"></canvas>`,
17
+ * })
18
+ * export class SignatureFieldComponent implements DynamicFieldComponentContract {
19
+ * @Input() field!: NestedFieldConfig;
20
+ * @Input() control!: AbstractControl;
21
+ * @Input() language = 'en';
22
+ * @Input() readonly = false;
23
+ * @Input() masked = false;
24
+ * }
25
+ *
26
+ * provideFieldTypes({ signature: SignatureFieldComponent });
27
+ * ```
28
+ *
29
+ * Two implementation notes that are easy to get wrong:
30
+ *
31
+ * - Declare the inputs with definite assignment (`field!: …`) or an initialiser. The
32
+ * renderer sets them through `ComponentRef.setInput()` after construction, so they are
33
+ * genuinely undefined until the first assignment.
34
+ * - `masked` means the value must not be displayed. Render a placeholder rather than the
35
+ * control's value. This is presentational: the real value stays in the control and is
36
+ * included in the submitted record, so it is not an access-control boundary.
37
+ */
38
+ export interface DynamicFieldComponentContract {
39
+ /** The field's schema entry: id, type, label, validators, options, and so on. */
40
+ field: NestedFieldConfig;
41
+ /** The reactive control backing this field. A `FormGroup` for `group`, `FormArray` for `array`. */
42
+ control: AbstractControl;
43
+ /** Active language, for resolving `LocalizedText` labels, placeholders and options. */
44
+ language: string;
45
+ /** Render the value as text rather than an editable control. */
46
+ readonly: boolean;
47
+ /** Hide the value from display. See the note above — presentational only. */
48
+ masked: boolean;
49
+ }
@@ -1,7 +1,7 @@
1
1
  import { AbstractControl } from '@angular/forms';
2
2
  import type { NestedFieldConfig, EntityFormConfig } from '@dynamic-entity/core';
3
3
  import * as i0 from "@angular/core";
4
- /** ADR-008: GroupFieldComponent — renders nested fieldset container for group.children */
4
+ /** Renders a nested fieldset container for a group field's children. */
5
5
  export declare class GroupFieldComponent {
6
6
  field: NestedFieldConfig;
7
7
  control: AbstractControl;
@@ -14,7 +14,7 @@ export declare class MultiSelectFieldComponent {
14
14
  readonly: boolean;
15
15
  masked: boolean;
16
16
  /** Inline `options`, or the field's named list resolved through the registry (§6.3). */
17
- readonly options: import("@angular/core").WritableSignal<import("@dynamic-entity/core").LocalizedText[]>;
17
+ readonly options: import("@angular/core").WritableSignal<DropdownOption[]>;
18
18
  readonly compareFn: (o1: any, o2: any) => boolean;
19
19
  isObjectVal(option: any): boolean;
20
20
  get label(): string;
@@ -2,6 +2,7 @@ import { AbstractControl } from '@angular/forms';
2
2
  import type { NestedFieldConfig } from '@dynamic-entity/core';
3
3
  import * as i0 from "@angular/core";
4
4
  export declare class NumberFieldComponent {
5
+ private readonly messages;
5
6
  field: NestedFieldConfig;
6
7
  control: AbstractControl;
7
8
  language: string;
@@ -15,7 +15,7 @@ export declare class RadioFieldComponent {
15
15
  readonly: boolean;
16
16
  masked: boolean;
17
17
  /** Inline `options`, or the field's named list resolved through the registry (§6.3). */
18
- readonly options: import("@angular/core").WritableSignal<import("@dynamic-entity/core").LocalizedText[]>;
18
+ readonly options: import("@angular/core").WritableSignal<DropdownOption[]>;
19
19
  get label(): string;
20
20
  getOptStoredVal(option: any): any;
21
21
  /**
@@ -1,12 +1,20 @@
1
+ import { OnDestroy } from '@angular/core';
1
2
  import { AbstractControl } from '@angular/forms';
2
3
  import type { NestedFieldConfig } from '@dynamic-entity/core';
3
4
  import * as i0 from "@angular/core";
4
- export declare class TextFieldComponent {
5
+ export declare class TextFieldComponent implements OnDestroy {
6
+ private readonly messages;
7
+ private displaySub?;
5
8
  field: NestedFieldConfig;
6
- control: AbstractControl;
9
+ set control(value: AbstractControl);
10
+ get control(): AbstractControl;
11
+ private _control;
7
12
  language: string;
8
13
  readonly: boolean;
9
14
  masked: boolean;
15
+ /** Readonly markup reads this signal so an external patch (autoPatch) is visible under OnPush. */
16
+ readonly displayValue: import("@angular/core").WritableSignal<unknown>;
17
+ ngOnDestroy(): void;
10
18
  get label(): string;
11
19
  get placeholder(): string;
12
20
  get errorMessage(): string;
@@ -1,16 +1,17 @@
1
- import { OnChanges, SimpleChanges } from '@angular/core';
1
+ import { OnChanges, OnDestroy, SimpleChanges } from '@angular/core';
2
2
  import { AbstractControl } from '@angular/forms';
3
3
  import type { NestedFieldConfig, EntityFormConfig } from '@dynamic-entity/core';
4
4
  import * as i0 from "@angular/core";
5
5
  /**
6
6
  * DynamicFieldComponent — mounts the correct field component for a given NestedFieldConfig.
7
- * Uses ViewContainerRef.createComponent() and passes inputs via setInput() (ADR-008).
7
+ * Uses ViewContainerRef.createComponent() and passes inputs via setInput(); see
8
+ * DynamicFieldComponentContract for the five inputs every field component receives.
8
9
  * Never add field-type-specific logic here — this component must remain generic.
9
10
  *
10
11
  * The 5 inputs (field, control, language, readonly, masked) are passed to every mounted
11
12
  * component uniformly — no special-casing per type.
12
13
  */
13
- export declare class DynamicFieldComponent implements OnChanges {
14
+ export declare class DynamicFieldComponent implements OnChanges, OnDestroy {
14
15
  field: NestedFieldConfig;
15
16
  control: AbstractControl;
16
17
  config: EntityFormConfig;
@@ -21,6 +22,12 @@ export declare class DynamicFieldComponent implements OnChanges {
21
22
  private readonly fieldHost;
22
23
  private readonly fieldRegistry;
23
24
  private readonly rbacService;
25
+ /**
26
+ * Passed into `createComponent` so a hosted field sees this host's injector — including
27
+ * `DynamicFormComponent`'s scoped `EntityRefSelectionService`. Without it, a standalone
28
+ * field resolves the root bus and `autoPatch` never runs.
29
+ */
30
+ private readonly injector;
24
31
  private componentRef;
25
32
  ngOnChanges(_changes: SimpleChanges): void;
26
33
  private mountField;
@@ -32,7 +39,35 @@ export declare class DynamicFieldComponent implements OnChanges {
32
39
  private warnUnresolved;
33
40
  private static readonly warnedTypes;
34
41
  /**
35
- * Pass all 5 contract inputs via ComponentRef.setInput() — uniform for all types (ADR-008).
42
+ * Re-check the hosted field component.
43
+ *
44
+ * The field components are OnPush, so they re-render on an input change, an event from
45
+ * their own template, or a signal they read. `form.markAllAsTouched()` is none of those —
46
+ * it flips `touched` from outside, and every field's error message depends on it. Without
47
+ * this, a blocked submit would mark the form touched and no error would appear.
48
+ */
49
+ refresh(): void;
50
+ /**
51
+ * Re-check the hosted component whenever its control changes from outside.
52
+ *
53
+ * The field components are OnPush, so they re-render on an input change or an event from
54
+ * their own template. A control mutated externally is neither — and that is not an edge
55
+ * case: patchForm, reset, autoPatch and patchOnTrue all do exactly that. Without this, a
56
+ * field patched from a record would keep rendering its previous value.
57
+ *
58
+ * `detectChanges()` rather than `markForCheck()`: a selection that arrives from a native
59
+ * `change` event (or Playwright) may not be followed by another Angular tick, so a dirty
60
+ * flag would never be flushed. Running CD on the hosted component now is the copy showing up.
61
+ *
62
+ * Done here rather than in each of the eighteen field components: the host owns the
63
+ * component reference, so one subscription covers every type including custom ones.
64
+ */
65
+ private watchControl;
66
+ private controlSub?;
67
+ ngOnDestroy(): void;
68
+ /**
69
+ * Pass all 5 contract inputs via ComponentRef.setInput() — uniform for all types.
70
+ * The public shape is DynamicFieldComponentContract.
36
71
  * Must use setInput(), not property assignment: the field components declare inputs with
37
72
  * definite assignment and no initializer, and the lib compiles with
38
73
  * useDefineForClassFields:false, so `'field' in instance` is false until first set.
@@ -1,6 +1,7 @@
1
1
  import { EventEmitter, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core';
2
2
  import { AbstractControl, FormArray, FormGroup } from '@angular/forms';
3
3
  import type { EntityFormConfig, FormRule, NestedFieldConfig, NestedTabConfig } from '@dynamic-entity/core';
4
+ import { RbacService } from '../services/rbac.service';
4
5
  import * as i0 from "@angular/core";
5
6
  /**
6
7
  * DynamicFormComponent — the main form component.
@@ -9,7 +10,21 @@ import * as i0 from "@angular/core";
9
10
  * keyboard shortcuts (Ctrl+S, Esc), and RBAC-gated submission.
10
11
  */
11
12
  export declare class DynamicFormComponent implements OnInit, OnChanges, OnDestroy {
12
- config: EntityFormConfig;
13
+ /**
14
+ * The schema to render.
15
+ *
16
+ * An accessor pair rather than a plain field: `ngOnChanges` normalises the option shape,
17
+ * and that result is kept here instead of being written back over the input. Reassigning
18
+ * an `@Input` works, but it mutates what the parent handed us — and a parent using OnPush
19
+ * or signals would never observe the replacement anyway.
20
+ *
21
+ * Reads of `this.config` get the normalised copy once it exists, so every internal caller
22
+ * is unaffected.
23
+ */
24
+ set config(value: EntityFormConfig);
25
+ get config(): EntityFormConfig;
26
+ private rawConfig;
27
+ private normalizedConfig?;
13
28
  rules?: FormRule[];
14
29
  initialData?: Record<string, any>;
15
30
  /** Session-original values for `VALUE_CHANGED` rules. Captured from the first build when omitted. */
@@ -42,6 +57,14 @@ export declare class DynamicFormComponent implements OnInit, OnChanges, OnDestro
42
57
  formReset: EventEmitter<void>;
43
58
  /** The visible tab changed. Lets a host track it without reaching into this component. */
44
59
  activeTabChange: EventEmitter<string>;
60
+ /**
61
+ * Emitted when a `beforeSave` hook aborted the save — by returning false or throwing.
62
+ * `formSubmit` does not fire in that case.
63
+ */
64
+ saveRejected: EventEmitter<{
65
+ reason: string;
66
+ error?: unknown;
67
+ }>;
45
68
  private readonly fb;
46
69
  private readonly validatorRegistry;
47
70
  private readonly hookRegistry;
@@ -49,6 +72,7 @@ export declare class DynamicFormComponent implements OnInit, OnChanges, OnDestro
49
72
  private readonly rulesEvaluation;
50
73
  private readonly entityRefSelection;
51
74
  private readonly commonModulesRegistry;
75
+ private readonly migrations;
52
76
  protected readonly Object: ObjectConstructor;
53
77
  readonly activeTab: import("@angular/core").WritableSignal<string>;
54
78
  readonly activeSubTab: import("@angular/core").WritableSignal<string>;
@@ -58,13 +82,17 @@ export declare class DynamicFormComponent implements OnInit, OnChanges, OnDestro
58
82
  readonly sessionBaseline: import("@angular/core").WritableSignal<Record<string, any>>;
59
83
  /** Critical fields the user has explicitly unlocked for editing. */
60
84
  readonly unlockedFields: import("@angular/core").WritableSignal<ReadonlySet<string>>;
85
+ /** The active tab's field panel, focused after a tab switch. */
86
+ private formPanel?;
87
+ /** The hosted field components, so an external control-state change can reach them. */
88
+ private fieldHosts?;
61
89
  form: FormGroup;
62
90
  private valueSub?;
63
91
  private selectionSub?;
64
92
  /** Previous values, for detecting `patchOnTrue` false→true transitions. */
65
93
  private previousValues;
66
94
  handleKeyboardEvent(event: KeyboardEvent): void;
67
- readonly ruleResult: import("@angular/core").Signal<import("@dynamic-entity/core").RuleEvaluationResult>;
95
+ readonly ruleResult: import("@angular/core").Signal<RuleEvaluationResult>;
68
96
  /**
69
97
  * Critical fields whose value differs from the session baseline.
70
98
  * These drive the deferred `VALUE_CHANGED` banner — a critical edit is announced once,
@@ -78,12 +106,57 @@ export declare class DynamicFormComponent implements OnInit, OnChanges, OnDestro
78
106
  get activeSubTabConfig(): NestedTabConfig | null;
79
107
  get activeTabModuleComponent(): any | null;
80
108
  get fieldsForActiveTab(): NestedFieldConfig[];
81
- get permissions(): {
82
- canView: boolean;
83
- canEdit: boolean;
84
- canDelete: boolean;
85
- };
109
+ /**
110
+ * Entity-level permissions for the current roles.
111
+ *
112
+ * Cached rather than recomputed: this is read from the template several times per change
113
+ * detection pass, and a fresh object each time is both wasted allocation and a new
114
+ * identity for anything comparing by reference. Invalidated in `ngOnChanges` when
115
+ * `config` or `userRoles` change, which are its only inputs.
116
+ */
117
+ get permissions(): ReturnType<RbacService['getPermissions']>;
118
+ private permissionsCache;
119
+ /**
120
+ * Whether the current roles may see this record at all.
121
+ *
122
+ * `permissions.view` used to be computed and thrown away — a user whose roles failed it
123
+ * still received the complete form with every value in the DOM. It is honoured now.
124
+ *
125
+ * As with masking, this is presentational: it stops the browser rendering data, it does
126
+ * not stop the data reaching the browser. Authorize on the server.
127
+ */
128
+ get canView(): boolean;
129
+ /**
130
+ * Whether the current roles may delete this record.
131
+ *
132
+ * The library ships no delete affordance, so this is surfaced for the consumer to gate
133
+ * their own — exposed rather than dropped, because a permission the schema declares
134
+ * should be answerable.
135
+ */
136
+ get canDelete(): boolean;
86
137
  get canSubmit(): boolean;
138
+ /**
139
+ * Errors raised by `validation` rules, keyed by the target they point at.
140
+ *
141
+ * A rule can declare the record invalid independently of Angular's validators, so form
142
+ * validity alone is not the whole picture. `DynamicRecordFormComponent.saveSection()`
143
+ * has always honoured these; `submit()` did not, which meant the same rule blocked one
144
+ * save path and merely painted a banner on the other — and anything wiring
145
+ * `(formSubmit)` to persistence wrote records the rules engine had already rejected.
146
+ */
147
+ get ruleValidationErrors(): Record<string, string>;
148
+ /** True when a `validation` rule is currently blocking submission. */
149
+ get hasRuleErrors(): boolean;
150
+ /**
151
+ * True while an async validator is still running.
152
+ *
153
+ * Angular reports a control with an outstanding async check as `pending`, and `invalid` is
154
+ * false until it settles — so without this a form could be submitted in the gap before a
155
+ * uniqueness check came back.
156
+ */
157
+ get isValidating(): boolean;
158
+ /** Single source of truth for both the submit guard and the button's disabled state. */
159
+ get submitBlocked(): boolean;
87
160
  ngOnInit(): void;
88
161
  ngOnChanges(changes: SimpleChanges): void;
89
162
  ngOnDestroy(): void;
@@ -113,7 +186,38 @@ export declare class DynamicFormComponent implements OnInit, OnChanges, OnDestro
113
186
  */
114
187
  createArrayRow(field: NestedFieldConfig, value?: unknown): AbstractControl;
115
188
  private buildArrayRow;
189
+ /**
190
+ * Mark every control touched, then re-check the field components.
191
+ *
192
+ * They are OnPush and `touched` is flipped from outside their own template, so the second
193
+ * half is what makes the error messages actually appear.
194
+ */
195
+ private markAllTouched;
196
+ /**
197
+ * Bring a record up to the config's `version` before it reaches the form.
198
+ *
199
+ * Done here because this is the one place a record enters the renderer, so a consumer
200
+ * cannot forget it. Migration is a no-op unless the consumer registered steps *and* the
201
+ * record is behind — an unstamped record is deliberately left alone (see `migrateRecord`).
202
+ *
203
+ * A missing or broken step throws from core. That is not swallowed: rendering a
204
+ * half-understood record would write it back in a shape matching neither schema.
205
+ */
206
+ private upgradeRecord;
116
207
  private patchForm;
208
+ /**
209
+ * A record is nested by tab id (`{ tabId: { fieldId: value } }`) unless the tab sets
210
+ * `flatData: true`. Handing a flat record to a nested tab finds nothing, so the fields
211
+ * stay empty — with no error and no clue. That silent miss is the most expensive way to
212
+ * lose an hour with this library, so name it in dev builds.
213
+ *
214
+ * Only top-level keys that match a known field id are reported: anything else is assumed
215
+ * to be the consumer's own record metadata (ids, timestamps, `_configVersion`) and is not
216
+ * our business.
217
+ */
218
+ private warnUnconsumedInitialData;
219
+ /** Warn once per key per form instance — patchForm re-runs on every initialData change. */
220
+ private readonly warnedUnconsumedKeys;
117
221
  /** Assemble full nested record from per-tab FormGroups, respecting flatData, refererField & arrays. */
118
222
  extractRecord(): Record<string, any>;
119
223
  private flattenFormValues;
@@ -130,15 +234,58 @@ export declare class DynamicFormComponent implements OnInit, OnChanges, OnDestro
130
234
  private runPatchOnTrue;
131
235
  /** Prefer a control declared on the configured target tab; fall back to a top-level control. */
132
236
  private resolveTargetControl;
237
+ /**
238
+ * The fields the render filter operates on — a tab's own fields, at every tab depth.
239
+ * Deliberately not `allFields()`: `group` and `array` children are shown or hidden with
240
+ * their parent, and toggling a child's state independently would fight the parent's.
241
+ */
242
+ private tabFields;
243
+ /**
244
+ * Keep a hidden field's control out of form validity.
245
+ *
246
+ * A field hidden by a rule or a `showWhen` condition is filtered out of the render, but
247
+ * its validators stay attached — so a hidden *required* field holds `form.invalid` true
248
+ * forever, permanently disabling Save with nothing on screen to explain why.
249
+ *
250
+ * Disabling is the fix rather than stripping validators: Angular excludes disabled
251
+ * controls from validity, and the field's own validators survive intact for when it
252
+ * comes back. Values are untouched — `flattenFormValues` and `extractRecord` both read
253
+ * `control.value`, which a disabled control still carries, so rule evaluation and the
254
+ * emitted record see exactly what they saw before. That also keeps this from feeding
255
+ * back on itself: hiding a field cannot change the values the rules are evaluated over.
256
+ *
257
+ * The visibility predicate is the same one `fieldsForActiveTab` filters with, so what is
258
+ * rendered and what counts toward validity cannot drift apart.
259
+ */
260
+ private syncHiddenFieldState;
133
261
  /** Every field in the config, including `group`/`array` children. */
134
262
  private allFields;
135
263
  /** An empty `originalData` counts as "not supplied" — fall back to the captured baseline. */
136
264
  private baseline;
137
265
  private sameValue;
138
266
  getControl(fieldId: string, currentTabId?: string): AbstractControl | null;
267
+ /**
268
+ * Move focus into the newly shown panel.
269
+ *
270
+ * Activating a tab replaces everything below it while focus stays on the tab button, so a
271
+ * screen-reader or keyboard user is given no indication that the content changed and must
272
+ * tab back through the strip to reach it. Deferred a frame because the panel's contents
273
+ * are rendered by the change detection this call is part of.
274
+ */
275
+ private focusActivePanel;
276
+ /** Accessible name for the field panel — it is the tabpanel for the active tab. */
277
+ get activeTabLabel(): string;
278
+ /**
279
+ * Why a referenced field is flagged.
280
+ *
281
+ * `hasDrift` was written by the builder and read by nothing at runtime, so a field whose
282
+ * source definition had changed since it was linked looked entirely normal to the person
283
+ * filling the form in.
284
+ */
285
+ driftHint(field: NestedFieldConfig): string;
139
286
  resolveTabLabel(tab: NestedTabConfig): string;
140
287
  submit(): Promise<void>;
141
288
  reset(): void;
142
289
  static ɵfac: i0.ɵɵFactoryDeclaration<DynamicFormComponent, never>;
143
- static ɵcmp: i0.ɵɵComponentDeclaration<DynamicFormComponent, "ngx-dynamic-form", never, { "config": { "alias": "config"; "required": false; }; "rules": { "alias": "rules"; "required": false; }; "initialData": { "alias": "initialData"; "required": false; }; "originalData": { "alias": "originalData"; "required": false; }; "userRoles": { "alias": "userRoles"; "required": false; }; "language": { "alias": "language"; "required": false; }; "readonly": { "alias": "readonly"; "required": false; }; "readOnlyFields": { "alias": "readOnlyFields"; "required": false; }; "showInfoBanners": { "alias": "showInfoBanners"; "required": false; }; "loading": { "alias": "loading"; "required": false; }; "error": { "alias": "error"; "required": false; }; "changeDebounceMs": { "alias": "changeDebounceMs"; "required": false; }; "preview": { "alias": "preview"; "required": false; }; }, { "formSubmit": "formSubmit"; "formChange": "formChange"; "formReset": "formReset"; "activeTabChange": "activeTabChange"; }, never, never, true, never>;
290
+ static ɵcmp: i0.ɵɵComponentDeclaration<DynamicFormComponent, "ngx-dynamic-form", never, { "config": { "alias": "config"; "required": false; }; "rules": { "alias": "rules"; "required": false; }; "initialData": { "alias": "initialData"; "required": false; }; "originalData": { "alias": "originalData"; "required": false; }; "userRoles": { "alias": "userRoles"; "required": false; }; "language": { "alias": "language"; "required": false; }; "readonly": { "alias": "readonly"; "required": false; }; "readOnlyFields": { "alias": "readOnlyFields"; "required": false; }; "showInfoBanners": { "alias": "showInfoBanners"; "required": false; }; "loading": { "alias": "loading"; "required": false; }; "error": { "alias": "error"; "required": false; }; "changeDebounceMs": { "alias": "changeDebounceMs"; "required": false; }; "preview": { "alias": "preview"; "required": false; }; }, { "formSubmit": "formSubmit"; "formChange": "formChange"; "formReset": "formReset"; "activeTabChange": "activeTabChange"; "saveRejected": "saveRejected"; }, never, never, true, never>;
144
291
  }
@@ -70,7 +70,7 @@ export declare class DynamicRecordFormComponent implements OnChanges {
70
70
  * building a row's FormGroup inside the tab view races the tab's own initialisation, so
71
71
  * the row form is kept detached and only pushed into the FormArray on save.
72
72
  */
73
- readonly inlineRowField: import("@angular/core").WritableSignal<NestedFieldConfig>;
73
+ readonly inlineRowField: import("@angular/core").WritableSignal<any>;
74
74
  readonly inlineRowIndex: import("@angular/core").WritableSignal<number>;
75
75
  inlineRowForm: FormGroup | null;
76
76
  /** `array` fields on the tab currently shown, so the template can list their rows. */
@@ -153,7 +153,18 @@ export declare class DynamicRecordFormComponent implements OnChanges {
153
153
  toggleHeaderStatus(): void;
154
154
  /** Locate a field's control wherever it lives, since header fields may be on any tab. */
155
155
  private controlFor;
156
- /** A field's current value, read from the record so it is stable during render. */
156
+ /**
157
+ * A field's current value, read from the record so it is stable during render.
158
+ *
159
+ * Resolved through `getTabData` so it honours exactly the nesting contract the form
160
+ * patches with — `{ tabId: { fieldId } }`, or the record root when the tab sets
161
+ * `flatData`. Reading the record flat instead is not a shortcut: it makes the summary
162
+ * display values the form never loaded, which reads as "the data is there" while every
163
+ * control behind it is empty.
164
+ *
165
+ * Walks sub-tabs as well: a summary, header, or profile field may sit on any tab at any
166
+ * depth.
167
+ */
157
168
  private fieldValue;
158
169
  readonly isModified: import("@angular/core").Signal<boolean>;
159
170
  readonly summaryFields: import("@angular/core").Signal<NestedFieldConfig[]>;
@@ -1,12 +1,13 @@
1
1
  import { EnvironmentProviders, Type } from '@angular/core';
2
- import type { EntityReferenceLoader, LookupListSource } from '@dynamic-entity/core';
2
+ import type { EntityReferenceLoader, LookupListSource, RecordMigration } from '@dynamic-entity/core';
3
+ import { type HookFn } from '../tokens/injection-tokens';
3
4
  export interface NgxDynamicEntityConfig {
4
5
  /** Roles that see masked field values as XXXXXXXXX */
5
6
  maskedRoles?: string[];
6
7
  /** Custom field type components keyed by type string */
7
8
  fieldTypes?: Record<string, Type<any>>;
8
9
  /**
9
- * Entity-ref option loaders keyed by entity key (ADR-006).
10
+ * Entity-ref option loaders keyed by entity key.
10
11
  * Each receives `{ parentValue, filters, lang }` and may return an array, Promise, or Observable.
11
12
  */
12
13
  entityRefs?: Record<string, EntityReferenceLoader>;
@@ -16,10 +17,31 @@ export interface NgxDynamicEntityConfig {
16
17
  * a loader for anything fetched, so an unused list is never loaded.
17
18
  */
18
19
  lookups?: Record<string, LookupListSource>;
20
+ /**
21
+ * Override the messages shown under invalid fields, keyed by Angular error key. A value is
22
+ * a string, or a function receiving `(language, error)` when it needs the error's detail.
23
+ * Unlisted keys keep their English default.
24
+ */
25
+ validationMessages?: Record<string, string | ((language: string, error: any) => string)>;
26
+ /**
27
+ * Async validator functions keyed by name, for checks that need a server — uniqueness,
28
+ * availability, a remote rule. Name them from a schema with
29
+ * `validators: { customAsync: ['uniqueEmail'] }`.
30
+ *
31
+ * Each is an Angular `AsyncValidatorFn`: it receives the control and returns a Promise or
32
+ * Observable of `ValidationErrors | null`. The control is `pending` while it runs, and the
33
+ * form cannot be submitted until every pending check settles.
34
+ */
35
+ asyncValidators?: Record<string, any>;
19
36
  /** Custom validator functions keyed by validator name */
20
37
  validators?: Record<string, any>;
21
38
  /** Hook functions keyed by hook name */
22
- hooks?: Record<string, Function>;
39
+ hooks?: Record<string, HookFn>;
40
+ /**
41
+ * Ordered steps that upgrade a saved record when `EntityFormConfig.version` moves ahead
42
+ * of the record's `_configVersion`. Applied to `initialData` before the form is patched.
43
+ */
44
+ migrations?: RecordMigration[];
23
45
  }
24
46
  /**
25
47
  * Provider function for ngx-dynamic-entity.
@@ -3,7 +3,7 @@ import * as i0 from "@angular/core";
3
3
  /**
4
4
  * CascadeDataService — loads entity-reference options and applies parent→child cascades.
5
5
  *
6
- * Loaders come from the registry only (ADR-006). This service owns the three things a
6
+ * Loaders come from the registry only. This service owns the three things a
7
7
  * field component should not: resolving the loader, coercing whatever it returned
8
8
  * (array | Promise | Observable) into a Promise, and filtering by the parent's value.
9
9
  */
@@ -2,7 +2,7 @@ import type { EntityReferenceLoader } from '@dynamic-entity/core';
2
2
  import * as i0 from "@angular/core";
3
3
  /**
4
4
  * EntityRefRegistryService — resolves entity-ref option loader functions.
5
- * ADR-006: Entity-ref options come ONLY from this registry.
5
+ * Entity-ref options come ONLY from this registry.
6
6
  * Never pass loaders via @Input() on field components.
7
7
  *
8
8
  * Consumers register loaders via provideNgxDynamicEntity({ entityRefs: { clients: () => [...] } }).
@@ -81,7 +81,7 @@ export interface ChoiceOptionHost {
81
81
  * Point a choice component's `options` signal at the right source (parity plan §6.3).
82
82
  *
83
83
  * Called from the `field` / `language` input setters of `dropdown`, `radio` and `multiSelect` —
84
- * the wiring is theirs, the decision is the service's, which is what keeps the ADR-008
84
+ * the wiring is theirs, the decision is the service's, which is what keeps the field
85
85
  * five-input contract intact: options are never pushed in as a sixth input.
86
86
  *
87
87
  * Setters rather than `ngOnChanges` on purpose. `ngOnChanges` only fires for inputs Angular
@@ -0,0 +1,43 @@
1
+ import type { ValidationErrors } from '@angular/forms';
2
+ import * as i0 from "@angular/core";
3
+ /**
4
+ * Resolves an Angular `ValidationErrors` object to the message shown under a field.
5
+ *
6
+ * These strings used to be hardcoded English literals inside each field component, so a
7
+ * German form meant re-implementing every field type — the labels honoured `language` while
8
+ * the errors underneath them did not.
9
+ *
10
+ * A message is a plain string, or a function when it needs the error's own detail
11
+ * (`minlength.requiredLength`, `min.min`, and so on). The active `language` is passed
12
+ * through, so one registration can serve every locale:
13
+ *
14
+ * ```typescript
15
+ * provideNgxDynamicEntity({
16
+ * validationMessages: {
17
+ * required: lang => (lang === 'de' ? 'Pflichtfeld.' : 'This field is required.'),
18
+ * minlength: (lang, err) => `Mindestens ${err['requiredLength']} Zeichen.`,
19
+ * },
20
+ * });
21
+ * ```
22
+ *
23
+ * Unregistered keys fall back to the defaults below, so overriding one message does not
24
+ * mean supplying them all.
25
+ */
26
+ export type ValidationMessage = string | ((language: string, error: any) => string);
27
+ /** The built-in English messages. Every key here can be overridden individually. */
28
+ export declare const DEFAULT_VALIDATION_MESSAGES: Record<string, ValidationMessage>;
29
+ export declare class ValidationMessagesService {
30
+ private readonly overrides;
31
+ /**
32
+ * The message for the first error present, in the order the caller lists.
33
+ *
34
+ * Order is the caller's because it is field-specific: a dropdown wants
35
+ * `requiredSelection` where a text input wants `required`, and the first match wins so the
36
+ * most specific message is the one shown.
37
+ */
38
+ resolve(errors: ValidationErrors | null | undefined, language: string, order: readonly (string | readonly [errorKey: string, messageKey: string])[], fallbackKey?: string): string;
39
+ /** One message by key, honouring an override before the default. */
40
+ messageFor(key: string, language: string, error: unknown): string;
41
+ static ɵfac: i0.ɵɵFactoryDeclaration<ValidationMessagesService, never>;
42
+ static ɵprov: i0.ɵɵInjectableDeclaration<ValidationMessagesService>;
43
+ }