@jarenjs/forms 0.72.3 → 0.75.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -1
- package/dist/types/messages.d.ts +3 -2
- package/package.json +5 -5
- package/src/messages.js +4 -2
- package/src/viewmodel.js +10 -8
package/README.md
CHANGED
|
@@ -189,7 +189,7 @@ These two externals are the whole vocabulary: any other free name in a rule is a
|
|
|
189
189
|
|
|
190
190
|
### Array item templates
|
|
191
191
|
|
|
192
|
-
A rule on an array item template (`/lines/-/amount`) compiles **once** and evaluates **per element** of the actual array, binding `$value`/`$pointer` per index — results are keyed by the expanded pointer (`/lines/2/amount`).
|
|
192
|
+
A rule on an array item template (`/lines/-/amount`) compiles **once** and evaluates **per element** of the actual array, binding `$value`/`$pointer` per index — results are keyed by the expanded pointer (`/lines/2/amount`). Rendering uses [`@jarenjs/json/jslt`](../json/docs/JSLT-FORMAT.md) rules over the composed view model. Composition retains the model, data and session cursors together; see the addressing measurement below.
|
|
193
193
|
|
|
194
194
|
### Schema literals in rules
|
|
195
195
|
|
|
@@ -383,6 +383,39 @@ Form data keeps plain JSON semantics — an untouched field is *absent*, not an
|
|
|
383
383
|
|
|
384
384
|
`buildFormViewModel(model, data, options)` composes everything above — the field tree, the current data, per-field validation and `x-form` rule state — into **one plain-JSON render tree**: the "computed view" layer this README promised. Each node carries `pointer`, `label`, `control`, `value` (`x-form.computed` wins, `null` when absent), precomputed select `options` (with `selected`), localized `errors`, `enabled`, and the write discipline flags (`element`: array elements must be written with RFC 6902 `replace`, since `add` inserts; `removable`; `addValue` from `createItemValue`). Rule-hidden fields are *excluded* — a renderer cannot leak hidden data by accident. Array item templates expand per data element with concrete pointers (`/lines/2/amount`), matching the pointer keys of `evaluateFormRules` and `validateAllFields`. A short tuple's `addValue` comes from its next prefix slot; the tail item template supplies starters only after the prefix is filled.
|
|
385
385
|
|
|
386
|
+
The composition walk carries the current **raw data cursor** and the
|
|
387
|
+
session's **initial cursor** down beside each field. It reads one pointer
|
|
388
|
+
suffix per descent instead of starting from the document root per field;
|
|
389
|
+
computed container values do not replace the raw cursor used by children.
|
|
390
|
+
Missing baseline ancestors stay missing, preserving dirty-state detection.
|
|
391
|
+
|
|
392
|
+
`$fold` can implement a second cursor with typed segments, but this alone
|
|
393
|
+
is not a reason to replace the composer. The executable experiment in
|
|
394
|
+
`benchmark/forms-composition.js` compares a fold-based JSLT rule with root
|
|
395
|
+
pointer getters and carried cursors on the same expanded fields. Its
|
|
396
|
+
parity tests include escaped keys, array indexes and missing ancestors.
|
|
397
|
+
|
|
398
|
+
<!--fact:forms.addressing-->
|
|
399
|
+
|
|
400
|
+
Measured on v24.19.0, linux/x64; median milliseconds per expanded-field projection. Compilation and field expansion are excluded from all contenders.
|
|
401
|
+
|
|
402
|
+
| Depth | Nodes | Root pointers | Carried cursors | Fold stylesheet |
|
|
403
|
+
|---|---|---|---|---|
|
|
404
|
+
| 1 | 33 | 0.0043 | 0.0040 | 0.0817 |
|
|
405
|
+
| 8 | 136 | 0.0314 | 0.0101 | 0.6325 |
|
|
406
|
+
| 24 | 536 | 0.2811 | 0.0319 | 5.1716 |
|
|
407
|
+
|
|
408
|
+
<!--/fact-->
|
|
409
|
+
|
|
410
|
+
The shallow timings are close and do not justify a universal speedup
|
|
411
|
+
claim; deeper trees benefit from carrying cursors. The fold stylesheet loses on every
|
|
412
|
+
fixture even with expansion excluded. These measurements cover addressing
|
|
413
|
+
only, not the complete session/validation/render pipeline. The production
|
|
414
|
+
composer therefore keeps the JS walk. A complete stylesheet replacement
|
|
415
|
+
still needs evidence for array-template expansion, hidden fields, enum
|
|
416
|
+
values, errors and session state; first-class functions are not a
|
|
417
|
+
prerequisite.
|
|
418
|
+
|
|
386
419
|
```javascript
|
|
387
420
|
const model = buildFormModel(schema);
|
|
388
421
|
const rules = compileFormRules(model);
|
|
@@ -478,3 +511,17 @@ Every subpath a consumer can import, derived from the manifest by
|
|
|
478
511
|
## Development
|
|
479
512
|
|
|
480
513
|
Unit tests live in `test/forms/` at the repository root. See the repository [README](../../README.md) for the full Jaren documentation, and the [ROADMAP](../../docs/ROADMAP.md) for planned forms work (rule dependency memoization, hidden-field pruning on submit, computed views through JSLT).
|
|
514
|
+
|
|
515
|
+
## Form chrome policy
|
|
516
|
+
|
|
517
|
+
`formChromeLabels(catalog)` supplies `addItem`, `removeItem` and
|
|
518
|
+
`jsonPlaceholder`, with English fallbacks and translations in every
|
|
519
|
+
locale pack. Pass them to `createFormView({ labels })`; the JSON hint is
|
|
520
|
+
both an empty-editor placeholder and an accessible description. A label
|
|
521
|
+
beginning with `$` remains literal text.
|
|
522
|
+
|
|
523
|
+
The add/remove glyphs and required marker are decoration, not message
|
|
524
|
+
keys: customize `addLabel`, `removeLabel` and `requiredMarker` on the
|
|
525
|
+
stylesheet. Required controls carry `aria-required`; the decorative
|
|
526
|
+
marker stays hidden from assistive technology. JSON syntax and the name
|
|
527
|
+
“JSON” retain their wire spelling; instructions around them are localized.
|
package/dist/types/messages.d.ts
CHANGED
|
@@ -47,18 +47,19 @@ export declare function formatDisplayName(catalog: Readonly<Record<string, (para
|
|
|
47
47
|
export declare function renderFormsMessage(catalog: Readonly<Record<string, (params: object, error?: object) => string>> | undefined, msgid: string, params: object): string;
|
|
48
48
|
/**
|
|
49
49
|
* The localized chrome strings a form renderer needs: the accessible
|
|
50
|
-
* names of the array add/remove buttons.
|
|
50
|
+
* names of the array add/remove buttons and the JSON editor's hint.
|
|
51
51
|
*
|
|
52
52
|
* The stylesheet that renders a form is plain JSON built once, so it
|
|
53
53
|
* cannot look anything up at render time — the host resolves these and
|
|
54
54
|
* hands them to `createFormView`. Kept next to the error catalog on
|
|
55
55
|
* purpose: one keyspace, one parity gate across every locale pack.
|
|
56
56
|
* @param {Readonly<Record<string, (params: object, error?: object) => string>>} [catalog] - A compiled catalog, or undefined for English
|
|
57
|
-
* @returns {{addItem: string, removeItem: string}}
|
|
57
|
+
* @returns {{addItem: string, removeItem: string, jsonPlaceholder: string}}
|
|
58
58
|
* @example
|
|
59
59
|
* createFormView({ labels: formChromeLabels(catalogs[locale]) });
|
|
60
60
|
*/
|
|
61
61
|
export declare function formChromeLabels(catalog?: Readonly<Record<string, (params: object, error?: object) => string>>): {
|
|
62
62
|
addItem: string;
|
|
63
63
|
removeItem: string;
|
|
64
|
+
jsonPlaceholder: string;
|
|
64
65
|
};
|
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.75.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"types": "./dist/types/index.d.ts",
|
|
@@ -47,9 +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.
|
|
53
|
-
"@jarenjs/validate": "^0.
|
|
50
|
+
"@jarenjs/core": "^0.75.0",
|
|
51
|
+
"@jarenjs/formats": "^0.75.0",
|
|
52
|
+
"@jarenjs/json": "^0.75.0",
|
|
53
|
+
"@jarenjs/validate": "^0.75.0"
|
|
54
54
|
}
|
|
55
55
|
}
|
package/src/messages.js
CHANGED
|
@@ -63,6 +63,7 @@ export const formsMessagesEn = {
|
|
|
63
63
|
// message like every other string an operator can hear.
|
|
64
64
|
'form/addItem': 'Add item',
|
|
65
65
|
'form/removeItem': 'Remove item',
|
|
66
|
+
'form/jsonPlaceholder': 'Enter a JSON value',
|
|
66
67
|
};
|
|
67
68
|
|
|
68
69
|
/** The compiled built-in English catalog (module-level singleton). */
|
|
@@ -127,14 +128,14 @@ export function renderFormsMessage(catalog, msgid, params) {
|
|
|
127
128
|
|
|
128
129
|
/**
|
|
129
130
|
* The localized chrome strings a form renderer needs: the accessible
|
|
130
|
-
* names of the array add/remove buttons.
|
|
131
|
+
* names of the array add/remove buttons and the JSON editor's hint.
|
|
131
132
|
*
|
|
132
133
|
* The stylesheet that renders a form is plain JSON built once, so it
|
|
133
134
|
* cannot look anything up at render time — the host resolves these and
|
|
134
135
|
* hands them to `createFormView`. Kept next to the error catalog on
|
|
135
136
|
* purpose: one keyspace, one parity gate across every locale pack.
|
|
136
137
|
* @param {Readonly<Record<string, (params: object, error?: object) => string>>} [catalog] - A compiled catalog, or undefined for English
|
|
137
|
-
* @returns {{addItem: string, removeItem: string}}
|
|
138
|
+
* @returns {{addItem: string, removeItem: string, jsonPlaceholder: string}}
|
|
138
139
|
* @example
|
|
139
140
|
* createFormView({ labels: formChromeLabels(catalogs[locale]) });
|
|
140
141
|
*/
|
|
@@ -142,6 +143,7 @@ export function formChromeLabels(catalog = undefined) {
|
|
|
142
143
|
return {
|
|
143
144
|
addItem: renderFormsMessage(catalog, 'form/addItem', {}),
|
|
144
145
|
removeItem: renderFormsMessage(catalog, 'form/removeItem', {}),
|
|
146
|
+
jsonPlaceholder: renderFormsMessage(catalog, 'form/jsonPlaceholder', {}),
|
|
145
147
|
};
|
|
146
148
|
}
|
|
147
149
|
|
package/src/viewmodel.js
CHANGED
|
@@ -184,7 +184,7 @@ export function buildFormViewModel(model, data, options = {}) {
|
|
|
184
184
|
const session = options.session !== undefined
|
|
185
185
|
? compileSession(options.session, data)
|
|
186
186
|
: null;
|
|
187
|
-
const root = buildNode(model, '', data, ruleState, fieldErrors, false, false, session);
|
|
187
|
+
const root = buildNode(model, '', data, ruleState, fieldErrors, false, false, session, session?.initial);
|
|
188
188
|
if (root !== null && session !== null) {
|
|
189
189
|
root.session = {
|
|
190
190
|
dirty: session.dirtyPaths.length > 0,
|
|
@@ -286,19 +286,19 @@ function pointerId(prefix, pointer) {
|
|
|
286
286
|
* @param {string} pointer - The concrete RFC 6901 pointer of this node
|
|
287
287
|
* (segments encoded with `encodeJSONPointerSegment`, the walk
|
|
288
288
|
* convention shared with the model, rule and validation pointers).
|
|
289
|
-
* @param {any}
|
|
289
|
+
* @param {any} raw - The data cursor at this field (before computed values).
|
|
290
290
|
* @param {Record<string, any>} ruleState
|
|
291
291
|
* @param {Record<string, any>} fieldErrors
|
|
292
292
|
* @param {boolean} element
|
|
293
293
|
* @param {boolean} removable
|
|
294
294
|
* @param {ReturnType<typeof compileSession> | null} [session]
|
|
295
|
+
* @param {any} [initialValue] - The baseline cursor at this field.
|
|
295
296
|
* @returns {FormViewNode|null}
|
|
296
297
|
*/
|
|
297
|
-
function buildNode(field, pointer,
|
|
298
|
+
function buildNode(field, pointer, raw, ruleState, fieldErrors, element, removable, session = null, initialValue = undefined) {
|
|
298
299
|
const rs = ruleState[pointer];
|
|
299
300
|
if (rs !== undefined && rs.visible === false) return null;
|
|
300
301
|
|
|
301
|
-
const raw = getValueAtPointer(data, pointer);
|
|
302
302
|
let value = field.kind === 'const' ? field.constValue : raw;
|
|
303
303
|
if (rs !== undefined && rs.computed !== undefined) value = rs.computed;
|
|
304
304
|
|
|
@@ -369,7 +369,6 @@ function buildNode(field, pointer, data, ruleState, fieldErrors, element, remova
|
|
|
369
369
|
if (session.hasInitial) {
|
|
370
370
|
// presence-aware: adding or removing a member whose value is
|
|
371
371
|
// null is a membership change, so it is dirty
|
|
372
|
-
const initialValue = getValueAtPointer(session.initial, pointer);
|
|
373
372
|
node.dirty = (initialValue === undefined) !== (raw === undefined)
|
|
374
373
|
|| (initialValue !== undefined && !equalsJson(initialValue, raw));
|
|
375
374
|
}
|
|
@@ -381,9 +380,11 @@ function buildNode(field, pointer, data, ruleState, fieldErrors, element, remova
|
|
|
381
380
|
if (field.children !== null && field.children !== undefined) {
|
|
382
381
|
const children = [];
|
|
383
382
|
for (const child of field.children) {
|
|
383
|
+
const segment = `/${encodeJSONPointerSegment(child.key)}`;
|
|
384
384
|
const built = buildNode(
|
|
385
|
-
child,
|
|
386
|
-
fieldErrors, false, false, session
|
|
385
|
+
child, pointer + segment, getValueAtPointer(raw, segment), ruleState,
|
|
386
|
+
fieldErrors, false, false, session,
|
|
387
|
+
session?.hasInitial ? getValueAtPointer(initialValue, segment) : undefined);
|
|
387
388
|
if (built !== null) children.push(built);
|
|
388
389
|
}
|
|
389
390
|
node.children = children;
|
|
@@ -404,7 +405,8 @@ function buildNode(field, pointer, data, ruleState, fieldErrors, element, remova
|
|
|
404
405
|
// still valid is `minItems`' answer to give, not this layer's.
|
|
405
406
|
const removable = !isTupleSlot || i === array.length - 1;
|
|
406
407
|
const built = buildNode(
|
|
407
|
-
template, `${pointer}/${i}`,
|
|
408
|
+
template, `${pointer}/${i}`, array[i], ruleState, fieldErrors, true, removable, session,
|
|
409
|
+
session?.hasInitial ? getValueAtPointer(initialValue, `/${i}`) : undefined);
|
|
408
410
|
if (built !== null) items.push(built);
|
|
409
411
|
}
|
|
410
412
|
node.items = items;
|