@jarenjs/forms 0.9.2 → 0.34.2
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 +204 -7
- package/dist/types/data.d.ts +23 -4
- package/dist/types/deps.d.ts +30 -0
- package/dist/types/index.d.ts +3 -1
- package/dist/types/messages.d.ts +36 -0
- package/dist/types/model.d.ts +34 -25
- package/dist/types/rules.d.ts +138 -26
- package/dist/types/validate.d.ts +15 -4
- package/dist/types/viewmodel.d.ts +373 -0
- package/package.json +6 -5
- package/src/data.js +133 -46
- package/src/deps.js +179 -0
- package/src/formats.js +35 -10
- package/src/index.js +13 -0
- package/src/messages.js +106 -0
- package/src/model.js +140 -42
- package/src/rules.js +454 -55
- package/src/validate.js +79 -59
- package/src/viewmodel.js +419 -0
package/dist/types/rules.d.ts
CHANGED
|
@@ -12,11 +12,26 @@ export type RuleResult = {
|
|
|
12
12
|
*/
|
|
13
13
|
computed?: any;
|
|
14
14
|
/**
|
|
15
|
-
* `[{ keyword: 'x-form/assert', message }]` when the
|
|
16
|
-
* (the validateField error shape, so error
|
|
15
|
+
* `[{ keyword: 'x-form/assert', params, msgid, message }]` when the
|
|
16
|
+
* `assert` rule fails (the validateField error shape, so error
|
|
17
|
+
* rendering works unchanged; `params` always carries the `pointer`)
|
|
17
18
|
*/
|
|
18
19
|
errors?: Array<import('./validate.js').FieldError>;
|
|
19
20
|
};
|
|
21
|
+
export type CompiledRuleMessage = {
|
|
22
|
+
/**
|
|
23
|
+
* - Catalog key, or null for a plain inline message
|
|
24
|
+
*/
|
|
25
|
+
msgid: string | null;
|
|
26
|
+
/**
|
|
27
|
+
* - Compiled inline template
|
|
28
|
+
*/
|
|
29
|
+
render: ((params: object) => string) | null;
|
|
30
|
+
/**
|
|
31
|
+
* - Author params, merged into the error's params
|
|
32
|
+
*/
|
|
33
|
+
params: object | null;
|
|
34
|
+
};
|
|
20
35
|
export type CompiledFieldRules = {
|
|
21
36
|
/**
|
|
22
37
|
* - The field's data pointer (template pointers keep `-`)
|
|
@@ -38,23 +53,16 @@ export type CompiledFieldRules = {
|
|
|
38
53
|
enabled: Function | null;
|
|
39
54
|
assert: Function | null;
|
|
40
55
|
computed: Function | null;
|
|
41
|
-
message:
|
|
56
|
+
message: CompiledRuleMessage | null;
|
|
57
|
+
/**
|
|
58
|
+
* - Pointer prefixes this rule reads (deps.js);
|
|
59
|
+
* the memo re-runs it only when a change touches one of them
|
|
60
|
+
*/
|
|
61
|
+
deps: string[];
|
|
42
62
|
};
|
|
43
63
|
export type CompiledRules = {
|
|
44
64
|
rules: Array<CompiledFieldRules>;
|
|
45
65
|
};
|
|
46
|
-
/**
|
|
47
|
-
* @typedef {object} CompiledFieldRules
|
|
48
|
-
* @property {string} pointer - The field's data pointer (template pointers keep `-`)
|
|
49
|
-
* @property {Array<string|symbol>} parts - Decoded segments; ITEM marks an array-item slot
|
|
50
|
-
* @property {boolean} templated - Whether `parts` contains an ITEM slot
|
|
51
|
-
* @property {((root: any) => any)|null} getValue - Compiled getter (non-template fields)
|
|
52
|
-
* @property {function|null} visible
|
|
53
|
-
* @property {function|null} enabled
|
|
54
|
-
* @property {function|null} assert
|
|
55
|
-
* @property {function|null} computed
|
|
56
|
-
* @property {string|null} message
|
|
57
|
-
*/
|
|
58
66
|
/**
|
|
59
67
|
* @typedef {object} CompiledRules
|
|
60
68
|
* @property {Array<CompiledFieldRules>} rules
|
|
@@ -92,14 +100,98 @@ export declare function compileFormRules(model: import('./model.js').FormField,
|
|
|
92
100
|
* field declares. Rules on array item templates are evaluated once per
|
|
93
101
|
* element of the actual array, keyed by the expanded pointer.
|
|
94
102
|
*
|
|
103
|
+
* Pass a `memo` from {@link createRuleMemo} to re-evaluate only the
|
|
104
|
+
* rules a change can have affected. The memo diffs the previous
|
|
105
|
+
* document against this one — reference-equal subtrees are skipped
|
|
106
|
+
* whole, so an immutable edit costs O(change) — and re-runs a rule only
|
|
107
|
+
* when a changed pointer touches one of its declared dependencies
|
|
108
|
+
* (deps.js). The result is identical to an unmemoized evaluation.
|
|
109
|
+
*
|
|
110
|
+
* The memo OWNS the map it returns and patches it on later calls: a
|
|
111
|
+
* caller must read it before evaluating again, and must not keep it as
|
|
112
|
+
* a snapshot (`buildFormViewModel` reads it synchronously, which is the
|
|
113
|
+
* intended shape). Handing back a fresh map instead would put a write
|
|
114
|
+
* per rule back on the hot path — on a wide form, the rules that did
|
|
115
|
+
* NOT change are the work worth skipping.
|
|
116
|
+
*
|
|
95
117
|
* @param {CompiledRules} compiled - From compileFormRules
|
|
96
118
|
* @param {any} data - The form data root (the query input `$`)
|
|
119
|
+
* @param {Readonly<Record<string, (params: object, error?: object) => string>>} [catalog] - Optional compiled message catalog (see messages.js), default English
|
|
120
|
+
* @param {RuleMemo} [memo] - Reused across calls; mutated in place
|
|
97
121
|
* @returns {Record<string, RuleResult>}
|
|
98
122
|
* @example
|
|
99
123
|
* const results = evaluateFormRules(compiled, { company: 'ACME', vatId: '' });
|
|
100
124
|
* results['/vatId'].errors; // [{ keyword: 'x-form/assert', message: '...' }]
|
|
101
125
|
*/
|
|
102
|
-
export declare function evaluateFormRules(compiled: CompiledRules, data: any): Record<string, RuleResult>;
|
|
126
|
+
export declare function evaluateFormRules(compiled: CompiledRules, data: any, catalog?: Readonly<Record<string, (params: object, error?: object) => string>>, memo?: RuleMemo): Record<string, RuleResult>;
|
|
127
|
+
export type RuleMemo = {
|
|
128
|
+
data: any;
|
|
129
|
+
catalog: any;
|
|
130
|
+
results: Record<string, RuleResult> | null;
|
|
131
|
+
keys: string[][] | null;
|
|
132
|
+
/**
|
|
133
|
+
* - Pointers declared through
|
|
134
|
+
* {@link RuleMemo.touch}, consumed by the next evaluation
|
|
135
|
+
*/
|
|
136
|
+
touched: string[] | null;
|
|
137
|
+
touch: (pointer: string) => RuleMemo;
|
|
138
|
+
};
|
|
139
|
+
/**
|
|
140
|
+
* The memo {@link evaluateFormRules} carries between keystrokes: the
|
|
141
|
+
* document it last saw, the results it produced, and which result keys
|
|
142
|
+
* each rule wrote (an item-template rule writes one per element, so the
|
|
143
|
+
* count is data-dependent and has to be recorded, not derived).
|
|
144
|
+
* @typedef {object} RuleMemo
|
|
145
|
+
* @property {any} data
|
|
146
|
+
* @property {any} catalog
|
|
147
|
+
* @property {Record<string, RuleResult>|null} results
|
|
148
|
+
* @property {string[][]|null} keys
|
|
149
|
+
* @property {string[]|null} touched - Pointers declared through
|
|
150
|
+
* {@link RuleMemo.touch}, consumed by the next evaluation
|
|
151
|
+
* @property {(pointer: string) => RuleMemo} touch
|
|
152
|
+
*/
|
|
153
|
+
/**
|
|
154
|
+
* Create an empty rule memo. One per form session: it is bound to the
|
|
155
|
+
* document lineage it has seen, so sharing it between two forms would
|
|
156
|
+
* diff unrelated documents (correct, but pointlessly expensive).
|
|
157
|
+
*
|
|
158
|
+
* `memo.touch(pointer)` declares a write before the next evaluation.
|
|
159
|
+
* It is an optimization AND a promise: the evaluation then trusts the
|
|
160
|
+
* declaration instead of diffing, so a caller that touches one pointer
|
|
161
|
+
* while changing another gets stale results for the rules it did not
|
|
162
|
+
* name. Say nothing and the diff works it out.
|
|
163
|
+
* @returns {RuleMemo}
|
|
164
|
+
* @example
|
|
165
|
+
* const memo = createRuleMemo();
|
|
166
|
+
* data = setValueAtPointer(data, '/lines/2/amount', 9);
|
|
167
|
+
* const state = evaluateFormRules(compiled, data, catalog, memo.touch('/lines/2/amount'));
|
|
168
|
+
*/
|
|
169
|
+
export declare function createRuleMemo(): RuleMemo;
|
|
170
|
+
/**
|
|
171
|
+
* Drop the values of fields their `visible` rules currently hide, for a
|
|
172
|
+
* caller about to submit.
|
|
173
|
+
*
|
|
174
|
+
* The policy this settles: hidden values are KEPT while editing (a
|
|
175
|
+
* field that reappears must not have forgotten what the operator typed)
|
|
176
|
+
* and dropped only here, at the submit boundary, by a caller who asked.
|
|
177
|
+
* Nothing prunes implicitly — `buildFormViewModel` still never touches
|
|
178
|
+
* the data, and this returns a copy.
|
|
179
|
+
*
|
|
180
|
+
* Visibility is evaluated ONCE against the incoming document, so a
|
|
181
|
+
* `visible` rule that reads a value this call removes still sees it.
|
|
182
|
+
* Hidden array ELEMENTS are removed and their siblings renumber, which
|
|
183
|
+
* is right for a document being sent but means the returned pointers no
|
|
184
|
+
* longer match the ones the view model rendered.
|
|
185
|
+
*
|
|
186
|
+
* @param {CompiledRules} compiled - From compileFormRules
|
|
187
|
+
* @param {any} data - The form data root
|
|
188
|
+
* @param {Readonly<Record<string, (params: object, error?: object) => string>>} [catalog]
|
|
189
|
+
* @returns {any} A copy without the hidden values (`data` itself when
|
|
190
|
+
* nothing is hidden; untouched subtrees are shared)
|
|
191
|
+
* @example
|
|
192
|
+
* const submitted = pruneHiddenValues(compiled, session.data);
|
|
193
|
+
*/
|
|
194
|
+
export declare function pruneHiddenValues(compiled: CompiledRules, data: any, catalog?: Readonly<Record<string, (params: object, error?: object) => string>>): any;
|
|
103
195
|
/**
|
|
104
196
|
* Copy every `x-form.assert` of a schema into a `$query` assertion, so a
|
|
105
197
|
* rule authored once for per-keystroke feedback is also enforced by the
|
|
@@ -107,23 +199,43 @@ export declare function evaluateFormRules(compiled: CompiledRules, data: any): R
|
|
|
107
199
|
* Pure schema-to-schema transform - no validator import; the output only
|
|
108
200
|
* spells the keyword.
|
|
109
201
|
*
|
|
110
|
-
* The
|
|
111
|
-
* instance root, matching the rule context),
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
202
|
+
* The asserts land on the ROOT schema (where the query input `$` is the
|
|
203
|
+
* instance root, matching the rule context), each as its OWN `allOf`
|
|
204
|
+
* branch `{ "$query": <wrapped>, "errorMessage": { "$query": <spec> } }`
|
|
205
|
+
* so per-assert identity - and the rule's authored message - survives
|
|
206
|
+
* into submit validation. Each assert is wrapped to rebuild its bindings:
|
|
207
|
+
* `value` binds to the field's location, `pointer` to its pointer string.
|
|
208
|
+
* An assert on an array item template quantifies with `$every` over the
|
|
209
|
+
* actual elements (`pointer` then stays the template pointer - element
|
|
210
|
+
* indexes are a render-time notion). An existing root `$query` is left
|
|
211
|
+
* untouched on the root itself.
|
|
212
|
+
*
|
|
213
|
+
* The carried message spec is the rule's `x-form.message` - inline string
|
|
214
|
+
* as an inline `message`, `$msgid` form passed through - with `params`
|
|
215
|
+
* merged over `{ pointer: <field pointer> }`; a rule with no message gets
|
|
216
|
+
* `{ "$msgid": "x-form/assert", "params": { "pointer": ... } }`. EVERY
|
|
217
|
+
* submit-time `$query` failure therefore carries the owning field's
|
|
218
|
+
* pointer in `params`, which lets UIs map root-level `$query` errors onto
|
|
219
|
+
* fields.
|
|
118
220
|
*
|
|
119
221
|
* The transform follows the same structural spine as buildFormModel
|
|
120
222
|
* (`properties`, `items`, `prefixItems`, `allOf`) but does not resolve
|
|
121
223
|
* `$ref`s - a `$def`'s data location depends on its use site.
|
|
122
224
|
*
|
|
225
|
+
* Absent fields bind exactly as they do per keystroke: `null`. A path
|
|
226
|
+
* that selects nothing is the empty sequence, which compares unequal to
|
|
227
|
+
* everything and would make `$ne`/`$eq` mean the opposite thing on the
|
|
228
|
+
* two sides of the same authored rule - so the binding is wrapped in a
|
|
229
|
+
* `$default` against `null`. For the same reason an item-template
|
|
230
|
+
* assert quantifies over the ELEMENTS rather than over the selected
|
|
231
|
+
* leaf values: quantifying over the leaves silently skips an element
|
|
232
|
+
* that lacks the member, where the keystroke path evaluates it with
|
|
233
|
+
* `null`.
|
|
234
|
+
*
|
|
123
235
|
* @param {object|boolean} schema - The root JSON schema
|
|
124
236
|
* @returns {object|boolean} A new root schema (input is not mutated;
|
|
125
|
-
* untouched subtrees are shared) with the collected `$query
|
|
126
|
-
* input itself when there is nothing to copy
|
|
237
|
+
* untouched subtrees are shared) with the collected `$query` branches,
|
|
238
|
+
* or the input itself when there is nothing to copy
|
|
127
239
|
* @example
|
|
128
240
|
* const submitSchema = formRulesToQueryAssertions(schema);
|
|
129
241
|
* const validate = new JarenValidator().compile(submitSchema); // caller-side
|
package/dist/types/validate.d.ts
CHANGED
|
@@ -4,7 +4,15 @@ export type FieldError = {
|
|
|
4
4
|
*/
|
|
5
5
|
keyword: string;
|
|
6
6
|
/**
|
|
7
|
-
* -
|
|
7
|
+
* - Structured, keyword-specific parameters
|
|
8
|
+
*/
|
|
9
|
+
params: object;
|
|
10
|
+
/**
|
|
11
|
+
* - Stable message key (`form/<keyword>` or `x-form/assert`)
|
|
12
|
+
*/
|
|
13
|
+
msgid: string;
|
|
14
|
+
/**
|
|
15
|
+
* - Human readable message (rendered through a catalog)
|
|
8
16
|
*/
|
|
9
17
|
message: string;
|
|
10
18
|
};
|
|
@@ -13,17 +21,20 @@ export type FieldError = {
|
|
|
13
21
|
*
|
|
14
22
|
* @param {import('./model.js').FormField} field - Field from buildFormModel
|
|
15
23
|
* @param {any} value - The TYPED value (see parseFieldInput); undefined = absent
|
|
24
|
+
* @param {Readonly<Record<string, (params: object, error?: object) => string>>} [catalog] - Optional compiled message catalog (see messages.js), default English
|
|
16
25
|
* @returns {FieldError[]} Empty when the value passes every per-field check
|
|
17
26
|
* @example
|
|
18
27
|
* const errors = validateField(emailField, 'not-an-email');
|
|
19
|
-
* // [{ keyword: 'format',
|
|
28
|
+
* // [{ keyword: 'format', params: { format: 'email' },
|
|
29
|
+
* // msgid: 'form/format', message: 'Must be a valid email' }]
|
|
20
30
|
*/
|
|
21
|
-
export declare function validateField(field: import('./model.js').FormField, value: any): FieldError[];
|
|
31
|
+
export declare function validateField(field: import('./model.js').FormField, value: any, catalog?: Readonly<Record<string, (params: object, error?: object) => string>>): FieldError[];
|
|
22
32
|
/**
|
|
23
33
|
* Validate every leaf field of a model against the current data.
|
|
24
34
|
* Returns a map of data-pointer -> FieldError[] for fields that fail.
|
|
25
35
|
* @param {import('./model.js').FormField} model - Root field from buildFormModel
|
|
26
36
|
* @param {any} data - Current form data
|
|
37
|
+
* @param {Readonly<Record<string, (params: object, error?: object) => string>>} [catalog] - Optional compiled message catalog, default English
|
|
27
38
|
* @returns {Record<string, FieldError[]>}
|
|
28
39
|
*/
|
|
29
|
-
export declare function validateAllFields(model: import('./model.js').FormField, data: any): Record<string, FieldError[]>;
|
|
40
|
+
export declare function validateAllFields(model: import('./model.js').FormField, data: any, catalog?: Readonly<Record<string, (params: object, error?: object) => string>>): Record<string, FieldError[]>;
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The form view model — the render tree.
|
|
3
|
+
*
|
|
4
|
+
* `buildFormViewModel` composes everything this package knows about a
|
|
5
|
+
* form at one instant — the field tree (model.js), the current data,
|
|
6
|
+
* per-field validation (validate.js) and `x-form` rule state (rules.js)
|
|
7
|
+
* — into ONE plain-JSON tree of render nodes. It is the "computed view"
|
|
8
|
+
* layer the package README promised: no DOM, no framework, just the
|
|
9
|
+
* document any renderer needs — a React component, a template engine,
|
|
10
|
+
* or a JSLT stylesheet (the standard form rules of `@jarenjs/app`
|
|
11
|
+
* dispatch over exactly this shape).
|
|
12
|
+
*
|
|
13
|
+
* Policies inherited from rules.js: a field whose rule state says
|
|
14
|
+
* `visible: false` is EXCLUDED from the tree (renderers cannot leak
|
|
15
|
+
* hidden data by accident); `enabled` and `computed` are carried on the
|
|
16
|
+
* node. Array item templates expand per element of the actual data, so
|
|
17
|
+
* node pointers are always concrete (`/lines/2/amount`), matching the
|
|
18
|
+
* pointer keys of `evaluateFormRules` and `validateAllFields`.
|
|
19
|
+
*/
|
|
20
|
+
export type FormField = import('./model.js').FormField;
|
|
21
|
+
export type FormViewNode = {
|
|
22
|
+
/**
|
|
23
|
+
* - Concrete data pointer ('' for the root).
|
|
24
|
+
*/
|
|
25
|
+
pointer: string;
|
|
26
|
+
/**
|
|
27
|
+
* - Property name, or the element index as a string.
|
|
28
|
+
*/
|
|
29
|
+
key: string;
|
|
30
|
+
label: string;
|
|
31
|
+
description: string | null;
|
|
32
|
+
/**
|
|
33
|
+
* - The field kind (model.js).
|
|
34
|
+
*/
|
|
35
|
+
kind: string;
|
|
36
|
+
/**
|
|
37
|
+
* - The rendering hint (model.js).
|
|
38
|
+
*/
|
|
39
|
+
control: string;
|
|
40
|
+
required: boolean;
|
|
41
|
+
readOnly: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* - `x-form.enabled`, defaulting true.
|
|
44
|
+
*/
|
|
45
|
+
enabled: boolean;
|
|
46
|
+
placeholder: string | null;
|
|
47
|
+
/**
|
|
48
|
+
* - The current value (`x-form.computed` wins);
|
|
49
|
+
* `null` when the field is absent from the data.
|
|
50
|
+
*/
|
|
51
|
+
value: any;
|
|
52
|
+
/**
|
|
53
|
+
* Select options, `selected` precomputed against the current value.
|
|
54
|
+
* `key` is the option's value as JSON text — what a string-valued
|
|
55
|
+
* control (a DOM `<option>`) can carry and hand back losslessly.
|
|
56
|
+
*/
|
|
57
|
+
options: Array<{
|
|
58
|
+
value: any;
|
|
59
|
+
key: string;
|
|
60
|
+
label: string;
|
|
61
|
+
selected: boolean;
|
|
62
|
+
}> | null;
|
|
63
|
+
/**
|
|
64
|
+
* - The value as indented JSON text, on
|
|
65
|
+
* `json`-control nodes only: the editable text for a structured
|
|
66
|
+
* value, precomputed like `options`.
|
|
67
|
+
*/
|
|
68
|
+
json?: string;
|
|
69
|
+
/**
|
|
70
|
+
* - Localized messages: field validation
|
|
71
|
+
* first, then rule asserts.
|
|
72
|
+
*/
|
|
73
|
+
errors: string[];
|
|
74
|
+
/**
|
|
75
|
+
* - True when this node is an array element
|
|
76
|
+
* (item or tuple slot). Writers need this: RFC 6902 `add` is
|
|
77
|
+
* set-or-replace for object members but INSERT for array indices, so
|
|
78
|
+
* element nodes must be written with `replace`.
|
|
79
|
+
*/
|
|
80
|
+
element: boolean;
|
|
81
|
+
/**
|
|
82
|
+
* - True for item-template elements
|
|
83
|
+
* (tuple slots are fixed).
|
|
84
|
+
*/
|
|
85
|
+
removable: boolean;
|
|
86
|
+
/**
|
|
87
|
+
* - Object members.
|
|
88
|
+
*/
|
|
89
|
+
children: FormViewNode[] | null;
|
|
90
|
+
/**
|
|
91
|
+
* - Array elements, expanded.
|
|
92
|
+
*/
|
|
93
|
+
items: FormViewNode[] | null;
|
|
94
|
+
/**
|
|
95
|
+
* - Starter value for a new array item
|
|
96
|
+
* (`createItemValue`); only on array nodes with an item template.
|
|
97
|
+
*/
|
|
98
|
+
addValue: any;
|
|
99
|
+
/**
|
|
100
|
+
* - Stable accessible element id derived from
|
|
101
|
+
* the pointer (session forms only).
|
|
102
|
+
*/
|
|
103
|
+
id?: string;
|
|
104
|
+
/**
|
|
105
|
+
* - The id of this node's error
|
|
106
|
+
* text (`aria-describedby` wiring), `null` when the node has no
|
|
107
|
+
* errors (session forms only).
|
|
108
|
+
*/
|
|
109
|
+
describedBy?: string | null;
|
|
110
|
+
/**
|
|
111
|
+
* - Whether this node's value differs from
|
|
112
|
+
* the session's initial data — presence-aware: a member added or
|
|
113
|
+
* removed is dirty even when the compared values coincide as `null`
|
|
114
|
+
* (session forms only).
|
|
115
|
+
*/
|
|
116
|
+
dirty?: boolean;
|
|
117
|
+
/**
|
|
118
|
+
* - Whether the session marked this
|
|
119
|
+
* pointer visited (session forms only).
|
|
120
|
+
*/
|
|
121
|
+
touched?: boolean;
|
|
122
|
+
/**
|
|
123
|
+
* - Server-reported messages for
|
|
124
|
+
* this pointer, kept distinct from the client-side `errors` (session
|
|
125
|
+
* forms only).
|
|
126
|
+
*/
|
|
127
|
+
serverErrors?: string[];
|
|
128
|
+
/**
|
|
129
|
+
* - The root summary (root
|
|
130
|
+
* node of session forms only).
|
|
131
|
+
*/
|
|
132
|
+
session?: FormSessionSummary;
|
|
133
|
+
};
|
|
134
|
+
export type FormSessionOptions = {
|
|
135
|
+
/**
|
|
136
|
+
* - The baseline document; each node's
|
|
137
|
+
* `dirty` is a JSON deep-compare of its value against this.
|
|
138
|
+
*/
|
|
139
|
+
initial?: any;
|
|
140
|
+
/**
|
|
141
|
+
* - Pointers
|
|
142
|
+
* the operator has visited.
|
|
143
|
+
*/
|
|
144
|
+
touched?: string[] | Record<string, boolean>;
|
|
145
|
+
/**
|
|
146
|
+
* - Whether a submit was attempted;
|
|
147
|
+
* echoed in the root summary (renderers typically surface every
|
|
148
|
+
* error once true).
|
|
149
|
+
*/
|
|
150
|
+
submitted?: boolean;
|
|
151
|
+
/**
|
|
152
|
+
* Server-reported messages by JSON Pointer; folded onto the matching
|
|
153
|
+
* nodes as `serverErrors`, never mixed into the client `errors`.
|
|
154
|
+
*/
|
|
155
|
+
serverErrors?: Record<string, string | string[]> | Array<{
|
|
156
|
+
pointer: string;
|
|
157
|
+
message: string;
|
|
158
|
+
}>;
|
|
159
|
+
/**
|
|
160
|
+
* - The submit task status
|
|
161
|
+
* (e.g. 'idle'/'pending'/'done'/'error'); echoed in the root summary.
|
|
162
|
+
*/
|
|
163
|
+
submitStatus?: string | null;
|
|
164
|
+
/**
|
|
165
|
+
* - The in-flight submit's request
|
|
166
|
+
* identity; echoed in the root summary.
|
|
167
|
+
*/
|
|
168
|
+
requestId?: any;
|
|
169
|
+
/**
|
|
170
|
+
* - Prefix for the stable accessible ids
|
|
171
|
+
* (default 'form'); ids are `<prefix>--<pointer segments joined
|
|
172
|
+
* with ->` and `'<prefix>--root'` for the root.
|
|
173
|
+
*/
|
|
174
|
+
idPrefix?: string;
|
|
175
|
+
};
|
|
176
|
+
export type FormSessionSummary = {
|
|
177
|
+
/**
|
|
178
|
+
* - Whether the current data differs from the
|
|
179
|
+
* initial document ANYWHERE.
|
|
180
|
+
*/
|
|
181
|
+
dirty: boolean;
|
|
182
|
+
/**
|
|
183
|
+
* - Every changed pointer between the
|
|
184
|
+
* initial document and the current data, in diff-walk (document)
|
|
185
|
+
* order. A member added or removed — including an explicit-`null`
|
|
186
|
+
* membership change and a shortened array tail — contributes the
|
|
187
|
+
* pointer of the added/removed location.
|
|
188
|
+
*/
|
|
189
|
+
dirtyPaths: string[];
|
|
190
|
+
submitted: boolean;
|
|
191
|
+
submitStatus: string | null;
|
|
192
|
+
requestId: any;
|
|
193
|
+
/**
|
|
194
|
+
* - Client-side error total.
|
|
195
|
+
*/
|
|
196
|
+
errorCount: number;
|
|
197
|
+
/**
|
|
198
|
+
* - Server-reported error total.
|
|
199
|
+
*/
|
|
200
|
+
serverErrorCount: number;
|
|
201
|
+
};
|
|
202
|
+
export type FormViewModelOptions = {
|
|
203
|
+
/**
|
|
204
|
+
* - Compiled rules from `compileFormRules`;
|
|
205
|
+
* when given, `x-form` state (visible/enabled/computed/asserts) is
|
|
206
|
+
* evaluated and folded into the tree.
|
|
207
|
+
*/
|
|
208
|
+
rules?: object;
|
|
209
|
+
/**
|
|
210
|
+
* - Run `validateAllFields` and
|
|
211
|
+
* fold the per-field errors in (default false: pristine forms show
|
|
212
|
+
* no errors until the app opts in).
|
|
213
|
+
*/
|
|
214
|
+
validateFields?: boolean;
|
|
215
|
+
/**
|
|
216
|
+
* - Compiled message catalog for error
|
|
217
|
+
* texts (compileMessageCatalog), default English.
|
|
218
|
+
*/
|
|
219
|
+
catalog?: object;
|
|
220
|
+
/**
|
|
221
|
+
* - A memo from
|
|
222
|
+
* `createRuleMemo`, reused across calls so only the rules a change
|
|
223
|
+
* can reach are re-evaluated. Keep one per form session; the tree it
|
|
224
|
+
* produces is the same either way.
|
|
225
|
+
*/
|
|
226
|
+
memo?: import('./rules.js').RuleMemo;
|
|
227
|
+
/**
|
|
228
|
+
* - Fold a form session
|
|
229
|
+
* (initial/touched/submitted/serverErrors/submit identity) into the
|
|
230
|
+
* tree: every node gains `id`/`describedBy`/`dirty`/`touched`/
|
|
231
|
+
* `serverErrors`, and the root gains a `session` summary. Absent, the
|
|
232
|
+
* tree is byte-identical to the sessionless shape.
|
|
233
|
+
*/
|
|
234
|
+
session?: FormSessionOptions;
|
|
235
|
+
};
|
|
236
|
+
/**
|
|
237
|
+
* One node of the render tree. Everything is plain JSON.
|
|
238
|
+
* @typedef {Object} FormViewNode
|
|
239
|
+
* @property {string} pointer - Concrete data pointer ('' for the root).
|
|
240
|
+
* @property {string} key - Property name, or the element index as a string.
|
|
241
|
+
* @property {string} label
|
|
242
|
+
* @property {string|null} description
|
|
243
|
+
* @property {string} kind - The field kind (model.js).
|
|
244
|
+
* @property {string} control - The rendering hint (model.js).
|
|
245
|
+
* @property {boolean} required
|
|
246
|
+
* @property {boolean} readOnly
|
|
247
|
+
* @property {boolean} enabled - `x-form.enabled`, defaulting true.
|
|
248
|
+
* @property {string|null} placeholder
|
|
249
|
+
* @property {any} value - The current value (`x-form.computed` wins);
|
|
250
|
+
* `null` when the field is absent from the data.
|
|
251
|
+
* @property {Array<{value: any, key: string, label: string, selected: boolean}>|null} options
|
|
252
|
+
* Select options, `selected` precomputed against the current value.
|
|
253
|
+
* `key` is the option's value as JSON text — what a string-valued
|
|
254
|
+
* control (a DOM `<option>`) can carry and hand back losslessly.
|
|
255
|
+
* @property {string} [json] - The value as indented JSON text, on
|
|
256
|
+
* `json`-control nodes only: the editable text for a structured
|
|
257
|
+
* value, precomputed like `options`.
|
|
258
|
+
* @property {string[]} errors - Localized messages: field validation
|
|
259
|
+
* first, then rule asserts.
|
|
260
|
+
* @property {boolean} element - True when this node is an array element
|
|
261
|
+
* (item or tuple slot). Writers need this: RFC 6902 `add` is
|
|
262
|
+
* set-or-replace for object members but INSERT for array indices, so
|
|
263
|
+
* element nodes must be written with `replace`.
|
|
264
|
+
* @property {boolean} removable - True for item-template elements
|
|
265
|
+
* (tuple slots are fixed).
|
|
266
|
+
* @property {FormViewNode[]|null} children - Object members.
|
|
267
|
+
* @property {FormViewNode[]|null} items - Array elements, expanded.
|
|
268
|
+
* @property {any} addValue - Starter value for a new array item
|
|
269
|
+
* (`createItemValue`); only on array nodes with an item template.
|
|
270
|
+
* @property {string} [id] - Stable accessible element id derived from
|
|
271
|
+
* the pointer (session forms only).
|
|
272
|
+
* @property {string|null} [describedBy] - The id of this node's error
|
|
273
|
+
* text (`aria-describedby` wiring), `null` when the node has no
|
|
274
|
+
* errors (session forms only).
|
|
275
|
+
* @property {boolean} [dirty] - Whether this node's value differs from
|
|
276
|
+
* the session's initial data — presence-aware: a member added or
|
|
277
|
+
* removed is dirty even when the compared values coincide as `null`
|
|
278
|
+
* (session forms only).
|
|
279
|
+
* @property {boolean} [touched] - Whether the session marked this
|
|
280
|
+
* pointer visited (session forms only).
|
|
281
|
+
* @property {string[]} [serverErrors] - Server-reported messages for
|
|
282
|
+
* this pointer, kept distinct from the client-side `errors` (session
|
|
283
|
+
* forms only).
|
|
284
|
+
* @property {FormSessionSummary} [session] - The root summary (root
|
|
285
|
+
* node of session forms only).
|
|
286
|
+
*/
|
|
287
|
+
/**
|
|
288
|
+
* The submit/draft session of a form: the lifecycle state around one
|
|
289
|
+
* edited document. Everything is JSON — the
|
|
290
|
+
* session lives in app state; this option only folds it into the tree.
|
|
291
|
+
*
|
|
292
|
+
* Hidden-field policy: fields excluded by `x-form` `visible` rules keep
|
|
293
|
+
* their values in the data — the view model never prunes; whether a
|
|
294
|
+
* submit drops them is a product decision made at the submit boundary.
|
|
295
|
+
*
|
|
296
|
+
* @typedef {Object} FormSessionOptions
|
|
297
|
+
* @property {any} [initial] - The baseline document; each node's
|
|
298
|
+
* `dirty` is a JSON deep-compare of its value against this.
|
|
299
|
+
* @property {string[] | Record<string, boolean>} [touched] - Pointers
|
|
300
|
+
* the operator has visited.
|
|
301
|
+
* @property {boolean} [submitted] - Whether a submit was attempted;
|
|
302
|
+
* echoed in the root summary (renderers typically surface every
|
|
303
|
+
* error once true).
|
|
304
|
+
* @property {Record<string, string | string[]> | Array<{ pointer: string, message: string }>} [serverErrors]
|
|
305
|
+
* Server-reported messages by JSON Pointer; folded onto the matching
|
|
306
|
+
* nodes as `serverErrors`, never mixed into the client `errors`.
|
|
307
|
+
* @property {string | null} [submitStatus] - The submit task status
|
|
308
|
+
* (e.g. 'idle'/'pending'/'done'/'error'); echoed in the root summary.
|
|
309
|
+
* @property {any} [requestId] - The in-flight submit's request
|
|
310
|
+
* identity; echoed in the root summary.
|
|
311
|
+
* @property {string} [idPrefix] - Prefix for the stable accessible ids
|
|
312
|
+
* (default 'form'); ids are `<prefix>--<pointer segments joined
|
|
313
|
+
* with ->` and `'<prefix>--root'` for the root.
|
|
314
|
+
*/
|
|
315
|
+
/**
|
|
316
|
+
* The root summary of a session form (`root.session`) — the
|
|
317
|
+
* navigation-guard authority: derived from a full JSON comparison of
|
|
318
|
+
* the initial document against the current data, independent of what
|
|
319
|
+
* is rendered, so removed members, hidden retained values and
|
|
320
|
+
* missing-versus-`null` membership changes all count. The visible
|
|
321
|
+
* per-node `dirty`/`errors` members remain the render-layer summary.
|
|
322
|
+
* @typedef {Object} FormSessionSummary
|
|
323
|
+
* @property {boolean} dirty - Whether the current data differs from the
|
|
324
|
+
* initial document ANYWHERE.
|
|
325
|
+
* @property {string[]} dirtyPaths - Every changed pointer between the
|
|
326
|
+
* initial document and the current data, in diff-walk (document)
|
|
327
|
+
* order. A member added or removed — including an explicit-`null`
|
|
328
|
+
* membership change and a shortened array tail — contributes the
|
|
329
|
+
* pointer of the added/removed location.
|
|
330
|
+
* @property {boolean} submitted
|
|
331
|
+
* @property {string | null} submitStatus
|
|
332
|
+
* @property {any} requestId
|
|
333
|
+
* @property {number} errorCount - Client-side error total.
|
|
334
|
+
* @property {number} serverErrorCount - Server-reported error total.
|
|
335
|
+
*/
|
|
336
|
+
/**
|
|
337
|
+
* Options for `buildFormViewModel`.
|
|
338
|
+
* @typedef {Object} FormViewModelOptions
|
|
339
|
+
* @property {object} [rules] - Compiled rules from `compileFormRules`;
|
|
340
|
+
* when given, `x-form` state (visible/enabled/computed/asserts) is
|
|
341
|
+
* evaluated and folded into the tree.
|
|
342
|
+
* @property {boolean} [validateFields] - Run `validateAllFields` and
|
|
343
|
+
* fold the per-field errors in (default false: pristine forms show
|
|
344
|
+
* no errors until the app opts in).
|
|
345
|
+
* @property {object} [catalog] - Compiled message catalog for error
|
|
346
|
+
* texts (compileMessageCatalog), default English.
|
|
347
|
+
* @property {import('./rules.js').RuleMemo} [memo] - A memo from
|
|
348
|
+
* `createRuleMemo`, reused across calls so only the rules a change
|
|
349
|
+
* can reach are re-evaluated. Keep one per form session; the tree it
|
|
350
|
+
* produces is the same either way.
|
|
351
|
+
* @property {FormSessionOptions} [session] - Fold a form session
|
|
352
|
+
* (initial/touched/submitted/serverErrors/submit identity) into the
|
|
353
|
+
* tree: every node gains `id`/`describedBy`/`dirty`/`touched`/
|
|
354
|
+
* `serverErrors`, and the root gains a `session` summary. Absent, the
|
|
355
|
+
* tree is byte-identical to the sessionless shape.
|
|
356
|
+
*/
|
|
357
|
+
/**
|
|
358
|
+
* Build the render tree for one form instant.
|
|
359
|
+
*
|
|
360
|
+
* @example
|
|
361
|
+
* const model = buildFormModel(schema);
|
|
362
|
+
* const rules = compileFormRules(model);
|
|
363
|
+
* const tree = buildFormViewModel(model, data, { rules, validateFields: true });
|
|
364
|
+
* // tree.children[0] -> { pointer: '/email', control: 'email',
|
|
365
|
+
* // value: 'a@b.c', errors: [], ... }
|
|
366
|
+
*
|
|
367
|
+
* @param {FormField} model - The field tree from `buildFormModel`.
|
|
368
|
+
* @param {any} data - The current form data.
|
|
369
|
+
* @param {FormViewModelOptions} [options]
|
|
370
|
+
* @returns {FormViewNode|null} The root render node (`null` only when a
|
|
371
|
+
* root rule hides the whole form).
|
|
372
|
+
*/
|
|
373
|
+
export declare function buildFormViewModel(model: FormField, data: any, options?: FormViewModelOptions): FormViewNode | null;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jarenjs/forms",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.34.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"types": "./dist/types/index.d.ts",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
},
|
|
27
27
|
"license": "MIT",
|
|
28
28
|
"engines": {
|
|
29
|
-
"node": ">=
|
|
29
|
+
"node": ">=24"
|
|
30
30
|
},
|
|
31
31
|
"publishConfig": {
|
|
32
32
|
"access": "public",
|
|
@@ -47,8 +47,9 @@
|
|
|
47
47
|
"prepack": "npm run build:types"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@jarenjs/core": "^0.
|
|
51
|
-
"@jarenjs/formats": "^0.
|
|
52
|
-
"@jarenjs/json": "^0.
|
|
50
|
+
"@jarenjs/core": "^0.34.2",
|
|
51
|
+
"@jarenjs/formats": "^0.34.2",
|
|
52
|
+
"@jarenjs/json": "^0.34.2",
|
|
53
|
+
"@jarenjs/validate": "^0.34.2"
|
|
53
54
|
}
|
|
54
55
|
}
|