@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.
package/README.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# @moatamed-sce/form-engine
|
|
2
|
+
|
|
3
|
+
The runtime half of the SCE form platform. [`@moatamed-sce/form-schema`](../form-schema) says what a form _is_ — a versioned JSON document; this package turns that document into a working Angular form: a typed `FormGroup`, resolved validators, evaluated rules, repeating rows, and lookup-backed options.
|
|
4
|
+
|
|
5
|
+
It renders nothing. Rendering is `sce-dynamic-form` in [`@moatamed-sce/ui-components`](../ui-components). The split is deliberate: the engine is `type:core`, so it may use `@angular/forms` but never `HttpClient`, NgRx, or the design system — which is what lets the same document be built into a form inside a designer preview, a unit test, and the app.
|
|
6
|
+
|
|
7
|
+
## Status — not published
|
|
8
|
+
|
|
9
|
+
**This package is not currently on npmjs.org**, and neither is `@moatamed-sce/form-schema`, which it declares as a peer. Inside the SCE monorepo both resolve through TypeScript path aliases.
|
|
10
|
+
|
|
11
|
+
## What's in it
|
|
12
|
+
|
|
13
|
+
**`buildFormGroup(definition, initialValue, fb)`** — the entry point. Walks the definition's pages depth-first and returns a non-nullable `FormGroup`: fields become `FormControl`s, `group` nodes become nested `FormGroup`s, and `array` nodes become a `FormArray` of row groups. A field's initial value is coerced to its type, so a `number` field handed `"5"` does not poison the form value with a string.
|
|
14
|
+
|
|
15
|
+
**Named validators** — `resolveNamedValidator` / `resolveNamedValidators` turn a `NamedValidator` (`{ name: 'saudiId' }`) into a real `ValidatorFn`, resolved against the implementations in `@moatamed-sce/core`. This is the half of the schema's central constraint that makes it work: the document carries a _name_ because a function is not serializable, and this is where the name becomes a function again.
|
|
16
|
+
|
|
17
|
+
**Rule evaluation** — `applyRuleState` reads the definition's rules against the current form value and produces the per-field state the renderer needs: hidden, disabled, required, readonly. Rules are the closed operator set the schema defines; there is no `eval` and no expression parser.
|
|
18
|
+
|
|
19
|
+
**Repeating rows** — `addArrayRow`, `removeArrayRow`, `canAddArrayRow`, `canRemoveArrayRow`, `buildArrayRow` and `arrayNodeAt`, which honour the node's `minItems` / `maxItems` so the UI never has to re-derive the bounds.
|
|
20
|
+
|
|
21
|
+
**Wizard steps** — `resolveSteps` turns a definition's `steps` into a `FormStepView` per step, reporting a `FormStepIssue` for anything that does not line up.
|
|
22
|
+
|
|
23
|
+
**Data sources** — the `FORM_DATA_SOURCE` injection token and `resolveDataSource`. The engine never fetches: the host provides a `FormDataSource`, and a `DataSourceRef` names a `lookupKey` the host resolves. A `dependsOn` source is re-requested with the value of the control it depends on.
|
|
24
|
+
|
|
25
|
+
**`pickActiveValues`** — the submit-time value with hidden and disabled controls removed, so a conditional branch the user never saw does not travel to the API.
|
|
26
|
+
|
|
27
|
+
**`structureSignature`** — a cheap string that changes only when a definition's _shape_ changes, which is what lets the renderer rebuild the `FormGroup` on a real structural change and leave it alone otherwise.
|
|
28
|
+
|
|
29
|
+
## Peer dependencies
|
|
30
|
+
|
|
31
|
+
Angular **21** (`@angular/core`, `@angular/forms`), RxJS 7.8, `@moatamed-sce/core` (the Saudi validators live there) and `@moatamed-sce/form-schema` (the contract).
|
|
32
|
+
|
|
33
|
+
## Quick start
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import { inject } from '@angular/core';
|
|
37
|
+
import { NonNullableFormBuilder } from '@angular/forms';
|
|
38
|
+
import { parseFormDefinition } from '@moatamed-sce/form-schema';
|
|
39
|
+
import { buildFormGroup, applyRuleState, pickActiveValues } from '@moatamed-sce/form-engine';
|
|
40
|
+
|
|
41
|
+
const fb = inject(NonNullableFormBuilder);
|
|
42
|
+
|
|
43
|
+
// Parse first — buildFormGroup trusts the definition completely.
|
|
44
|
+
const { definition, problems } = parseFormDefinition(stored);
|
|
45
|
+
if (!definition) {
|
|
46
|
+
return this.error.set(problems);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const { group } = buildFormGroup(definition, { nationalId: '1234567890' }, fb);
|
|
50
|
+
|
|
51
|
+
// Which fields are hidden / required / readonly right now:
|
|
52
|
+
const state = applyRuleState(definition, group);
|
|
53
|
+
|
|
54
|
+
// On submit — hidden and disabled controls are dropped:
|
|
55
|
+
const payload = pickActiveValues(definition, group, state);
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Provide a data source when the definition uses `lookup` options:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import {
|
|
62
|
+
FORM_DATA_SOURCE,
|
|
63
|
+
type FormDataSource,
|
|
64
|
+
type FormDataSourceRequest,
|
|
65
|
+
type FormOption,
|
|
66
|
+
} from '@moatamed-sce/form-engine';
|
|
67
|
+
|
|
68
|
+
@Injectable({ providedIn: 'root' })
|
|
69
|
+
export class LookupFormDataSource implements FormDataSource {
|
|
70
|
+
private readonly lookups = inject(LookupsApiService);
|
|
71
|
+
|
|
72
|
+
// `lookupKey` is a key, never a URL — the document cannot choose an endpoint.
|
|
73
|
+
// Returns an Observable; the engine subscribes, it never fetches.
|
|
74
|
+
options(request: FormDataSourceRequest): Observable<readonly FormOption[]> {
|
|
75
|
+
return this.lookups.byKey(request.lookupKey, request.dependsOn?.value);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
providers: [{ provide: FORM_DATA_SOURCE, useExisting: LookupFormDataSource }];
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The request also carries `definitionId`, so a host serving several forms can scope caching or
|
|
83
|
+
permissions. The renderer injects the token `{ optional: true }` — a form built entirely from
|
|
84
|
+
static sources should not force every host that renders one to supply a resolver.
|
|
85
|
+
|
|
86
|
+
## A caveat worth knowing
|
|
87
|
+
|
|
88
|
+
`buildFormGroup` trusts the definition it is handed. Run it through `parseFormDefinition` first for anything that did not come from a TypeScript literal in this repo — a stored draft, an API response, an uploaded file. The parser is where prototype-pollution paths, non-JSON values and unbounded row counts are refused; the engine assumes that already happened.
|
|
89
|
+
|
|
90
|
+
## Docs
|
|
91
|
+
|
|
92
|
+
- [ADR 0002 — a form is a versioned JSON definition](../../docs/adr/0002-form-definition-platform.md)
|
|
93
|
+
- [Form definition platform — full plan](../../plans/form-builder-platform.md)
|
|
94
|
+
- [The JSON contract](../form-schema/README.md)
|
|
95
|
+
|
|
96
|
+
## License
|
|
97
|
+
|
|
98
|
+
MIT
|