@jarenjs/forms 0.9.2 → 0.34.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.
@@ -12,11 +12,26 @@ export type RuleResult = {
12
12
  */
13
13
  computed?: any;
14
14
  /**
15
- * `[{ keyword: 'x-form/assert', message }]` when the `assert` rule fails
16
- * (the validateField error shape, so error rendering works unchanged)
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: string | null;
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 `$query` lands on the ROOT schema (where the query input `$` is the
111
- * instance root, matching the rule context), with each assert wrapped to
112
- * rebuild its bindings: `value` binds to the field's location, `pointer`
113
- * to its pointer string. An assert on an array item template quantifies
114
- * with `$every` over the actual elements (`pointer` then stays the
115
- * template pointer - element indexes are a render-time notion). Multiple
116
- * asserts conjoin under `$and`; an existing root `$query` is preserved by
117
- * wrapping the new one in an `allOf` branch.
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`, or the
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
@@ -4,7 +4,15 @@ export type FieldError = {
4
4
  */
5
5
  keyword: string;
6
6
  /**
7
- * - Human readable message
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', message: 'Must be a valid email' }]
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.9.2",
4
+ "version": "0.34.0",
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": ">=22"
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.9.2",
51
- "@jarenjs/formats": "^0.9.2",
52
- "@jarenjs/json": "^0.9.2"
50
+ "@jarenjs/core": "^0.34.0",
51
+ "@jarenjs/formats": "^0.34.0",
52
+ "@jarenjs/json": "^0.34.0",
53
+ "@jarenjs/validate": "^0.34.0"
53
54
  }
54
55
  }