@moatamed-sce/form-engine 0.1.5
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.
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
import { FormGroup, NonNullableFormBuilder, AbstractControl, ValidatorFn } from '@angular/forms';
|
|
2
|
+
import { FormDefinition, FormArrayNode, FormFieldType, FormField, Condition, I18nText, DataSourceRef, FormPage, NamedValidator, NamedValidatorName } from '@moatamed-sce/form-schema';
|
|
3
|
+
import { InjectionToken } from '@angular/core';
|
|
4
|
+
import { Observable } from 'rxjs';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Growing and shrinking a repeater.
|
|
8
|
+
*
|
|
9
|
+
* `buildFormGroup` already produces a `FormArray` for an `array` node and seeds it to `minItems`,
|
|
10
|
+
* and there it stopped: nothing could add a row or take one away, so a repeater rendered a fixed
|
|
11
|
+
* number of rows forever and `maxItems` described a limit nothing enforced. The capability was
|
|
12
|
+
* half-built, which is worse than absent — a definition could declare `minItems: 1, maxItems: 5`
|
|
13
|
+
* and get neither.
|
|
14
|
+
*
|
|
15
|
+
* Both bounds are enforced here rather than as validators on the array. A validator would let the
|
|
16
|
+
* user delete the last row and *then* tell them the form is invalid; refusing the removal keeps
|
|
17
|
+
* the form in a state the definition allows at every moment, and lets the renderer disable the
|
|
18
|
+
* button instead of rendering an error. {@link canAddArrayRow} and {@link canRemoveArrayRow} are
|
|
19
|
+
* that button's enabled state.
|
|
20
|
+
*
|
|
21
|
+
* Nothing throws. A path that names no repeater is an authoring mistake in the caller, and it is
|
|
22
|
+
* reported the way the rest of the engine reports one — in the result, with the form untouched.
|
|
23
|
+
*
|
|
24
|
+
* **Run `applyRuleState` after any change here.** Row state is keyed by index, so removing row 0
|
|
25
|
+
* makes what was row 1 into row 0 and every state below the cut is now describing its neighbour
|
|
26
|
+
* — a hidden field reappears one row up. A new row likewise has only its static state until a
|
|
27
|
+
* rule pass reads the values it was seeded with. Callers driven by a value-change effect get
|
|
28
|
+
* this for free, because both operations move `getRawValue()`.
|
|
29
|
+
*/
|
|
30
|
+
/** Why a row could not be added or removed. */
|
|
31
|
+
type ArrayRowRefusal =
|
|
32
|
+
/** The path names no `array` node, or the group holds no `FormArray` there. */
|
|
33
|
+
'no-such-array'
|
|
34
|
+
/** Already at `maxItems`. */
|
|
35
|
+
| 'at-max'
|
|
36
|
+
/** Already at `minItems`. */
|
|
37
|
+
| 'at-min'
|
|
38
|
+
/** The index is outside the rows that exist. */
|
|
39
|
+
| 'no-such-row';
|
|
40
|
+
/** What happened to the repeater. */
|
|
41
|
+
interface ArrayRowResult {
|
|
42
|
+
readonly changed: boolean;
|
|
43
|
+
/** Row count after the attempt — unchanged when `changed` is false. */
|
|
44
|
+
readonly rows: number;
|
|
45
|
+
/** Set when `changed` is false. */
|
|
46
|
+
readonly refused?: ArrayRowRefusal;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The `array` node a control path names.
|
|
50
|
+
*
|
|
51
|
+
* Arrays nested inside arrays are not walked: their controls are indexed (`partners.0.phones`),
|
|
52
|
+
* and v1 has no way to address one. A definition that nests them still builds — only this lookup
|
|
53
|
+
* declines to find the inner one.
|
|
54
|
+
*/
|
|
55
|
+
declare function arrayNodeAt(definition: FormDefinition, path: string): FormArrayNode | undefined;
|
|
56
|
+
/** True when the repeater has room for another row. An absent `maxItems` means no ceiling. */
|
|
57
|
+
declare function canAddArrayRow(definition: FormDefinition, group: FormGroup, path: string): boolean;
|
|
58
|
+
/** True when a row can be taken away without dropping below `minItems`. */
|
|
59
|
+
declare function canRemoveArrayRow(definition: FormDefinition, group: FormGroup, path: string): boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Appends a row, seeded from `initial` where it has values for the row's fields.
|
|
62
|
+
*
|
|
63
|
+
* The row is built by the same code that built the ones already there, so a field's coercion and
|
|
64
|
+
* validators cannot drift between "was in the initial data" and "was added by the user".
|
|
65
|
+
*/
|
|
66
|
+
declare function addArrayRow(definition: FormDefinition, group: FormGroup, path: string, fb: NonNullableFormBuilder, initial?: Readonly<Record<string, unknown>>): ArrayRowResult;
|
|
67
|
+
/**
|
|
68
|
+
* Removes one row.
|
|
69
|
+
*
|
|
70
|
+
* A missing row is reported before the `minItems` check, so a stale index from a double click
|
|
71
|
+
* does not come back as "you cannot remove any more rows" — which would be a lie the user would
|
|
72
|
+
* act on.
|
|
73
|
+
*/
|
|
74
|
+
declare function removeArrayRow(definition: FormDefinition, group: FormGroup, path: string, index: number): ArrayRowResult;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The value shape each control type holds.
|
|
78
|
+
*
|
|
79
|
+
* A `FormGroup` built from a definition has to be typed before any data arrives, and getting
|
|
80
|
+
* this wrong is subtle: a multi-select seeded with `''` instead of `[]` makes `value.length`
|
|
81
|
+
* read 0 either way, then throws the first time something calls `.map`. A currency field seeded
|
|
82
|
+
* with `''` submits an empty string where the API expects a number or null.
|
|
83
|
+
*/
|
|
84
|
+
/** Types whose value is an array rather than a scalar. */
|
|
85
|
+
declare const MULTI_VALUE_TYPES: ReadonlySet<FormFieldType>;
|
|
86
|
+
/** The empty value a type holds before any input. */
|
|
87
|
+
declare function typeFallback(type: FormFieldType): unknown;
|
|
88
|
+
/** Coerces a provided value into the shape the type expects. */
|
|
89
|
+
declare function coerceValue(type: FormFieldType, provided: unknown): unknown;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Rule evaluation.
|
|
93
|
+
*
|
|
94
|
+
* No `eval`, no `new Function`, no expression parser — a closed operator set walked recursively.
|
|
95
|
+
* A definition may arrive from an API, so the evaluator has to be safe against a hostile
|
|
96
|
+
* document by construction rather than by sanitising one.
|
|
97
|
+
*/
|
|
98
|
+
/** What the rules currently say about one field. */
|
|
99
|
+
interface FieldRuleState {
|
|
100
|
+
readonly hidden: boolean;
|
|
101
|
+
readonly disabled: boolean;
|
|
102
|
+
readonly readonly: boolean;
|
|
103
|
+
/** `true` when a rule added a requirement, `false` when one removed it, `undefined` when rules are silent. */
|
|
104
|
+
readonly required?: boolean;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Rule state for every field, keyed by the **control path** that addresses it.
|
|
108
|
+
*
|
|
109
|
+
* For a flat definition a path is just the field's key, so nothing changes. Inside a group it is
|
|
110
|
+
* dotted — `riyadh.q-5` — which is what `FormGroup.get` needs. Keying by the bare key instead
|
|
111
|
+
* meant a rule silently did nothing the moment a definition used groups: `group.get('q-5')`
|
|
112
|
+
* returns null when the control lives at `riyadh.q-5`, and the follow-up it was meant to reveal
|
|
113
|
+
* stayed hidden with nothing to say so.
|
|
114
|
+
*/
|
|
115
|
+
type RuleState = ReadonlyMap<string, FieldRuleState>;
|
|
116
|
+
/**
|
|
117
|
+
* The repeater row a field lives in.
|
|
118
|
+
*
|
|
119
|
+
* Rules are authored against a definition, which knows the *shape* of a row and never how many
|
|
120
|
+
* of them exist — so an effect naming `crNumber` has to mean "that field, in whichever row is
|
|
121
|
+
* being resolved". The prefix is what turns it into a control path, and `keys` is what decides
|
|
122
|
+
* whether a condition is asking about this row or about the form.
|
|
123
|
+
*/
|
|
124
|
+
interface RowScope {
|
|
125
|
+
/** Control path of the row group — `partners.0`. */
|
|
126
|
+
readonly prefix: string;
|
|
127
|
+
/** Field paths declared inside one row, relative to {@link prefix}. */
|
|
128
|
+
readonly keys: ReadonlySet<string>;
|
|
129
|
+
}
|
|
130
|
+
/** A field and the control path that reaches it. */
|
|
131
|
+
interface FieldPath {
|
|
132
|
+
readonly field: FormField;
|
|
133
|
+
readonly path: string;
|
|
134
|
+
/** Set when the field is inside a repeater row, which is what scopes a rule to that row. */
|
|
135
|
+
readonly row?: RowScope;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Every field a rule can address, with its control path.
|
|
139
|
+
*
|
|
140
|
+
* Repeater rows are walked only when `values` are supplied, because how many rows exist is a
|
|
141
|
+
* fact about the data and never about the definition — the same reason `buildFormGroup` seeds
|
|
142
|
+
* `max(rows.length, minItems)` rather than a number the schema could state. Called without
|
|
143
|
+
* values this returns the static tree, which is what a caller wants when it needs the fields a
|
|
144
|
+
* definition declares rather than the controls a form currently holds.
|
|
145
|
+
*/
|
|
146
|
+
declare function fieldPaths(definition: FormDefinition, values?: Readonly<Record<string, unknown>>): FieldPath[];
|
|
147
|
+
/**
|
|
148
|
+
* Evaluates a condition against the live form values.
|
|
149
|
+
*
|
|
150
|
+
* `scope` is the row a rule is being resolved for. Inside one, a condition naming a field the
|
|
151
|
+
* row declares reads *that row's* answer — which is the only reading that lets "when this
|
|
152
|
+
* partner is a company, require this partner's CR number" be written at all. Every other field
|
|
153
|
+
* it names is read from the form, so a row can still depend on a question asked once above it.
|
|
154
|
+
*/
|
|
155
|
+
declare function evaluateCondition(condition: Condition, values: Readonly<Record<string, unknown>>, scope?: RowScope): boolean;
|
|
156
|
+
/**
|
|
157
|
+
* Resolves every field's visibility, enablement and requirement for the current values.
|
|
158
|
+
*
|
|
159
|
+
* Starts from the definition's static state — `hiddenByDefault` — then applies each matching
|
|
160
|
+
* rule in document order, so a later rule wins over an earlier one. That ordering is the reason
|
|
161
|
+
* rules live on the definition rather than the field: it is deterministic and reviewable in one
|
|
162
|
+
* place, where a per-field predicate is neither.
|
|
163
|
+
*
|
|
164
|
+
* A rule that reaches into a repeater is resolved **once per row**, against that row's answers.
|
|
165
|
+
* Evaluating the condition once for the whole form instead would make every row share one
|
|
166
|
+
* verdict — the first partner declared a company would require a CR number from all of them.
|
|
167
|
+
*/
|
|
168
|
+
declare function evaluateRules(definition: FormDefinition, values: Readonly<Record<string, unknown>>): RuleState;
|
|
169
|
+
/** Convenience default for a field the rules never mention. */
|
|
170
|
+
declare const DEFAULT_FIELD_RULE_STATE: FieldRuleState;
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Where a select's options come from at runtime.
|
|
174
|
+
*
|
|
175
|
+
* A definition can already say `{ type: 'lookup', lookupKey: 'cities' }`, and until now nothing
|
|
176
|
+
* turned that into options — `formDefinitionToConfig` renders an empty list and says so in a
|
|
177
|
+
* comment. This is the seam that closes it, and the shape of the seam is the whole point:
|
|
178
|
+
*
|
|
179
|
+
* - **The engine never fetches.** It is `type:core` and must not know `HttpClient` exists. A
|
|
180
|
+
* feature implements this interface over whatever it already uses — `LookupApiService` here
|
|
181
|
+
* returns promises, so `from()` is the adapter — and provides it at the route.
|
|
182
|
+
* - **A lookup is a key, never a URL.** Enforced at the schema boundary already; restated here
|
|
183
|
+
* because the port is the place an endpoint would otherwise sneak back in. A host maps keys
|
|
184
|
+
* it recognises and refuses the rest, so a definition from an API cannot make the app fetch
|
|
185
|
+
* from an arbitrary origin.
|
|
186
|
+
* - **Only the declared dependency is passed.** A dependent dropdown needs its parent's value
|
|
187
|
+
* and nothing else. Handing the resolver the whole value record would send every answer the
|
|
188
|
+
* user has typed — a national ID, a salary — to whichever service happens to serve city
|
|
189
|
+
* names.
|
|
190
|
+
* - **Static sources never reach the port.** They are data on the document; routing them
|
|
191
|
+
* through an injected service would make a form that renders perfectly offline fail because
|
|
192
|
+
* a provider was missing.
|
|
193
|
+
*/
|
|
194
|
+
/** One option a select-like field offers. Mirrors the shape a static `DataSourceRef` carries. */
|
|
195
|
+
interface FormOption {
|
|
196
|
+
readonly value: string;
|
|
197
|
+
readonly label: I18nText;
|
|
198
|
+
}
|
|
199
|
+
/** The control a dependent source reads, and what it currently holds. */
|
|
200
|
+
interface FormDataSourceDependency {
|
|
201
|
+
/** Control key named by `DataSourceRef.dependsOn`. */
|
|
202
|
+
readonly field: string;
|
|
203
|
+
readonly value: unknown;
|
|
204
|
+
}
|
|
205
|
+
/** What the host is asked to resolve. */
|
|
206
|
+
interface FormDataSourceRequest {
|
|
207
|
+
/** The definition's `lookupKey` — a key the host maps to an endpoint, never a URL. */
|
|
208
|
+
readonly lookupKey: string;
|
|
209
|
+
/** The definition asking, so a host serving several forms can scope caching or permissions. */
|
|
210
|
+
readonly definitionId: string;
|
|
211
|
+
/** Present only when the source declares `dependsOn`. */
|
|
212
|
+
readonly dependsOn?: FormDataSourceDependency;
|
|
213
|
+
}
|
|
214
|
+
/** The port a feature implements so the engine never has to know about HTTP. */
|
|
215
|
+
interface FormDataSource {
|
|
216
|
+
options(request: FormDataSourceRequest): Observable<readonly FormOption[]>;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Provided in **features**, never in `ui-components`.
|
|
220
|
+
*
|
|
221
|
+
* The renderer injects it `{ optional: true }`: a form built entirely from static sources has
|
|
222
|
+
* no reason to force every host that renders one to supply a resolver.
|
|
223
|
+
*/
|
|
224
|
+
declare const FORM_DATA_SOURCE: InjectionToken<FormDataSource>;
|
|
225
|
+
/** The outcome of resolving one field's data source. */
|
|
226
|
+
interface ResolvedDataSource {
|
|
227
|
+
/** Options as they arrive. Emits once and completes for a static source. */
|
|
228
|
+
readonly options$: Observable<readonly FormOption[]>;
|
|
229
|
+
/**
|
|
230
|
+
* Why the list is empty, when it is empty for a reason worth surfacing.
|
|
231
|
+
*
|
|
232
|
+
* Follows `BuiltForm.unresolvedValidators`: the engine reports what it could not do and
|
|
233
|
+
* renders the rest, rather than throwing and blanking a page over one missing dropdown.
|
|
234
|
+
*/
|
|
235
|
+
readonly issue?: string;
|
|
236
|
+
}
|
|
237
|
+
/** The source a field's `dataSourceId` names, if the definition declares one. */
|
|
238
|
+
declare function dataSourceById(definition: FormDefinition, id: string | undefined): DataSourceRef | undefined;
|
|
239
|
+
/**
|
|
240
|
+
* Resolves the options for one data source id.
|
|
241
|
+
*
|
|
242
|
+
* `source` is the injected port, or `null` when the host provided none. Passing it in rather
|
|
243
|
+
* than injecting it here keeps this function callable from a test — and from a mapper — without
|
|
244
|
+
* an injection context.
|
|
245
|
+
*/
|
|
246
|
+
declare function resolveDataSource(definition: FormDefinition, dataSourceId: string | undefined, values: Readonly<Record<string, unknown>>, source: FormDataSource | null): ResolvedDataSource;
|
|
247
|
+
/**
|
|
248
|
+
* The value that decides whether a source must be re-fetched.
|
|
249
|
+
*
|
|
250
|
+
* A renderer re-resolves a dependent source when this changes and leaves every other source
|
|
251
|
+
* alone — refetching all of them on each keystroke is how a form ends up firing a request per
|
|
252
|
+
* character typed into an unrelated field.
|
|
253
|
+
*/
|
|
254
|
+
declare function dataSourceDependencyValue(definition: FormDefinition, dataSourceId: string | undefined, values: Readonly<Record<string, unknown>>): unknown;
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Wizard steps.
|
|
258
|
+
*
|
|
259
|
+
* `FormDefinition.steps` groups pages under a label, and until now nothing read it: a definition
|
|
260
|
+
* could describe a six-step service journey and the renderer drew all six pages stacked in one
|
|
261
|
+
* scroll. The schema said wizard and the runtime said long form.
|
|
262
|
+
*
|
|
263
|
+
* What lives here is the arithmetic a wizard needs and nothing else — which controls belong to a
|
|
264
|
+
* step, whether that step is answered, how far the user may go. Deliberately no chrome and no
|
|
265
|
+
* current-step state: the stepper is `sce-stepper`, and which step is showing is the host's, so
|
|
266
|
+
* that a wizard driven by a route parameter and one driven by a signal share one notion of
|
|
267
|
+
* "step 3 is not finished".
|
|
268
|
+
*/
|
|
269
|
+
/** Why a definition's steps could not be taken at face value. */
|
|
270
|
+
interface FormStepIssue {
|
|
271
|
+
/** The step or page the problem is about. */
|
|
272
|
+
readonly id: string;
|
|
273
|
+
readonly reason: string;
|
|
274
|
+
}
|
|
275
|
+
/** One step, resolved against the pages and controls it actually covers. */
|
|
276
|
+
interface FormStepView {
|
|
277
|
+
readonly id: string;
|
|
278
|
+
readonly index: number;
|
|
279
|
+
readonly title: I18nText;
|
|
280
|
+
readonly pages: readonly FormPage[];
|
|
281
|
+
/**
|
|
282
|
+
* Control paths this step holds, expanded against the current values.
|
|
283
|
+
*
|
|
284
|
+
* Repeater rows are included — `partners.0.name` — because a step containing a repeater is
|
|
285
|
+
* finished only when the rows the user actually added are.
|
|
286
|
+
*/
|
|
287
|
+
readonly paths: readonly string[];
|
|
288
|
+
}
|
|
289
|
+
/** Steps as the runtime sees them, with whatever the definition got wrong. */
|
|
290
|
+
interface ResolvedSteps {
|
|
291
|
+
readonly steps: readonly FormStepView[];
|
|
292
|
+
readonly issues: readonly FormStepIssue[];
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Resolves a definition's steps, or falls back to one step per page.
|
|
296
|
+
*
|
|
297
|
+
* The fallback is what makes a step-aware host safe to point at any definition: a form that
|
|
298
|
+
* declares no steps is a one-page-per-step wizard, which for the single-page case is exactly the
|
|
299
|
+
* flat form it already was. A host that had to branch on `definition.steps` being present would
|
|
300
|
+
* end up with two layouts to keep in step.
|
|
301
|
+
*
|
|
302
|
+
* A page no step claims is reported rather than appended. Appending it would invent a step the
|
|
303
|
+
* author never wrote; dropping it in silence would lose the questions on it. The caller has an
|
|
304
|
+
* issues list to render, which is how the rest of this platform reports a document it cannot
|
|
305
|
+
* take literally.
|
|
306
|
+
*/
|
|
307
|
+
declare function resolveSteps(definition: FormDefinition, values?: Readonly<Record<string, unknown>>): ResolvedSteps;
|
|
308
|
+
/**
|
|
309
|
+
* The controls a step holds, skipping paths the group does not have.
|
|
310
|
+
*
|
|
311
|
+
* A missing control is not an error here: a renderer that flattens groups holds `q-5` where the
|
|
312
|
+
* definition says `riyadh.q-5`, and a step asserting on every path would call such a form
|
|
313
|
+
* permanently incomplete.
|
|
314
|
+
*/
|
|
315
|
+
declare function stepControls(step: FormStepView, group: FormGroup): AbstractControl[];
|
|
316
|
+
/**
|
|
317
|
+
* True when nothing in the step is blocking.
|
|
318
|
+
*
|
|
319
|
+
* Disabled controls are skipped rather than counted valid, because Angular reports a disabled
|
|
320
|
+
* control's `valid` as **false** — a step whose conditional half is currently hidden would never
|
|
321
|
+
* pass, and the Next button would sit dead with no error to explain why.
|
|
322
|
+
*
|
|
323
|
+
* A step holding no controls at all is complete: a page of `display` copy is something the user
|
|
324
|
+
* reads, not something they answer.
|
|
325
|
+
*/
|
|
326
|
+
declare function isStepComplete(step: FormStepView, group: FormGroup): boolean;
|
|
327
|
+
/** Marks everything in the step touched, so `Next` reveals the errors instead of doing nothing. */
|
|
328
|
+
declare function markStepTouched(step: FormStepView, group: FormGroup): void;
|
|
329
|
+
/**
|
|
330
|
+
* Whether the user may move on from `index`.
|
|
331
|
+
*
|
|
332
|
+
* False on the last step: advancing past the end is a submit, and a wizard that treats it as a
|
|
333
|
+
* step change is how a form gets a seventh page nobody wrote.
|
|
334
|
+
*/
|
|
335
|
+
declare function canAdvance(steps: readonly FormStepView[], group: FormGroup, index: number): boolean;
|
|
336
|
+
/**
|
|
337
|
+
* The furthest step the user has earned.
|
|
338
|
+
*
|
|
339
|
+
* Every step up to it is complete. This is what a stepper binds its clickable steps to — letting
|
|
340
|
+
* someone jump to step 5 from an unfinished step 2 produces a review page summarising answers
|
|
341
|
+
* that were never given.
|
|
342
|
+
*/
|
|
343
|
+
declare function furthestReachableStep(steps: readonly FormStepView[], group: FormGroup): number;
|
|
344
|
+
/**
|
|
345
|
+
* The first step that is not complete, or `-1` when all of them are.
|
|
346
|
+
*
|
|
347
|
+
* A submit that fails should land the user on the question that failed it, not on step 1 with a
|
|
348
|
+
* toast — this is the index to navigate to.
|
|
349
|
+
*/
|
|
350
|
+
declare function firstIncompleteStep(steps: readonly FormStepView[], group: FormGroup): number;
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Resolves one named validator, or `null` when its parameters are missing or unusable.
|
|
354
|
+
*
|
|
355
|
+
* Returning `null` rather than throwing is deliberate: a definition with a malformed `min` should
|
|
356
|
+
* render the form without that one check, not blank the screen. Callers that need to know use
|
|
357
|
+
* {@link resolveNamedValidators}, which reports what it could not build.
|
|
358
|
+
*/
|
|
359
|
+
declare function resolveNamedValidator(validator: NamedValidator): ValidatorFn | null;
|
|
360
|
+
/** Resolves a list of named validators, reporting the names that could not be built. */
|
|
361
|
+
declare function resolveNamedValidators(validators?: readonly NamedValidator[]): {
|
|
362
|
+
readonly fns: ValidatorFn[];
|
|
363
|
+
readonly unresolved: NamedValidatorName[];
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Builds the `FormGroup` a definition describes.
|
|
368
|
+
*
|
|
369
|
+
* Three decisions here are the ones that bite if they are made the other way:
|
|
370
|
+
*
|
|
371
|
+
* - **Hidden fields still get a control.** They are `disable()`d rather than omitted, so
|
|
372
|
+
* `getRawValue()` still carries their key and a rule can reveal one without rebuilding the
|
|
373
|
+
* group and losing everything the user has typed. A hidden control that is merely absent is
|
|
374
|
+
* how a conditional field ends up blocking submit with an error nobody can see.
|
|
375
|
+
* - **`emitEvent: false` on every disable.** Toggling enablement during a `valueChanges`
|
|
376
|
+
* handler otherwise re-enters the rule pass, and the second pass sees a half-applied state.
|
|
377
|
+
* - **Requirement is reconciled, never toggled.** Each pass recomputes it from the rules and
|
|
378
|
+
* the definition together, so a field a rule un-required becomes required again the moment
|
|
379
|
+
* the condition stops matching.
|
|
380
|
+
*/
|
|
381
|
+
/** Everything the renderer needs after a build: the group, and what could not be resolved. */
|
|
382
|
+
interface BuiltForm {
|
|
383
|
+
readonly group: FormGroup;
|
|
384
|
+
/** Validator names present in the definition that the engine could not resolve. Empty on a clean build. */
|
|
385
|
+
readonly unresolvedValidators: readonly string[];
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Builds a row for an existing repeater, so a consumer can append one.
|
|
389
|
+
*
|
|
390
|
+
* Exported because `addArrayRow` has to produce a row indistinguishable from the ones the
|
|
391
|
+
* initial build produced — a second implementation would drift the moment a field type gained a
|
|
392
|
+
* coercion rule.
|
|
393
|
+
*
|
|
394
|
+
* A fresh row carries the definition's static state only. Rule state cannot be known here: the
|
|
395
|
+
* row does not exist in the values yet, so there is nothing for a condition to read. The caller
|
|
396
|
+
* settles it by running {@link applyRuleState} once the row is in the array, which
|
|
397
|
+
* {@link addArrayRow} documents as the step that must follow.
|
|
398
|
+
*/
|
|
399
|
+
declare function buildArrayRow(node: FormArrayNode, initial: Readonly<Record<string, unknown>>, fb: NonNullableFormBuilder, rowPath?: string): FormGroup;
|
|
400
|
+
/** Builds a typed, non-nullable `FormGroup` from a definition and its initial values. */
|
|
401
|
+
declare function buildFormGroup(definition: FormDefinition, initial: Readonly<Record<string, unknown>>, fb: NonNullableFormBuilder): BuiltForm;
|
|
402
|
+
/**
|
|
403
|
+
* Re-applies rule state to an existing group after a value change.
|
|
404
|
+
*
|
|
405
|
+
* Rebuilding the group instead would discard `touched`, `dirty` and focus — which is exactly
|
|
406
|
+
* what users report as "the form reset itself while I was typing".
|
|
407
|
+
*/
|
|
408
|
+
declare function applyRuleState(definition: FormDefinition, group: FormGroup): RuleState;
|
|
409
|
+
/**
|
|
410
|
+
* The values a submit should carry.
|
|
411
|
+
*
|
|
412
|
+
* Hidden fields are omitted rather than sent as empty: a conditional branch the user never saw
|
|
413
|
+
* should not appear in the payload at all, which is different from appearing as `""`.
|
|
414
|
+
*/
|
|
415
|
+
declare function pickActiveValues(definition: FormDefinition, group: FormGroup, state: RuleState): Record<string, unknown>;
|
|
416
|
+
/**
|
|
417
|
+
* A signature of everything about a definition that requires a *rebuild* rather than a patch.
|
|
418
|
+
*
|
|
419
|
+
* Options are deliberately excluded. Including them meant every lookup fetch changed the
|
|
420
|
+
* signature, rebuilt the group and wiped whatever the user had already typed — options change
|
|
421
|
+
* what a select offers, never the shape of the control holding it. Keys are sorted so that two
|
|
422
|
+
* definitions differing only in property order produce the same signature.
|
|
423
|
+
*/
|
|
424
|
+
declare function structureSignature(definition: FormDefinition): string;
|
|
425
|
+
|
|
426
|
+
export { DEFAULT_FIELD_RULE_STATE, FORM_DATA_SOURCE, MULTI_VALUE_TYPES, addArrayRow, applyRuleState, arrayNodeAt, buildArrayRow, buildFormGroup, canAddArrayRow, canAdvance, canRemoveArrayRow, coerceValue, dataSourceById, dataSourceDependencyValue, evaluateCondition, evaluateRules, fieldPaths, firstIncompleteStep, furthestReachableStep, isStepComplete, markStepTouched, pickActiveValues, removeArrayRow, resolveDataSource, resolveNamedValidator, resolveNamedValidators, resolveSteps, stepControls, structureSignature, typeFallback };
|
|
427
|
+
export type { ArrayRowRefusal, ArrayRowResult, BuiltForm, FieldPath, FieldRuleState, FormDataSource, FormDataSourceDependency, FormDataSourceRequest, FormOption, FormStepIssue, FormStepView, ResolvedDataSource, ResolvedSteps, RowScope, RuleState };
|