ngx-dynamic-entity 0.0.1 → 1.1.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.
- package/README.md +129 -0
- package/esm2022/lib/field-types/array-field.component.mjs +160 -0
- package/esm2022/lib/field-types/boolean-field.component.mjs +100 -0
- package/esm2022/lib/field-types/checkbox-field.component.mjs +25 -17
- package/esm2022/lib/field-types/currency-field.component.mjs +101 -0
- package/esm2022/lib/field-types/date-field.component.mjs +21 -13
- package/esm2022/lib/field-types/dropdown-field.component.mjs +120 -29
- package/esm2022/lib/field-types/email-field.component.mjs +101 -0
- package/esm2022/lib/field-types/entity-ref-field.component.mjs +96 -32
- package/esm2022/lib/field-types/file-field.component.mjs +156 -0
- package/esm2022/lib/field-types/group-field.component.mjs +89 -0
- package/esm2022/lib/field-types/image-field.component.mjs +168 -0
- package/esm2022/lib/field-types/month-year-field.component.mjs +151 -0
- package/esm2022/lib/field-types/multi-select-field.component.mjs +79 -25
- package/esm2022/lib/field-types/number-field.component.mjs +60 -17
- package/esm2022/lib/field-types/password-field.component.mjs +109 -0
- package/esm2022/lib/field-types/radio-field.component.mjs +152 -0
- package/esm2022/lib/field-types/text-field.component.mjs +58 -18
- package/esm2022/lib/field-types/textarea-field.component.mjs +26 -15
- package/esm2022/lib/form/dynamic-field/dynamic-field.component.mjs +44 -26
- package/esm2022/lib/form/dynamic-form.component.mjs +676 -69
- package/esm2022/lib/form/dynamic-record-form.component.mjs +494 -0
- package/esm2022/lib/providers/provide-field-types.mjs +72 -0
- package/esm2022/lib/providers/provide-ngx-dynamic-entity.mjs +4 -5
- package/esm2022/lib/services/cascade-data.service.mjs +129 -0
- package/esm2022/lib/services/config-source.service.mjs +67 -0
- package/esm2022/lib/services/entity-ref-cache.mjs +20 -0
- package/esm2022/lib/services/entity-ref-registry.service.mjs +3 -3
- package/esm2022/lib/services/entity-ref-selection.service.mjs +27 -0
- package/esm2022/lib/services/entity-reference.service.mjs +168 -0
- package/esm2022/lib/services/field-registry.service.mjs +35 -30
- package/esm2022/lib/services/file-upload.service.mjs +89 -0
- package/esm2022/lib/services/lookup-registry.service.mjs +246 -0
- package/esm2022/lib/services/rbac.service.mjs +5 -11
- package/esm2022/lib/services/rules-evaluation.service.mjs +27 -0
- package/esm2022/lib/services/validator-registry.service.mjs +35 -7
- package/esm2022/lib/tokens/injection-tokens.mjs +104 -7
- package/esm2022/public-api.mjs +31 -6
- package/fesm2022/ngx-dynamic-entity.mjs +4094 -759
- package/fesm2022/ngx-dynamic-entity.mjs.map +1 -1
- package/lib/field-types/array-field.component.d.ts +22 -0
- package/lib/field-types/boolean-field.component.d.ts +18 -0
- package/lib/field-types/checkbox-field.component.d.ts +3 -2
- package/lib/field-types/currency-field.component.d.ts +17 -0
- package/lib/field-types/date-field.component.d.ts +3 -2
- package/lib/field-types/dropdown-field.component.d.ts +24 -4
- package/lib/field-types/email-field.component.d.ts +15 -0
- package/lib/field-types/entity-ref-field.component.d.ts +24 -11
- package/lib/field-types/file-field.component.d.ts +25 -0
- package/lib/field-types/group-field.component.d.ts +17 -0
- package/lib/field-types/image-field.component.d.ts +30 -0
- package/lib/field-types/month-year-field.component.d.ts +28 -0
- package/lib/field-types/multi-select-field.component.d.ts +18 -4
- package/lib/field-types/number-field.component.d.ts +5 -2
- package/lib/field-types/password-field.component.d.ts +17 -0
- package/lib/field-types/radio-field.component.d.ts +33 -0
- package/lib/field-types/text-field.component.d.ts +5 -3
- package/lib/field-types/textarea-field.component.d.ts +4 -2
- package/lib/form/dynamic-field/dynamic-field.component.d.ts +20 -7
- package/lib/form/dynamic-form.component.d.ts +203 -24
- package/lib/form/dynamic-record-form.component.d.ts +180 -0
- package/lib/providers/provide-field-types.d.ts +24 -0
- package/lib/providers/provide-ngx-dynamic-entity.d.ts +19 -8
- package/lib/services/cascade-data.service.d.ts +45 -0
- package/lib/services/config-source.service.d.ts +14 -0
- package/lib/services/entity-ref-cache.d.ts +27 -0
- package/lib/services/entity-ref-registry.service.d.ts +4 -3
- package/lib/services/entity-ref-selection.service.d.ts +24 -0
- package/lib/services/entity-reference.service.d.ts +49 -0
- package/lib/services/field-registry.service.d.ts +17 -7
- package/lib/services/file-upload.service.d.ts +27 -0
- package/lib/services/lookup-registry.service.d.ts +94 -0
- package/lib/services/rbac.service.d.ts +4 -9
- package/lib/services/rules-evaluation.service.d.ts +17 -0
- package/lib/services/validator-registry.service.d.ts +5 -4
- package/lib/tokens/injection-tokens.d.ts +113 -8
- package/package.json +18 -8
- package/public-api.d.ts +21 -4
- package/esm2022/lib/services/config.service.mjs +0 -58
- package/esm2022/lib/services/version.service.mjs +0 -40
- package/esm2022/lib/table/dynamic-table.component.mjs +0 -160
- package/lib/services/config.service.d.ts +0 -25
- package/lib/services/version.service.d.ts +0 -26
- package/lib/table/dynamic-table.component.d.ts +0 -56
|
@@ -1,52 +1,231 @@
|
|
|
1
|
-
import { EventEmitter, OnChanges, SimpleChanges } from '@angular/core';
|
|
2
|
-
import { FormGroup } from '@angular/forms';
|
|
3
|
-
import type {
|
|
4
|
-
import {
|
|
1
|
+
import { EventEmitter, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core';
|
|
2
|
+
import { AbstractControl, FormArray, FormGroup } from '@angular/forms';
|
|
3
|
+
import type { EntityFormConfig, FormRule, NestedFieldConfig, NestedTabConfig } from '@dynamic-entity/core';
|
|
4
|
+
import { RbacService } from '../services/rbac.service';
|
|
5
5
|
import * as i0 from "@angular/core";
|
|
6
6
|
/**
|
|
7
7
|
* DynamicFormComponent — the main form component.
|
|
8
|
-
* Renders a reactive form from
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* ONEHERMES pattern: isSaving state lock prevents subscription cyclones (ADR-002).
|
|
12
|
-
* ONEHERMES bug fix: visibility rules re-evaluated after initialData loads (form-fixes doc).
|
|
13
|
-
* ONEHERMES bug fix: use @for + @if separately, not both on same element (form-fixes doc).
|
|
8
|
+
* Renders a reactive form from EntityFormConfig with tab support, responsive 12-col grid,
|
|
9
|
+
* rules evaluation, conditional visibility, autoPatch/patchOnTrue, criticalField locking,
|
|
10
|
+
* keyboard shortcuts (Ctrl+S, Esc), and RBAC-gated submission.
|
|
14
11
|
*/
|
|
15
|
-
export declare class DynamicFormComponent implements OnChanges {
|
|
16
|
-
config:
|
|
12
|
+
export declare class DynamicFormComponent implements OnInit, OnChanges, OnDestroy {
|
|
13
|
+
config: EntityFormConfig;
|
|
14
|
+
rules?: FormRule[];
|
|
17
15
|
initialData?: Record<string, any>;
|
|
16
|
+
/** Session-original values for `VALUE_CHANGED` rules. Captured from the first build when omitted. */
|
|
17
|
+
originalData?: Record<string, any>;
|
|
18
18
|
userRoles: string[];
|
|
19
19
|
language: string;
|
|
20
20
|
readonly: boolean;
|
|
21
|
+
/** Field ids forced read-only while the rest of the form stays editable. */
|
|
22
|
+
readOnlyFields: string[];
|
|
23
|
+
/**
|
|
24
|
+
* Render rule `info` banners inline. The record editor sets this false and renders its
|
|
25
|
+
* own dismissible versions, so the two do not double up.
|
|
26
|
+
*/
|
|
27
|
+
showInfoBanners: boolean;
|
|
21
28
|
loading: boolean;
|
|
22
29
|
error: string | null;
|
|
30
|
+
/**
|
|
31
|
+
* Milliseconds to debounce `formChange`. The reference debounces at 300ms so a consumer
|
|
32
|
+
* re-rendering on every value change is not driven per keystroke. Zero emits
|
|
33
|
+
* synchronously, which the record editor relies on for its own bookkeeping.
|
|
34
|
+
*/
|
|
35
|
+
changeDebounceMs: number;
|
|
36
|
+
/**
|
|
37
|
+
* Preview mode: seed one empty row per `array` field so the structure is visible, then
|
|
38
|
+
* disable the form. Used by the builder's live preview.
|
|
39
|
+
*/
|
|
40
|
+
preview: boolean;
|
|
23
41
|
formSubmit: EventEmitter<Record<string, any>>;
|
|
24
42
|
formChange: EventEmitter<Record<string, any>>;
|
|
25
43
|
formReset: EventEmitter<void>;
|
|
44
|
+
/** The visible tab changed. Lets a host track it without reaching into this component. */
|
|
45
|
+
activeTabChange: EventEmitter<string>;
|
|
26
46
|
private readonly fb;
|
|
27
47
|
private readonly validatorRegistry;
|
|
28
48
|
private readonly hookRegistry;
|
|
29
|
-
protected readonly versionService: VersionService;
|
|
30
49
|
private readonly rbacService;
|
|
50
|
+
private readonly rulesEvaluation;
|
|
51
|
+
private readonly entityRefSelection;
|
|
52
|
+
private readonly commonModulesRegistry;
|
|
53
|
+
private readonly migrations;
|
|
54
|
+
protected readonly Object: ObjectConstructor;
|
|
31
55
|
readonly activeTab: import("@angular/core").WritableSignal<string>;
|
|
56
|
+
readonly activeSubTab: import("@angular/core").WritableSignal<string>;
|
|
32
57
|
readonly isSaving: import("@angular/core").WritableSignal<boolean>;
|
|
58
|
+
readonly formValues: import("@angular/core").WritableSignal<Record<string, any>>;
|
|
59
|
+
/** Baseline captured at first build, used when `originalData` is not supplied. */
|
|
60
|
+
readonly sessionBaseline: import("@angular/core").WritableSignal<Record<string, any>>;
|
|
61
|
+
/** Critical fields the user has explicitly unlocked for editing. */
|
|
62
|
+
readonly unlockedFields: import("@angular/core").WritableSignal<ReadonlySet<string>>;
|
|
33
63
|
form: FormGroup;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
64
|
+
private valueSub?;
|
|
65
|
+
private selectionSub?;
|
|
66
|
+
/** Previous values, for detecting `patchOnTrue` false→true transitions. */
|
|
67
|
+
private previousValues;
|
|
68
|
+
handleKeyboardEvent(event: KeyboardEvent): void;
|
|
69
|
+
readonly ruleResult: import("@angular/core").Signal<RuleEvaluationResult>;
|
|
70
|
+
/**
|
|
71
|
+
* Critical fields whose value differs from the session baseline.
|
|
72
|
+
* These drive the deferred `VALUE_CHANGED` banner — a critical edit is announced once,
|
|
73
|
+
* against the value the record had when the session started, not per keystroke.
|
|
74
|
+
*/
|
|
75
|
+
readonly changedCriticalFields: import("@angular/core").Signal<NestedFieldConfig[]>;
|
|
76
|
+
get tabs(): NestedTabConfig[];
|
|
77
|
+
get visibleTabs(): NestedTabConfig[];
|
|
78
|
+
get activeTabConfig(): NestedTabConfig | null;
|
|
79
|
+
get visibleSubTabs(): NestedTabConfig[];
|
|
80
|
+
get activeSubTabConfig(): NestedTabConfig | null;
|
|
81
|
+
get activeTabModuleComponent(): any | null;
|
|
82
|
+
get fieldsForActiveTab(): NestedFieldConfig[];
|
|
83
|
+
/**
|
|
84
|
+
* Entity-level permissions for the current roles.
|
|
85
|
+
*
|
|
86
|
+
* Cached rather than recomputed: this is read from the template several times per change
|
|
87
|
+
* detection pass, and a fresh object each time is both wasted allocation and a new
|
|
88
|
+
* identity for anything comparing by reference. Invalidated in `ngOnChanges` when
|
|
89
|
+
* `config` or `userRoles` change, which are its only inputs.
|
|
90
|
+
*/
|
|
91
|
+
get permissions(): ReturnType<RbacService['getPermissions']>;
|
|
92
|
+
private permissionsCache;
|
|
93
|
+
/**
|
|
94
|
+
* Whether the current roles may see this record at all.
|
|
95
|
+
*
|
|
96
|
+
* `permissions.view` used to be computed and thrown away — a user whose roles failed it
|
|
97
|
+
* still received the complete form with every value in the DOM. It is honoured now.
|
|
98
|
+
*
|
|
99
|
+
* As with masking, this is presentational: it stops the browser rendering data, it does
|
|
100
|
+
* not stop the data reaching the browser. Authorize on the server.
|
|
101
|
+
*/
|
|
102
|
+
get canView(): boolean;
|
|
103
|
+
/**
|
|
104
|
+
* Whether the current roles may delete this record.
|
|
105
|
+
*
|
|
106
|
+
* The library ships no delete affordance, so this is surfaced for the consumer to gate
|
|
107
|
+
* their own — exposed rather than dropped, because a permission the schema declares
|
|
108
|
+
* should be answerable.
|
|
109
|
+
*/
|
|
110
|
+
get canDelete(): boolean;
|
|
42
111
|
get canSubmit(): boolean;
|
|
112
|
+
/**
|
|
113
|
+
* Errors raised by `validation` rules, keyed by the target they point at.
|
|
114
|
+
*
|
|
115
|
+
* A rule can declare the record invalid independently of Angular's validators, so form
|
|
116
|
+
* validity alone is not the whole picture. `DynamicRecordFormComponent.saveSection()`
|
|
117
|
+
* has always honoured these; `submit()` did not, which meant the same rule blocked one
|
|
118
|
+
* save path and merely painted a banner on the other — and anything wiring
|
|
119
|
+
* `(formSubmit)` to persistence wrote records the rules engine had already rejected.
|
|
120
|
+
*/
|
|
121
|
+
get ruleValidationErrors(): Record<string, string>;
|
|
122
|
+
/** True when a `validation` rule is currently blocking submission. */
|
|
123
|
+
get hasRuleErrors(): boolean;
|
|
124
|
+
/** Single source of truth for both the submit guard and the button's disabled state. */
|
|
125
|
+
get submitBlocked(): boolean;
|
|
126
|
+
ngOnInit(): void;
|
|
43
127
|
ngOnChanges(changes: SimpleChanges): void;
|
|
128
|
+
ngOnDestroy(): void;
|
|
129
|
+
setActiveTab(tabId: string): void;
|
|
130
|
+
setActiveSubTab(subTabId: string): void;
|
|
131
|
+
getFieldSpan(field: NestedFieldConfig): string;
|
|
132
|
+
/** A critical field stays read-only until the user deliberately unlocks it. */
|
|
133
|
+
isFieldLocked(field: NestedFieldConfig): boolean;
|
|
134
|
+
/**
|
|
135
|
+
* Whether this field renders read-only, from any of the four reasons: the whole form,
|
|
136
|
+
* the field's own flag, a caller-supplied `readOnlyFields` entry, or a criticalField lock.
|
|
137
|
+
*/
|
|
138
|
+
isFieldReadonly(field: NestedFieldConfig): boolean;
|
|
139
|
+
toggleFieldLock(field: NestedFieldConfig): void;
|
|
140
|
+
resolveFieldLabel(field: NestedFieldConfig): string;
|
|
44
141
|
private buildForm;
|
|
142
|
+
private buildFieldControl;
|
|
143
|
+
/** One row of an `array` field: a FormGroup when the field declares columns, else a bare control. */
|
|
144
|
+
/**
|
|
145
|
+
* The `FormArray` behind an `array` field, for callers that manage rows themselves —
|
|
146
|
+
* the record editor's inline row drawer.
|
|
147
|
+
*/
|
|
148
|
+
getArrayControl(fieldId: string, tabId?: string): FormArray | null;
|
|
149
|
+
/**
|
|
150
|
+
* Build one detached row for an `array` field, with the field's own validators applied.
|
|
151
|
+
* Exposed so a row can be edited outside the form and pushed on save.
|
|
152
|
+
*/
|
|
153
|
+
createArrayRow(field: NestedFieldConfig, value?: unknown): AbstractControl;
|
|
154
|
+
private buildArrayRow;
|
|
155
|
+
/**
|
|
156
|
+
* Bring a record up to the config's `version` before it reaches the form.
|
|
157
|
+
*
|
|
158
|
+
* Done here because this is the one place a record enters the renderer, so a consumer
|
|
159
|
+
* cannot forget it. Migration is a no-op unless the consumer registered steps *and* the
|
|
160
|
+
* record is behind — an unstamped record is deliberately left alone (see `migrateRecord`).
|
|
161
|
+
*
|
|
162
|
+
* A missing or broken step throws from core. That is not swallowed: rendering a
|
|
163
|
+
* half-understood record would write it back in a shape matching neither schema.
|
|
164
|
+
*/
|
|
165
|
+
private upgradeRecord;
|
|
45
166
|
private patchForm;
|
|
46
|
-
|
|
167
|
+
/**
|
|
168
|
+
* A record is nested by tab id (`{ tabId: { fieldId: value } }`) unless the tab sets
|
|
169
|
+
* `flatData: true`. Handing a flat record to a nested tab finds nothing, so the fields
|
|
170
|
+
* stay empty — with no error and no clue. That silent miss is the most expensive way to
|
|
171
|
+
* lose an hour with this library, so name it in dev builds.
|
|
172
|
+
*
|
|
173
|
+
* Only top-level keys that match a known field id are reported: anything else is assumed
|
|
174
|
+
* to be the consumer's own record metadata (ids, timestamps, `_configVersion`) and is not
|
|
175
|
+
* our business.
|
|
176
|
+
*/
|
|
177
|
+
private warnUnconsumedInitialData;
|
|
178
|
+
/** Warn once per key per form instance — patchForm re-runs on every initialData change. */
|
|
179
|
+
private readonly warnedUnconsumedKeys;
|
|
180
|
+
/** Assemble full nested record from per-tab FormGroups, respecting flatData, refererField & arrays. */
|
|
181
|
+
extractRecord(): Record<string, any>;
|
|
182
|
+
private flattenFormValues;
|
|
183
|
+
private getTabGroup;
|
|
184
|
+
/**
|
|
185
|
+
* `autoPatch` — copy mapped fields out of the record the user just selected in an
|
|
186
|
+
* entity-ref field into the configured target tab's controls.
|
|
187
|
+
*/
|
|
188
|
+
private runAutoPatch;
|
|
189
|
+
/**
|
|
190
|
+
* `patchOnTrue` — when a boolean/checkbox field transitions to `true`,
|
|
191
|
+
* copy `from` → `to` within the current record.
|
|
192
|
+
*/
|
|
193
|
+
private runPatchOnTrue;
|
|
194
|
+
/** Prefer a control declared on the configured target tab; fall back to a top-level control. */
|
|
195
|
+
private resolveTargetControl;
|
|
196
|
+
/**
|
|
197
|
+
* The fields the render filter operates on — a tab's own fields, at every tab depth.
|
|
198
|
+
* Deliberately not `allFields()`: `group` and `array` children are shown or hidden with
|
|
199
|
+
* their parent, and toggling a child's state independently would fight the parent's.
|
|
200
|
+
*/
|
|
201
|
+
private tabFields;
|
|
202
|
+
/**
|
|
203
|
+
* Keep a hidden field's control out of form validity.
|
|
204
|
+
*
|
|
205
|
+
* A field hidden by a rule or a `showWhen` condition is filtered out of the render, but
|
|
206
|
+
* its validators stay attached — so a hidden *required* field holds `form.invalid` true
|
|
207
|
+
* forever, permanently disabling Save with nothing on screen to explain why.
|
|
208
|
+
*
|
|
209
|
+
* Disabling is the fix rather than stripping validators: Angular excludes disabled
|
|
210
|
+
* controls from validity, and the field's own validators survive intact for when it
|
|
211
|
+
* comes back. Values are untouched — `flattenFormValues` and `extractRecord` both read
|
|
212
|
+
* `control.value`, which a disabled control still carries, so rule evaluation and the
|
|
213
|
+
* emitted record see exactly what they saw before. That also keeps this from feeding
|
|
214
|
+
* back on itself: hiding a field cannot change the values the rules are evaluated over.
|
|
215
|
+
*
|
|
216
|
+
* The visibility predicate is the same one `fieldsForActiveTab` filters with, so what is
|
|
217
|
+
* rendered and what counts toward validity cannot drift apart.
|
|
218
|
+
*/
|
|
219
|
+
private syncHiddenFieldState;
|
|
220
|
+
/** Every field in the config, including `group`/`array` children. */
|
|
221
|
+
private allFields;
|
|
222
|
+
/** An empty `originalData` counts as "not supplied" — fall back to the captured baseline. */
|
|
223
|
+
private baseline;
|
|
224
|
+
private sameValue;
|
|
225
|
+
getControl(fieldId: string, currentTabId?: string): AbstractControl | null;
|
|
226
|
+
resolveTabLabel(tab: NestedTabConfig): string;
|
|
47
227
|
submit(): Promise<void>;
|
|
48
228
|
reset(): void;
|
|
49
|
-
getControl(fieldId: string): import("@angular/forms").AbstractControl<any, any>;
|
|
50
229
|
static ɵfac: i0.ɵɵFactoryDeclaration<DynamicFormComponent, never>;
|
|
51
|
-
static ɵcmp: i0.ɵɵComponentDeclaration<DynamicFormComponent, "ngx-dynamic-form", never, { "config": { "alias": "config"; "required": false; }; "initialData": { "alias": "initialData"; "required": false; }; "userRoles": { "alias": "userRoles"; "required": false; }; "language": { "alias": "language"; "required": false; }; "readonly": { "alias": "readonly"; "required": false; }; "loading": { "alias": "loading"; "required": false; }; "error": { "alias": "error"; "required": false; }; }, { "formSubmit": "formSubmit"; "formChange": "formChange"; "formReset": "formReset"; }, never, never, true, never>;
|
|
230
|
+
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>;
|
|
52
231
|
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { EventEmitter, OnChanges, SimpleChanges } from '@angular/core';
|
|
2
|
+
import { FormGroup } from '@angular/forms';
|
|
3
|
+
import type { EntityFormConfig, FormRule, NestedFieldConfig } from '@dynamic-entity/core';
|
|
4
|
+
import { DynamicFormComponent } from './dynamic-form.component';
|
|
5
|
+
import * as i0 from "@angular/core";
|
|
6
|
+
/**
|
|
7
|
+
* DynamicRecordFormComponent — comprehensive tabbed record view & edit component.
|
|
8
|
+
* Supports cross-tab rule evaluation, profile header, summary drawer (showOnMinimize),
|
|
9
|
+
* interactive quick-jump links, and baseline modification tracking.
|
|
10
|
+
*/
|
|
11
|
+
export declare class DynamicRecordFormComponent implements OnChanges {
|
|
12
|
+
config: EntityFormConfig;
|
|
13
|
+
rules?: FormRule[];
|
|
14
|
+
initialData?: Record<string, any>;
|
|
15
|
+
userRoles: string[];
|
|
16
|
+
language: string;
|
|
17
|
+
readonly: boolean;
|
|
18
|
+
loading: boolean;
|
|
19
|
+
error: string | null;
|
|
20
|
+
/** Whole record read-only, regardless of RBAC. Equivalent to the reference's `isReadOnly`. */
|
|
21
|
+
isReadOnly: boolean;
|
|
22
|
+
/** Specific field ids forced read-only while the rest of the record stays editable. */
|
|
23
|
+
readOnlyFields: string[];
|
|
24
|
+
/**
|
|
25
|
+
* Open the record read-only, with a per-tab "Edit section" flow — one tab is edited and
|
|
26
|
+
* validated at a time. This is the framework's `EntityRecordComponent` model and the
|
|
27
|
+
* default here.
|
|
28
|
+
*
|
|
29
|
+
* Set false for a directly editable record with no view/edit distinction.
|
|
30
|
+
*/
|
|
31
|
+
viewMode: boolean;
|
|
32
|
+
formSubmit: EventEmitter<Record<string, any>>;
|
|
33
|
+
formChange: EventEmitter<Record<string, any>>;
|
|
34
|
+
formReset: EventEmitter<void>;
|
|
35
|
+
/** One tab section was saved. Carries the whole record, plus which tab was edited. */
|
|
36
|
+
sectionSave: EventEmitter<{
|
|
37
|
+
tabId: string;
|
|
38
|
+
record: Record<string, any>;
|
|
39
|
+
}>;
|
|
40
|
+
dynamicFormComp?: DynamicFormComponent;
|
|
41
|
+
private readonly rulesEvaluation;
|
|
42
|
+
readonly currentData: import("@angular/core").WritableSignal<Record<string, any>>;
|
|
43
|
+
readonly originalBaseline: import("@angular/core").WritableSignal<Record<string, any>>;
|
|
44
|
+
/**
|
|
45
|
+
* Info banners the user has dismissed, by field id.
|
|
46
|
+
*
|
|
47
|
+
* The reference's contract: an info banner persists until dismissed, **not** until the
|
|
48
|
+
* value changes. Dismissal is keyed by field so re-triggering a different field's rule
|
|
49
|
+
* still shows. Re-armed when a different record is loaded.
|
|
50
|
+
*/
|
|
51
|
+
private readonly dismissed;
|
|
52
|
+
/**
|
|
53
|
+
* Info banners from the rules engine that are still showing.
|
|
54
|
+
*
|
|
55
|
+
* A getter, not a `computed`: the source is `dynamicFormComp`, a ViewChild. A computed
|
|
56
|
+
* evaluated before the view initialises would capture no dependency on the child's
|
|
57
|
+
* `ruleResult` signal and then never recompute. Read per change-detection pass instead.
|
|
58
|
+
*/
|
|
59
|
+
get infoBanners(): {
|
|
60
|
+
fieldId: string;
|
|
61
|
+
message: string;
|
|
62
|
+
}[];
|
|
63
|
+
dismissInfoBanner(fieldId: string): void;
|
|
64
|
+
/** True when nothing in the record may be edited. */
|
|
65
|
+
get recordReadOnly(): boolean;
|
|
66
|
+
/**
|
|
67
|
+
* A row of an `array` field is edited in a drawer, not inline in the tab panel.
|
|
68
|
+
*
|
|
69
|
+
* The drawer is rendered outside the tab container on purpose: the reference found that
|
|
70
|
+
* building a row's FormGroup inside the tab view races the tab's own initialisation, so
|
|
71
|
+
* the row form is kept detached and only pushed into the FormArray on save.
|
|
72
|
+
*/
|
|
73
|
+
readonly inlineRowField: import("@angular/core").WritableSignal<any>;
|
|
74
|
+
readonly inlineRowIndex: import("@angular/core").WritableSignal<number>;
|
|
75
|
+
inlineRowForm: FormGroup | null;
|
|
76
|
+
/** `array` fields on the tab currently shown, so the template can list their rows. */
|
|
77
|
+
get arrayFieldsForActiveTab(): NestedFieldConfig[];
|
|
78
|
+
/**
|
|
79
|
+
* Rows currently held by an `array` field, read from the record value.
|
|
80
|
+
*
|
|
81
|
+
* Deliberately not read off the child's FormArray: that control only exists once the
|
|
82
|
+
* child has initialised, so rendering from it changed value mid-pass. `currentData` is a
|
|
83
|
+
* signal seeded from `initialData` and kept current by the child's `formChange`.
|
|
84
|
+
*/
|
|
85
|
+
rowsOf(field: NestedFieldConfig): Record<string, unknown>[];
|
|
86
|
+
/** One row rendered as text, so the list is readable without opening the drawer. */
|
|
87
|
+
rowSummary(field: NestedFieldConfig, row: Record<string, unknown>): string;
|
|
88
|
+
openAddRow(field: NestedFieldConfig): void;
|
|
89
|
+
openEditRow(field: NestedFieldConfig, index: number): void;
|
|
90
|
+
cancelRow(): void;
|
|
91
|
+
/** Commit the drawer into the FormArray. Rules re-evaluate off the resulting value change. */
|
|
92
|
+
saveRow(): void;
|
|
93
|
+
deleteRow(field: NestedFieldConfig, index: number): void;
|
|
94
|
+
/**
|
|
95
|
+
* The tab currently open for editing, or null when the record is being viewed.
|
|
96
|
+
*
|
|
97
|
+
* The reference edits one tab section at a time rather than the whole record, and
|
|
98
|
+
* validates only that section on save — a required field on an untouched tab must not
|
|
99
|
+
* block saving the tab you are actually working on.
|
|
100
|
+
*/
|
|
101
|
+
readonly editingTabId: import("@angular/core").WritableSignal<string>;
|
|
102
|
+
/**
|
|
103
|
+
* The tab the inner form is showing.
|
|
104
|
+
*
|
|
105
|
+
* Tracked here rather than read off the ViewChild. The child resolves its first tab as
|
|
106
|
+
* part of its own initialisation, so reading it during this component's render changed
|
|
107
|
+
* value mid-pass — an ExpressionChangedAfterItHasBeenChecked error. Seeded from the
|
|
108
|
+
* config and kept in step by the child's `activeTabChange` output.
|
|
109
|
+
*/
|
|
110
|
+
private readonly activeTabIdSig;
|
|
111
|
+
get activeTabId(): string | null;
|
|
112
|
+
onActiveTabChange(tabId: string): void;
|
|
113
|
+
/** First tab a user can see, used to seed the active tab before the child reports one. */
|
|
114
|
+
private firstVisibleTabId;
|
|
115
|
+
/** Errors from the last save attempt, by field id. */
|
|
116
|
+
readonly sectionErrors: import("@angular/core").WritableSignal<Record<string, string>>;
|
|
117
|
+
/** Template access to `Object.keys` for iterating the error map. */
|
|
118
|
+
protected readonly Object: ObjectConstructor;
|
|
119
|
+
get isEditingActiveTab(): boolean;
|
|
120
|
+
/** Fields render read-only unless their own tab is the one being edited. */
|
|
121
|
+
get sectionReadOnly(): boolean;
|
|
122
|
+
editSection(): void;
|
|
123
|
+
/** Discard this section's edits, restoring each field from the session baseline. */
|
|
124
|
+
cancelSection(): void;
|
|
125
|
+
/**
|
|
126
|
+
* Validate this section only, then emit it. Validation is scoped two ways: Angular
|
|
127
|
+
* validity of the tab's own controls, and rules filtered to this tab — a rule targeting
|
|
128
|
+
* a hidden tab must not block the save (the reference's OV0-968 fix).
|
|
129
|
+
*/
|
|
130
|
+
saveSection(): void;
|
|
131
|
+
/**
|
|
132
|
+
* Seed the session baseline from the loaded record so `VALUE_CHANGED` rules and the
|
|
133
|
+
* modification banner compare against the record as it was opened — not as it was
|
|
134
|
+
* after the first keystroke.
|
|
135
|
+
*/
|
|
136
|
+
ngOnChanges(changes: SimpleChanges): void;
|
|
137
|
+
get recordTitle(): string;
|
|
138
|
+
get avatarLetter(): string;
|
|
139
|
+
/** Every field in the config, at any depth. */
|
|
140
|
+
private allFields;
|
|
141
|
+
/** The field flagged `isProfileImage`, rendered as the record avatar. */
|
|
142
|
+
get headerProfileField(): NestedFieldConfig | null;
|
|
143
|
+
/** The field flagged `isHeaderToggle`, rendered as a status switch in the header. */
|
|
144
|
+
get headerToggleField(): NestedFieldConfig | null;
|
|
145
|
+
/** Avatar image URL, when the profile field holds a persisted FileRef. */
|
|
146
|
+
get profileImageUrl(): string | null;
|
|
147
|
+
get headerToggleValue(): boolean;
|
|
148
|
+
/**
|
|
149
|
+
* Flip the header toggle. It writes straight to the control rather than going through
|
|
150
|
+
* section editing — the reference treats it as a record-level status switch, not part of
|
|
151
|
+
* any one tab's section.
|
|
152
|
+
*/
|
|
153
|
+
toggleHeaderStatus(): void;
|
|
154
|
+
/** Locate a field's control wherever it lives, since header fields may be on any tab. */
|
|
155
|
+
private controlFor;
|
|
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
|
+
*/
|
|
168
|
+
private fieldValue;
|
|
169
|
+
readonly isModified: import("@angular/core").Signal<boolean>;
|
|
170
|
+
readonly summaryFields: import("@angular/core").Signal<NestedFieldConfig[]>;
|
|
171
|
+
onFormChange(data: Record<string, any>): void;
|
|
172
|
+
onFormSubmit(data: Record<string, any>): void;
|
|
173
|
+
onFormReset(): void;
|
|
174
|
+
formatFieldLabel(field: NestedFieldConfig): string;
|
|
175
|
+
/** Summary values render through the shared core formatter, not a local stringifier. */
|
|
176
|
+
formatFieldValue(field: NestedFieldConfig): string;
|
|
177
|
+
jumpToField(fieldId: string): void;
|
|
178
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<DynamicRecordFormComponent, never>;
|
|
179
|
+
static ɵcmp: i0.ɵɵComponentDeclaration<DynamicRecordFormComponent, "ngx-dynamic-record-form", never, { "config": { "alias": "config"; "required": false; }; "rules": { "alias": "rules"; "required": false; }; "initialData": { "alias": "initialData"; "required": false; }; "userRoles": { "alias": "userRoles"; "required": false; }; "language": { "alias": "language"; "required": false; }; "readonly": { "alias": "readonly"; "required": false; }; "loading": { "alias": "loading"; "required": false; }; "error": { "alias": "error"; "required": false; }; "isReadOnly": { "alias": "isReadOnly"; "required": false; }; "readOnlyFields": { "alias": "readOnlyFields"; "required": false; }; "viewMode": { "alias": "viewMode"; "required": false; }; }, { "formSubmit": "formSubmit"; "formChange": "formChange"; "formReset": "formReset"; "sectionSave": "sectionSave"; }, never, never, true, never>;
|
|
180
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { EnvironmentProviders, Type } from '@angular/core';
|
|
2
|
+
/**
|
|
3
|
+
* Field-type registration — the tree-shaking seam.
|
|
4
|
+
*
|
|
5
|
+
* `FieldRegistryService` imports no components. Whatever you register here is what gets
|
|
6
|
+
* bundled, so an app that only uses text/number/dropdown pays for three components and
|
|
7
|
+
* their imports, not nineteen.
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* // Everything (convenient; bundles all built-ins):
|
|
11
|
+
* provideNgxDynamicEntity(), provideBuiltInFieldTypes()
|
|
12
|
+
*
|
|
13
|
+
* // Only what this app renders:
|
|
14
|
+
* provideFieldTypes({ text: TextFieldComponent, number: NumberFieldComponent })
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* Both merge into `FIELD_TYPE_REGISTRY`; later registrations win on key collision.
|
|
18
|
+
*/
|
|
19
|
+
/** Map of every built-in field type key → component. Referencing this pulls in all of them. */
|
|
20
|
+
export declare function builtInFieldTypes(): Record<string, Type<any>>;
|
|
21
|
+
/** Register a specific set of field types. Only the components you name are bundled. */
|
|
22
|
+
export declare function provideFieldTypes(types: Record<string, Type<any>>): EnvironmentProviders;
|
|
23
|
+
/** Register all 19 built-in field type keys. Convenience for apps that use most of them. */
|
|
24
|
+
export declare function provideBuiltInFieldTypes(): EnvironmentProviders;
|
|
@@ -1,19 +1,31 @@
|
|
|
1
1
|
import { EnvironmentProviders, Type } from '@angular/core';
|
|
2
|
+
import type { EntityReferenceLoader, LookupListSource, RecordMigration } from '@dynamic-entity/core';
|
|
3
|
+
import { type HookFn } from '../tokens/injection-tokens';
|
|
2
4
|
export interface NgxDynamicEntityConfig {
|
|
3
|
-
/** Base URL of the dynamic-entity-server API. Example: 'http://localhost:3000/api/entities' */
|
|
4
|
-
apiUrl?: string;
|
|
5
5
|
/** Roles that see masked field values as XXXXXXXXX */
|
|
6
6
|
maskedRoles?: string[];
|
|
7
|
-
/** Migration strategy: 'strict' rejects stale records, 'graceful' flags them. Never 'auto' (ADR-005). */
|
|
8
|
-
migrationStrategy?: 'strict' | 'graceful';
|
|
9
7
|
/** Custom field type components keyed by type string */
|
|
10
8
|
fieldTypes?: Record<string, Type<any>>;
|
|
11
|
-
/**
|
|
12
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Entity-ref option loaders keyed by entity key (ADR-006).
|
|
11
|
+
* Each receives `{ parentValue, filters, lang }` and may return an array, Promise, or Observable.
|
|
12
|
+
*/
|
|
13
|
+
entityRefs?: Record<string, EntityReferenceLoader>;
|
|
14
|
+
/**
|
|
15
|
+
* Named master lists keyed by list name, for fields that set `listName`.
|
|
16
|
+
* Each may be the values themselves (array/Promise/Observable) or a loader function — prefer
|
|
17
|
+
* a loader for anything fetched, so an unused list is never loaded.
|
|
18
|
+
*/
|
|
19
|
+
lookups?: Record<string, LookupListSource>;
|
|
13
20
|
/** Custom validator functions keyed by validator name */
|
|
14
21
|
validators?: Record<string, any>;
|
|
15
22
|
/** Hook functions keyed by hook name */
|
|
16
|
-
hooks?: Record<string,
|
|
23
|
+
hooks?: Record<string, HookFn>;
|
|
24
|
+
/**
|
|
25
|
+
* Ordered steps that upgrade a saved record when `EntityFormConfig.version` moves ahead
|
|
26
|
+
* of the record's `_configVersion`. Applied to `initialData` before the form is patched.
|
|
27
|
+
*/
|
|
28
|
+
migrations?: RecordMigration[];
|
|
17
29
|
}
|
|
18
30
|
/**
|
|
19
31
|
* Provider function for ngx-dynamic-entity.
|
|
@@ -21,7 +33,6 @@ export interface NgxDynamicEntityConfig {
|
|
|
21
33
|
*
|
|
22
34
|
* Usage in app.config.ts:
|
|
23
35
|
* provideNgxDynamicEntity({
|
|
24
|
-
* apiUrl: 'http://localhost:3000/api/entities',
|
|
25
36
|
* maskedRoles: ['IT_SUPPORT'],
|
|
26
37
|
* entityRefs: { clients: () => clientService.getOptions() },
|
|
27
38
|
* })
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { EntityFormConfig, NestedFieldConfig, ReferenceLoaderContext, ReferenceOption } from '@dynamic-entity/core';
|
|
2
|
+
import * as i0 from "@angular/core";
|
|
3
|
+
/**
|
|
4
|
+
* CascadeDataService — loads entity-reference options and applies parent→child cascades.
|
|
5
|
+
*
|
|
6
|
+
* Loaders come from the registry only (ADR-006). This service owns the three things a
|
|
7
|
+
* field component should not: resolving the loader, coercing whatever it returned
|
|
8
|
+
* (array | Promise | Observable) into a Promise, and filtering by the parent's value.
|
|
9
|
+
*/
|
|
10
|
+
export declare class CascadeDataService {
|
|
11
|
+
private readonly registry;
|
|
12
|
+
private readonly references;
|
|
13
|
+
/** Preload runs by formConfigId, so a second call joins the first rather than re-fetching. */
|
|
14
|
+
private readonly preloads;
|
|
15
|
+
/** Registry key for a field: explicit `linkedEntityKey`, else the field id. */
|
|
16
|
+
entityKeyFor(field: NestedFieldConfig): string;
|
|
17
|
+
/** Whether a loader is registered for this field. */
|
|
18
|
+
canLoad(field: NestedFieldConfig): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Load the options for a field, cascade-filtered against `parentValue`.
|
|
21
|
+
* Returns `[]` when no loader is registered, or when a cascade child has no parent value yet.
|
|
22
|
+
*/
|
|
23
|
+
load(field: NestedFieldConfig, ctx?: ReferenceLoaderContext): Promise<ReferenceOption[]>;
|
|
24
|
+
/**
|
|
25
|
+
* Warm the cache for every entity a config's cascades depend on, so changing a parent
|
|
26
|
+
* resolves from memory instead of a round-trip.
|
|
27
|
+
*
|
|
28
|
+
* Idempotent per `formConfigId`: calling it again returns the in-flight or completed run
|
|
29
|
+
* rather than re-fetching.
|
|
30
|
+
*/
|
|
31
|
+
initializeCascadeData(formConfigId: string, config: EntityFormConfig, lang?: string): Promise<void>;
|
|
32
|
+
/** Resolves true once `initializeCascadeData` for this config has finished. */
|
|
33
|
+
waitForData(formConfigId: string): Promise<boolean>;
|
|
34
|
+
/**
|
|
35
|
+
* Child options for a parent selection, from cache only — no load is triggered.
|
|
36
|
+
* Returns `[]` when the entity has not been preloaded.
|
|
37
|
+
*/
|
|
38
|
+
getCachedChildOptions(field: NestedFieldConfig, parentValue: unknown, lang?: string): ReferenceOption[];
|
|
39
|
+
/** Forget the preload for one config. The option cache itself is untouched. */
|
|
40
|
+
clearCache(formConfigId: string): void;
|
|
41
|
+
/** Every entity-ref field in the config that participates in a cascade. */
|
|
42
|
+
private cascadeFields;
|
|
43
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<CascadeDataService, never>;
|
|
44
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<CascadeDataService>;
|
|
45
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { EntityFormConfig } from '@dynamic-entity/core';
|
|
2
|
+
import * as i0 from "@angular/core";
|
|
3
|
+
export declare class ConfigSourceService {
|
|
4
|
+
private readonly cache;
|
|
5
|
+
private readonly inFlight;
|
|
6
|
+
private readonly handler?;
|
|
7
|
+
constructor(handler?: any);
|
|
8
|
+
/** Retrieve an entity config by key, with caching and in-flight deduplication. */
|
|
9
|
+
getConfig(entityKey: string): Promise<EntityFormConfig | undefined>;
|
|
10
|
+
/** Clear cached configs (e.g. after a config edit in the builder). */
|
|
11
|
+
clearCache(entityKey?: string): void;
|
|
12
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<ConfigSourceService, [{ optional: true; }]>;
|
|
13
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<ConfigSourceService>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ReferenceOption } from '@dynamic-entity/core';
|
|
2
|
+
/**
|
|
3
|
+
* Backing store for cached reference options.
|
|
4
|
+
*
|
|
5
|
+
* Pluggable on purpose. The reference implementation kept its cache in a service instance,
|
|
6
|
+
* so every entity label was re-fetched on a browser refresh (issue #22 in the architecture
|
|
7
|
+
* doc). A consumer that wants the cache to survive a reload can back this with
|
|
8
|
+
* sessionStorage; the default keeps it in memory, which is the right call for a library
|
|
9
|
+
* that must not silently persist a tenant's data.
|
|
10
|
+
*/
|
|
11
|
+
export interface EntityRefCacheStore {
|
|
12
|
+
get(key: string): ReferenceOption[] | undefined;
|
|
13
|
+
set(key: string, value: ReferenceOption[]): void;
|
|
14
|
+
delete(key: string): void;
|
|
15
|
+
/** Every key currently held — used for prefix invalidation of one entity. */
|
|
16
|
+
keys(): string[];
|
|
17
|
+
clear(): void;
|
|
18
|
+
}
|
|
19
|
+
/** Default in-memory store. Lives as long as the injector that created it. */
|
|
20
|
+
export declare class InMemoryEntityRefCacheStore implements EntityRefCacheStore {
|
|
21
|
+
private readonly map;
|
|
22
|
+
get(key: string): ReferenceOption[] | undefined;
|
|
23
|
+
set(key: string, value: ReferenceOption[]): void;
|
|
24
|
+
delete(key: string): void;
|
|
25
|
+
keys(): string[];
|
|
26
|
+
clear(): void;
|
|
27
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { EntityReferenceLoader } from '@dynamic-entity/core';
|
|
1
2
|
import * as i0 from "@angular/core";
|
|
2
3
|
/**
|
|
3
4
|
* EntityRefRegistryService — resolves entity-ref option loader functions.
|
|
@@ -10,10 +11,10 @@ import * as i0 from "@angular/core";
|
|
|
10
11
|
export declare class EntityRefRegistryService {
|
|
11
12
|
private readonly registry;
|
|
12
13
|
/**
|
|
13
|
-
* Get the option loader
|
|
14
|
-
* @returns
|
|
14
|
+
* Get the option loader for an entity key.
|
|
15
|
+
* @returns The loader, or null if none is registered.
|
|
15
16
|
*/
|
|
16
|
-
resolve(entityKey: string):
|
|
17
|
+
resolve(entityKey: string): EntityReferenceLoader | null;
|
|
17
18
|
has(entityKey: string): boolean;
|
|
18
19
|
static ɵfac: i0.ɵɵFactoryDeclaration<EntityRefRegistryService, never>;
|
|
19
20
|
static ɵprov: i0.ɵɵInjectableDeclaration<EntityRefRegistryService>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ReferenceOption } from '@dynamic-entity/core';
|
|
2
|
+
import * as i0 from "@angular/core";
|
|
3
|
+
export interface EntityRefSelection {
|
|
4
|
+
fieldId: string;
|
|
5
|
+
/** The chosen option, or `null` when the selection was cleared. */
|
|
6
|
+
option: ReferenceOption | null;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* EntityRefSelectionService — the channel an entity-ref field uses to publish *which record*
|
|
10
|
+
* the user picked, so the owning form can run `autoPatch` against it.
|
|
11
|
+
*
|
|
12
|
+
* The control value alone is not enough: `autoPatch` copies fields out of the selected
|
|
13
|
+
* record, which only the field component has after loading its options.
|
|
14
|
+
*
|
|
15
|
+
* Provided at `DynamicFormComponent` level so each form instance gets its own channel;
|
|
16
|
+
* the `root` fallback keeps standalone field components injectable in isolation.
|
|
17
|
+
*/
|
|
18
|
+
export declare class EntityRefSelectionService {
|
|
19
|
+
private readonly subject;
|
|
20
|
+
readonly selection$: import("rxjs").Observable<EntityRefSelection>;
|
|
21
|
+
emit(fieldId: string, option: ReferenceOption | null): void;
|
|
22
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<EntityRefSelectionService, never>;
|
|
23
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<EntityRefSelectionService>;
|
|
24
|
+
}
|