@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/README.md
CHANGED
|
@@ -61,7 +61,7 @@ const result = validate(data); // { valid, errors: [{ instancePath, keyword, mes
|
|
|
61
61
|
| `pointer` | JSON pointer into the data (`/user/name`) |
|
|
62
62
|
| `label` | `title` or a humanized property name (`firstName` → "First Name") |
|
|
63
63
|
| `kind` | `string` `number` `integer` `boolean` `enum` `const` `object` `array` |
|
|
64
|
-
| `control` | Rendering hint: `text` `email` `url` `password` `textarea` `number` `checkbox` `select` `date` `color` `json` |
|
|
64
|
+
| `control` | Rendering hint: `text` `email` `url` `password` `textarea` `number` `checkbox` `select` `date` `datetime-local` `time` `color` `json` |
|
|
65
65
|
| `required` | Whether the parent object requires this property |
|
|
66
66
|
| `constraints` | `minLength`/`maxLength`/`pattern`/`format`/`minimum`/`maximum`/`multipleOf`/`minItems`/... |
|
|
67
67
|
| `rules` | The raw `x-form` rules annotation, if any (see below) |
|
|
@@ -71,6 +71,8 @@ const result = validate(data); // { valid, errors: [{ instancePath, keyword, mes
|
|
|
71
71
|
|
|
72
72
|
Field kinds are inferred from structural keywords when `type` is absent, and `format` maps to input controls and placeholders through the same registry the preemptive validation uses (`getFormatInfo`).
|
|
73
73
|
|
|
74
|
+
Which date formats get a **native** control is decided by the offset, not by convenience. HTML's `datetime-local` and `time` inputs cannot produce one, and RFC 3339 requires one — binding them to `date-time`/`time` would make the control emit values its own schema rejects, so those stay text inputs. The `iso-date-time`/`iso-time` formats leave the offset optional and are exactly what those inputs spell, so they map losslessly. `formatMinimum`/`formatMaximum` reach the field as constraints and become the control's `min`/`max`, so the picker itself refuses an out-of-range date; HTML has no exclusive date bounds, so `formatExclusive*` stays a submit-time check.
|
|
75
|
+
|
|
74
76
|
## Layer 1 — preemptive per-field validation
|
|
75
77
|
|
|
76
78
|
`validateField(field, value)` returns `[{ keyword, message }]` using `@jarenjs/core` directly:
|
|
@@ -170,9 +172,9 @@ The same constraint can be spelled twice — `x-form.assert` for keystroke feedb
|
|
|
170
172
|
```javascript
|
|
171
173
|
import { formRulesToQueryAssertions } from '@jarenjs/forms';
|
|
172
174
|
|
|
173
|
-
// pure schema-to-schema transform: every x-form.assert
|
|
174
|
-
//
|
|
175
|
-
//
|
|
175
|
+
// pure schema-to-schema transform: every x-form.assert becomes its own
|
|
176
|
+
// allOf branch { $query, errorMessage } on the root, with value/pointer
|
|
177
|
+
// rebound to the field's location and the rule's message carried along
|
|
176
178
|
const submitSchema = formRulesToQueryAssertions(schema);
|
|
177
179
|
|
|
178
180
|
import { JarenValidator } from '@jarenjs/validate'; // app-side
|
|
@@ -180,17 +182,212 @@ const validate = new JarenValidator().compile(submitSchema);
|
|
|
180
182
|
validate({ company: 'ACME', vatId: '' }); // false - the vatId assert, now authoritative
|
|
181
183
|
```
|
|
182
184
|
|
|
183
|
-
|
|
185
|
+
Each branch's `errorMessage` carries the rule's `message` (inline string
|
|
186
|
+
or `$msgid` form) with `params` merged over `{ pointer: <field pointer> }`
|
|
187
|
+
— so every submit-time `$query` failure names its owning field in
|
|
188
|
+
`params.pointer`, letting the UI place root-level `$query` errors onto
|
|
189
|
+
fields, and renders the **same text** as the keystroke path (see below).
|
|
190
|
+
|
|
191
|
+
**One rule, one meaning.** Whatever the keystroke evaluation says about a
|
|
192
|
+
document, submit says too — the copy is not allowed to change what an author
|
|
193
|
+
wrote. Three things enforce that, and each is a place the naive copy went
|
|
194
|
+
wrong:
|
|
195
|
+
|
|
196
|
+
- an **absent field binds `null`**, not the empty sequence, so `$ne`/`$eq`
|
|
197
|
+
cannot mean opposite things on the two sides (the binding is wrapped in
|
|
198
|
+
`$default`);
|
|
199
|
+
- an **item-template assert quantifies over the ELEMENTS**, not over the
|
|
200
|
+
selected leaf values — quantifying over leaves silently skips an element
|
|
201
|
+
that lacks the member, where the keystroke path evaluates it with `null`;
|
|
202
|
+
- an assert on a field with a `visible` rule is **guarded by it**, holding
|
|
203
|
+
vacuously while the field is hidden — which is what the keystroke path
|
|
204
|
+
already does, since `buildFormViewModel` drops hidden nodes and their
|
|
205
|
+
errors never render or count.
|
|
206
|
+
|
|
207
|
+
One divergence remains, and is inherent: `$pointer` for template elements
|
|
208
|
+
stays the template pointer, because element indexes are a render-time notion.
|
|
209
|
+
|
|
210
|
+
A hidden field's *value* is a separate question, and the answer is explicit:
|
|
211
|
+
values are kept while editing — a field that reappears must not have
|
|
212
|
+
forgotten what the operator typed — and dropped only at the submit boundary,
|
|
213
|
+
by a caller who asks:
|
|
214
|
+
|
|
215
|
+
```javascript
|
|
216
|
+
import { pruneHiddenValues } from '@jarenjs/forms';
|
|
217
|
+
|
|
218
|
+
const submitted = pruneHiddenValues(compiledRules, session.data);
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Visibility is evaluated once, against the incoming document. Hidden array
|
|
222
|
+
elements are removed and their siblings renumber, which is right for a
|
|
223
|
+
document being sent — and means the returned pointers no longer line up with
|
|
224
|
+
the ones the view model rendered.
|
|
225
|
+
|
|
226
|
+
## Messages & i18n
|
|
227
|
+
|
|
228
|
+
Every `FieldError` is structured: `{ keyword, params, msgid, message }`.
|
|
229
|
+
The `msgid` is `form/<keyword>` (field checks) or `x-form/assert` /
|
|
230
|
+
the author's `$msgid` (rules); `message` is rendered eagerly —
|
|
231
|
+
failure-only, cheap — through a **catalog** (see
|
|
232
|
+
[ERROR-MESSAGES.md](../validate/docs/ERROR-MESSAGES.md) for the shared
|
|
233
|
+
contract). `validateField`, `validateAllFields` and `evaluateFormRules`
|
|
234
|
+
take an optional compiled catalog, default English:
|
|
235
|
+
|
|
236
|
+
```javascript
|
|
237
|
+
import { validateAllFields, compileMessageCatalog } from '@jarenjs/forms';
|
|
238
|
+
import { nl } from '@jarenjs/locales';
|
|
239
|
+
|
|
240
|
+
const catalog = compileMessageCatalog(nl);
|
|
241
|
+
const errors = validateAllFields(model, data, catalog);
|
|
242
|
+
// errors['/name'][0].message === 'Dit veld is verplicht'
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### MessageSpec in `x-form.message`
|
|
246
|
+
|
|
247
|
+
A rule's `message` may be a plain string (backward compatible — an inline
|
|
248
|
+
template, `{pointer}` etc. interpolated) or a `$msgid` spec resolving
|
|
249
|
+
through the catalog:
|
|
250
|
+
|
|
251
|
+
```javascript
|
|
252
|
+
{ "x-form": {
|
|
253
|
+
"assert": { "$or": [{ "$eq": ["$.company", ""] }, { "$ne": ["$.vatId", ""] }] },
|
|
254
|
+
"message": { "$msgid": "checkout.vat-required",
|
|
255
|
+
"message": "A VAT id is required for companies" } } }
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### One rule, one message — keystroke and submit
|
|
259
|
+
|
|
260
|
+
The same rule renders the **identical string** per keystroke
|
|
261
|
+
(`evaluateFormRules`) and at submit (the transformed schema's `$query`
|
|
262
|
+
failure through the validator), in every locale:
|
|
263
|
+
|
|
264
|
+
```javascript
|
|
265
|
+
const compiled = compileFormRules(buildFormModel(schema));
|
|
266
|
+
evaluateFormRules(compiled, data, catalog); // keystroke: Dutch text
|
|
267
|
+
|
|
268
|
+
const validate = new JarenValidator({ collectErrors: true })
|
|
269
|
+
.compile(formRulesToQueryAssertions(schema)); // app-side
|
|
270
|
+
const result = validate(data);
|
|
271
|
+
localizeErrors(result.errors, catalog); // submit: the same Dutch text
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### Static text — the l10n surface everyone forgets
|
|
275
|
+
|
|
276
|
+
Labels, descriptions, placeholders and enum option labels resolve once at
|
|
277
|
+
model-build time; `buildFormModel` takes a `t` hook (default: identity)
|
|
278
|
+
receiving role-qualified message ids built from each field's base — its
|
|
279
|
+
`x-msgid` annotation or its data pointer:
|
|
280
|
+
|
|
281
|
+
```javascript
|
|
282
|
+
const staticNl = {
|
|
283
|
+
'/firstName#label': 'Voornaam',
|
|
284
|
+
'account.country#label': 'Land',
|
|
285
|
+
'account.country#enum/nl': 'Nederland',
|
|
286
|
+
};
|
|
287
|
+
const model = buildFormModel(schema, {
|
|
288
|
+
t: (msgid, fallback) => staticNl[msgid] ?? fallback,
|
|
289
|
+
});
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Enum option labels come from the JSON Schema idiom
|
|
293
|
+
`oneOf: [{ "const": "nl", "title": "Netherlands" }, ...]` (treated as an
|
|
294
|
+
enum with per-option titles) or `String(value)`, each through
|
|
295
|
+
`t('<base>#enum/<value>', fallback)`; the labels land on
|
|
296
|
+
`field.enumLabels`, parallel to `field.enumValues`.
|
|
184
297
|
|
|
185
298
|
## Data helpers
|
|
186
299
|
|
|
187
|
-
Form data keeps plain JSON semantics — an untouched field is *absent*, not an empty string. Pointers parse
|
|
300
|
+
Form data keeps plain JSON semantics — an untouched field is *absent*, not an empty string. Pointers parse, read AND write through the [`@jarenjs/json`](../json) engines (RFC 6901, one implementation repo-wide): reads hit a compiled-getter cache and allocate nothing, and writes run on the same copy-on-write kernel as the patch module (`compileJSONPointerSetter` with `parents: 'create'`), so untouched siblings are shared by reference on every keystroke — which feeds the JSLT memo and the view patcher's reference-equality fast path downstream:
|
|
188
301
|
|
|
189
302
|
- `createInitialData(model)` — defaults and `const` values filled in, everything else absent
|
|
190
303
|
- `parseFieldInput(field, raw)` — input coercion (`''` → undefined, numeric strings → numbers, enum options → typed values)
|
|
191
304
|
- `getValueAtPointer` / `setValueAtPointer` / `appendItem` / `removeItemAt` — immutable updates addressed by JSON pointer
|
|
192
305
|
- `createItemValue(field.item)` — starter value for a new array item
|
|
193
306
|
|
|
307
|
+
## The view model — one render tree per instant
|
|
308
|
+
|
|
309
|
+
`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`.
|
|
310
|
+
|
|
311
|
+
```javascript
|
|
312
|
+
const model = buildFormModel(schema);
|
|
313
|
+
const rules = compileFormRules(model);
|
|
314
|
+
const tree = buildFormViewModel(model, data, { rules, validateFields: true, catalog });
|
|
315
|
+
// tree.children[0] -> { pointer: '/email', control: 'email', value: 'a@b',
|
|
316
|
+
// errors: ['Must be a valid email'], ... }
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
Render it with anything — a React component walking the tree, or **no framework at all**: the standard form rules of [`@jarenjs/app`](../app) are a shipped JSLT rule set that dispatches over exactly this shape and produces [`@jarenjs/view`](../view) vnodes, closing the loop from JSON Schema to live DOM without a single hand-written render function.
|
|
320
|
+
|
|
321
|
+
Two node members exist because a **DOM control's value is a string** and not
|
|
322
|
+
every field's value is: each select option carries `key`, its value as JSON
|
|
323
|
+
text, and a `json`-control node carries `json`, its value as indented JSON
|
|
324
|
+
text. A renderer puts those in the control and hands them back verbatim;
|
|
325
|
+
`@jarenjs/app`'s `formEventFields()` decodes them. That keeps a `enum: [1, 2]`
|
|
326
|
+
selection a number instead of `"2"`.
|
|
327
|
+
|
|
328
|
+
### Re-evaluating only what changed
|
|
329
|
+
|
|
330
|
+
Rules compile once and run per keystroke. On a wide form most of that work is
|
|
331
|
+
wasted — a keystroke in one field cannot change what a rule reading two other
|
|
332
|
+
fields concludes — so pass a **memo** and only the reachable rules re-run:
|
|
333
|
+
|
|
334
|
+
```javascript
|
|
335
|
+
import { createRuleMemo } from '@jarenjs/forms';
|
|
336
|
+
|
|
337
|
+
const memo = createRuleMemo(); // one per form session
|
|
338
|
+
const tree = buildFormViewModel(model, data, { rules, memo, catalog });
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
Each rule's dependencies are derived at compile time from the root-anchored
|
|
342
|
+
paths in its query documents (plus its own location). That over-approximates
|
|
343
|
+
every read, and soundly: the only way into the document is such a path, and a
|
|
344
|
+
`$let`/`$for` variable can only hold what one of them produced.
|
|
345
|
+
|
|
346
|
+
The memo diffs the previous document against the new one — reference-equal
|
|
347
|
+
subtrees are skipped whole, so an immutable edit costs O(change). A host that
|
|
348
|
+
already knows what it wrote can skip even that with `memo.touch(pointer)`,
|
|
349
|
+
which is a promise as much as an optimization: touch one pointer while
|
|
350
|
+
changing another and the rules you did not name keep stale results.
|
|
351
|
+
|
|
352
|
+
Measured on a 200-rule form (one `visible` + one `assert` each):
|
|
353
|
+
|
|
354
|
+
| tick | cost |
|
|
355
|
+
|---|---|
|
|
356
|
+
| full evaluation | ~54 µs |
|
|
357
|
+
| memo, one field changed (diffed) | ~13 µs |
|
|
358
|
+
| memo, one field changed (declared) | ~4.7 µs |
|
|
359
|
+
| memo, a field every rule reads | ~58 µs |
|
|
360
|
+
|
|
361
|
+
The last row is the honest one: when a change reaches every rule there is
|
|
362
|
+
nothing to skip, and the memo's bookkeeping makes it slightly *slower* than
|
|
363
|
+
evaluating straight through. It pays when a form is wide and its rules are
|
|
364
|
+
mostly local — which is what a large form usually is, and exactly when the
|
|
365
|
+
full evaluation starts to hurt.
|
|
366
|
+
|
|
367
|
+
### The form session — submit/draft lifecycle around one document
|
|
368
|
+
|
|
369
|
+
Pass `options.session` and every node additionally carries `id` (a stable, **injective** accessible element id derived from the pointer — distinct pointers can never collide, because every character outside `[A-Za-z0-9]` (`-` and `_` themselves included) is escaped as `_<codepoint>_` *before* the segments are joined on `-`, so a member name containing the separator or an escape cannot forge another pointer's id: `/a/b` → `<prefix>--f-a-b`, `/a-b` → `<prefix>--f-a_45_b`. Member ids carry the `f-` marker so the `<prefix>--root` sentinel is disjoint from every member, a member literally named `root` included. Write the same escaping in your `<label for=…>` if you build an id by hand), `describedBy` (the id of the node's error text, or `null`), `dirty` (presence-aware deep compare against `session.initial` — adding or removing a member counts even when both sides read back `null`), `touched`, and `serverErrors` (kept distinct from client `errors`). The root gains a `session` summary:
|
|
370
|
+
|
|
371
|
+
```javascript
|
|
372
|
+
const tree = buildFormViewModel(model, data, {
|
|
373
|
+
rules, validateFields: true,
|
|
374
|
+
session: {
|
|
375
|
+
initial, // the baseline document
|
|
376
|
+
touched: ['/email'], // pointers the operator visited
|
|
377
|
+
submitted: true,
|
|
378
|
+
submitStatus: 'pending', // echoed verbatim
|
|
379
|
+
requestId: 'req-7', // the in-flight submit's identity
|
|
380
|
+
serverErrors: { '/email': ['Al in gebruik.'] },
|
|
381
|
+
idPrefix: 'customer', // ids become customer--…
|
|
382
|
+
},
|
|
383
|
+
});
|
|
384
|
+
tree.session;
|
|
385
|
+
// { dirty, dirtyPaths, submitted, submitStatus, requestId,
|
|
386
|
+
// errorCount, serverErrorCount }
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
`session.dirty`/`session.dirtyPaths` are the **navigation-guard authority**: they come from a full JSON diff of `initial` against the current data — independent of what is rendered — so removed array tails, members removed or added (including explicit `null`), and values retained under rule-hidden fields all count, each contributing its pointer. The diff walks **own** keys only (`Object.hasOwn`), so hostile-but-legal member names like `constructor` or a JSON-parsed `__proto__` diff as data, never through the prototype chain, and it RFC 6901-encodes every member name as it builds the pointer (`~` → `~0`, `/` → `~1`, the same `encodeJSONPointerSegment` the model, rule and validation walks use) — so a key containing a slash or a tilde cannot collide with a nested path, and the entries of `dirtyPaths` feed straight back into `getValueAtPointer`. To keep the authority unconditional, a root-level `x-form` `visible` rule is rejected by `compileFormRules` with a `TypeError` — hiding the whole form would null the render tree *and* its summary; gate whole-form visibility at the mount boundary instead. The per-node `dirty`/`errors` members remain the render-layer, visible-only summary. Without `session`, the tree is byte-identical to the sessionless shape.
|
|
390
|
+
|
|
194
391
|
## Development
|
|
195
392
|
|
|
196
|
-
Unit tests live in `test/forms/` at the repository root. See the repository [README](../../README.md) for the full Jaren documentation, and the [ROADMAP](../../ROADMAP.md) for planned forms work (rule dependency memoization, hidden-field pruning on submit, computed views through JSLT).
|
|
393
|
+
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).
|
package/dist/types/data.d.ts
CHANGED
|
@@ -15,10 +15,11 @@ export declare function parsePointer(pointer: string): string[];
|
|
|
15
15
|
export declare function getValueAtPointer(data: any, pointer: string): any;
|
|
16
16
|
/**
|
|
17
17
|
* Return a copy of `data` with the value at `pointer` replaced.
|
|
18
|
-
* Setting `undefined` REMOVES the
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
18
|
+
* Setting `undefined` REMOVES the location (deleting something that does
|
|
19
|
+
* not exist is a no-op returning `data` unchanged). Missing intermediate
|
|
20
|
+
* containers are created (objects for name segments, arrays for numeric
|
|
21
|
+
* segments), and untouched siblings are shared by reference — the same
|
|
22
|
+
* copy-on-write engine as `@jarenjs/json`'s patch and write modules.
|
|
22
23
|
* @param {any} data
|
|
23
24
|
* @param {string} pointer
|
|
24
25
|
* @param {any} value
|
|
@@ -43,6 +44,24 @@ export declare function removeItemAt(data: any, pointer: string, index: number):
|
|
|
43
44
|
* @returns {any}
|
|
44
45
|
*/
|
|
45
46
|
export declare function appendItem(data: any, pointer: string, value: any): any;
|
|
47
|
+
/**
|
|
48
|
+
* Every pointer whose value differs between two documents, in
|
|
49
|
+
* document order.
|
|
50
|
+
*
|
|
51
|
+
* Membership is significant: an added or removed member (or array tail
|
|
52
|
+
* slot) contributes its pointer even when both sides read back as
|
|
53
|
+
* `null` through a pointer lookup. Reference-equal subtrees are skipped
|
|
54
|
+
* whole, so over copy-on-write edits — which is how every writer in
|
|
55
|
+
* this package produces its next document — the walk costs O(change),
|
|
56
|
+
* not O(document).
|
|
57
|
+
*
|
|
58
|
+
* Two callers rely on it: the session's navigation-guard evidence
|
|
59
|
+
* (`dirtyPaths`) and the rule memo's invalidation set.
|
|
60
|
+
* @param {any} previous
|
|
61
|
+
* @param {any} current
|
|
62
|
+
* @returns {string[]}
|
|
63
|
+
*/
|
|
64
|
+
export declare function changedPointers(previous: any, current: any): string[];
|
|
46
65
|
/**
|
|
47
66
|
* Create initial data for a form model: schema defaults and const values
|
|
48
67
|
* are filled in, everything else stays absent.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export type RuleDependencies = string[];
|
|
2
|
+
/**
|
|
3
|
+
* The dependency set of a rule: pointer prefixes, or `ALL_POINTERS` for
|
|
4
|
+
* "any change matters" (a query that reads the whole document).
|
|
5
|
+
* @typedef {string[]} RuleDependencies
|
|
6
|
+
*/
|
|
7
|
+
/** A rule that reads the root depends on everything. */
|
|
8
|
+
export declare const ALL_POINTERS: string[];
|
|
9
|
+
/**
|
|
10
|
+
* Collect the data dependencies of one rule query document.
|
|
11
|
+
* @param {any} doc - The authored rule document
|
|
12
|
+
* @returns {RuleDependencies} Pointer prefixes, deduplicated
|
|
13
|
+
*/
|
|
14
|
+
export declare function queryDependencies(doc: any): RuleDependencies;
|
|
15
|
+
/**
|
|
16
|
+
* Merge dependency sets, collapsing to {@link ALL_POINTERS} when any of
|
|
17
|
+
* them reads the root and dropping prefixes another already covers.
|
|
18
|
+
* @param {RuleDependencies[]} sets
|
|
19
|
+
* @returns {RuleDependencies}
|
|
20
|
+
*/
|
|
21
|
+
export declare function mergeDependencies(sets: RuleDependencies[]): RuleDependencies;
|
|
22
|
+
/**
|
|
23
|
+
* Whether a change at `changed` can affect a rule depending on `dep`.
|
|
24
|
+
* True in BOTH nesting directions: a change inside a dependency
|
|
25
|
+
* (`/a` vs `/a/b`) alters what the rule reads, and a change ABOVE one
|
|
26
|
+
* (`/a/b` vs `/a`) can replace the container it reads through.
|
|
27
|
+
* @param {string} dep @param {string} changed
|
|
28
|
+
* @returns {boolean}
|
|
29
|
+
*/
|
|
30
|
+
export declare function dependencyTouched(dep: string, changed: string): boolean;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
* JSON pointer through the @jarenjs/json compiled pointer engine.
|
|
12
12
|
*/
|
|
13
13
|
export { buildFormModel, resolveSchema, getFieldKind, humanizeKey, escapePointerKey, } from './model.js';
|
|
14
|
-
export { compileFormRules, evaluateFormRules, formRulesToQueryAssertions, } from './rules.js';
|
|
14
|
+
export { compileFormRules, evaluateFormRules, formRulesToQueryAssertions, pruneHiddenValues, createRuleMemo, } from './rules.js';
|
|
15
15
|
export { validateField, validateAllFields, } from './validate.js';
|
|
16
|
+
export { formsMessagesEn, formChromeLabels, compileMessageTemplate, compileMessageCatalog, } from './messages.js';
|
|
16
17
|
export { createInitialData, createItemValue, parseFieldInput, parsePointer, getValueAtPointer, setValueAtPointer, appendItem, removeItemAt, } from './data.js';
|
|
17
18
|
export { FORM_FORMATS, getFormatInfo, } from './formats.js';
|
|
19
|
+
export { buildFormViewModel, } from './viewmodel.js';
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export { compileMessageTemplate, compileMessageCatalog, } from '@jarenjs/core/message';
|
|
2
|
+
/**
|
|
3
|
+
* The built-in English forms catalog. Key set = exactly the `form/*` keys
|
|
4
|
+
* `validateField` emits, plus `x-form/assert` (the rules default). The
|
|
5
|
+
* strings are byte-identical to the historical inline template literals.
|
|
6
|
+
* @type {Record<string, string | ((params: object, error?: object) => string)>}
|
|
7
|
+
*/
|
|
8
|
+
export declare const formsMessagesEn: Record<string, string | ((params: object, error?: object) => string)>;
|
|
9
|
+
/** The compiled built-in English catalog (module-level singleton). */
|
|
10
|
+
export declare const formsMessages: Readonly<Record<string, (params: object, error?: object) => string>>;
|
|
11
|
+
/**
|
|
12
|
+
* Resolve a message key through a caller catalog with built-in English
|
|
13
|
+
* fallback and render it.
|
|
14
|
+
* @param {Readonly<Record<string, (params: object, error?: object) => string>>|undefined} catalog - A compiled catalog, or undefined for English
|
|
15
|
+
* @param {string} msgid - The message key
|
|
16
|
+
* @param {object} params - The structured params
|
|
17
|
+
* @returns {string} The rendered message
|
|
18
|
+
*/
|
|
19
|
+
export declare function renderFormsMessage(catalog: Readonly<Record<string, (params: object, error?: object) => string>> | undefined, msgid: string, params: object): string;
|
|
20
|
+
/**
|
|
21
|
+
* The localized chrome strings a form renderer needs: the accessible
|
|
22
|
+
* names of the array add/remove buttons.
|
|
23
|
+
*
|
|
24
|
+
* The stylesheet that renders a form is plain JSON built once, so it
|
|
25
|
+
* cannot look anything up at render time — the host resolves these and
|
|
26
|
+
* hands them to `createFormView`. Kept next to the error catalog on
|
|
27
|
+
* purpose: one keyspace, one parity gate across every locale pack.
|
|
28
|
+
* @param {Readonly<Record<string, (params: object, error?: object) => string>>} [catalog] - A compiled catalog, or undefined for English
|
|
29
|
+
* @returns {{addItem: string, removeItem: string}}
|
|
30
|
+
* @example
|
|
31
|
+
* createFormView({ labels: formChromeLabels(catalogs[locale]) });
|
|
32
|
+
*/
|
|
33
|
+
export declare function formChromeLabels(catalog?: Readonly<Record<string, (params: object, error?: object) => string>>): {
|
|
34
|
+
addItem: string;
|
|
35
|
+
removeItem: string;
|
|
36
|
+
};
|
package/dist/types/model.d.ts
CHANGED
|
@@ -8,7 +8,11 @@ export type FormField = {
|
|
|
8
8
|
*/
|
|
9
9
|
key: string;
|
|
10
10
|
/**
|
|
11
|
-
* -
|
|
11
|
+
* - The field's message-id base: `x-msgid` annotation or the pointer (the root field's base is the empty pointer '')
|
|
12
|
+
*/
|
|
13
|
+
msgid: string;
|
|
14
|
+
/**
|
|
15
|
+
* - Human friendly label (schema title or humanized key), through the `t` hook
|
|
12
16
|
*/
|
|
13
17
|
label: string;
|
|
14
18
|
description: string | undefined;
|
|
@@ -21,7 +25,7 @@ export type FormField = {
|
|
|
21
25
|
*/
|
|
22
26
|
kind: string;
|
|
23
27
|
/**
|
|
24
|
-
* - Suggested control: 'text'|'email'|'url'|'password'|'textarea'|'number'|'checkbox'|'select'|'date'|'color'|'json'
|
|
28
|
+
* - Suggested control: 'text'|'email'|'url'|'password'|'textarea'|'number'|'checkbox'|'select'|'date'|'datetime-local'|'time'|'color'|'json'
|
|
25
29
|
*/
|
|
26
30
|
control: string;
|
|
27
31
|
/**
|
|
@@ -33,6 +37,10 @@ export type FormField = {
|
|
|
33
37
|
* - Options for a select control
|
|
34
38
|
*/
|
|
35
39
|
enumValues: Array<any> | null;
|
|
40
|
+
/**
|
|
41
|
+
* - Display labels parallel to enumValues (oneOf const/title idiom, through the `t` hook)
|
|
42
|
+
*/
|
|
43
|
+
enumLabels: Array<string> | null;
|
|
36
44
|
/**
|
|
37
45
|
* - Fixed value when the schema is a const
|
|
38
46
|
*/
|
|
@@ -60,29 +68,12 @@ export type FormField = {
|
|
|
60
68
|
*/
|
|
61
69
|
tuple: Array<FormField> | null;
|
|
62
70
|
};
|
|
63
|
-
|
|
64
|
-
* @typedef {object} FormField
|
|
65
|
-
* @property {string} pointer - JSON pointer into the DATA (e.g. '/user/name')
|
|
66
|
-
* @property {string} key - Property name (or '-' for an array item template)
|
|
67
|
-
* @property {string} label - Human friendly label (schema title or humanized key)
|
|
68
|
-
* @property {string|undefined} description
|
|
69
|
-
* @property {object} schema - The resolved subschema for this field
|
|
70
|
-
* @property {string} kind - 'string'|'number'|'integer'|'boolean'|'enum'|'const'|'object'|'array'|'unknown'
|
|
71
|
-
* @property {string} control - Suggested control: 'text'|'email'|'url'|'password'|'textarea'|'number'|'checkbox'|'select'|'date'|'color'|'json'
|
|
72
|
-
* @property {boolean} required - Whether the parent object requires this property
|
|
73
|
-
* @property {boolean} readOnly
|
|
74
|
-
* @property {Array<any>|null} enumValues - Options for a select control
|
|
75
|
-
* @property {any} constValue - Fixed value when the schema is a const
|
|
76
|
-
* @property {any} defaultValue
|
|
77
|
-
* @property {string|undefined} placeholder
|
|
78
|
-
* @property {object} constraints - minLength/maxLength/pattern/minimum/... extracted for the UI
|
|
79
|
-
* @property {object|null} rules - The raw `x-form` rules annotation, if any (see rules.js)
|
|
80
|
-
* @property {Array<FormField>|null} children - Child fields for object kinds
|
|
81
|
-
* @property {FormField|null} item - Template field for array items
|
|
82
|
-
* @property {Array<FormField>|null} tuple - Fixed prefix fields for tuple arrays
|
|
83
|
-
*/
|
|
71
|
+
export type TranslateHook = (msgid: string, fallback: string | undefined, params?: object) => string | undefined;
|
|
84
72
|
/**
|
|
85
73
|
* Convert 'firstName' / 'first_name' / 'first-name' to 'First Name'.
|
|
74
|
+
* Latin-script-oriented (word splitting on case/underscore/hyphen and
|
|
75
|
+
* ASCII capitalization); the `t` hook of buildFormModel is the override
|
|
76
|
+
* point for anything it mangles.
|
|
86
77
|
* @param {string} key
|
|
87
78
|
* @returns {string}
|
|
88
79
|
*/
|
|
@@ -104,7 +95,9 @@ export declare function resolveSchema(schema: object | boolean, rootSchema: obje
|
|
|
104
95
|
export declare function getFieldKind(schema: object | boolean): string;
|
|
105
96
|
/**
|
|
106
97
|
* Encode a property name as an RFC 6901 reference token (`~` -> `~0`,
|
|
107
|
-
* `/` -> `~1`), the write-side inverse of the shared parse.
|
|
98
|
+
* `/` -> `~1`), the write-side inverse of the shared parse. An alias of
|
|
99
|
+
* `encodeJSONPointerSegment` from `@jarenjs/json/pointer`, kept for
|
|
100
|
+
* compatibility.
|
|
108
101
|
* @param {string} key
|
|
109
102
|
* @returns {string}
|
|
110
103
|
*/
|
|
@@ -112,7 +105,16 @@ export declare function escapePointerKey(key: string): string;
|
|
|
112
105
|
/**
|
|
113
106
|
* Build the form model for a JSON schema.
|
|
114
107
|
*
|
|
108
|
+
* Static text (labels, descriptions, placeholders, enum option labels) is
|
|
109
|
+
* resolved ONCE here, at model-build time - the right place for
|
|
110
|
+
* translation. The optional `t` hook receives role-qualified message ids
|
|
111
|
+
* built from each field's base (`x-msgid` annotation or data pointer):
|
|
112
|
+
* `<base>#label`, `<base>#description`, `<base>#placeholder`,
|
|
113
|
+
* `<base>#enum/<String(value)>` - and the schema-derived fallback text.
|
|
114
|
+
*
|
|
115
115
|
* @param {object|boolean} schema - The root JSON schema
|
|
116
|
+
* @param {object} [options]
|
|
117
|
+
* @param {TranslateHook} [options.t] - Static-text translation hook, default the zero-cost identity `(id, fb) => fb`
|
|
116
118
|
* @returns {FormField} The root field descriptor (kind 'object' for object schemas)
|
|
117
119
|
* @example
|
|
118
120
|
* const model = buildFormModel({
|
|
@@ -121,5 +123,12 @@ export declare function escapePointerKey(key: string): string;
|
|
|
121
123
|
* required: ['email'],
|
|
122
124
|
* });
|
|
123
125
|
* model.children[0].control; // 'email'
|
|
126
|
+
* @example
|
|
127
|
+
* // static-text i18n
|
|
128
|
+
* const nlModel = buildFormModel(schema, {
|
|
129
|
+
* t: (msgid, fallback) => staticTextNl[msgid] ?? fallback,
|
|
130
|
+
* });
|
|
124
131
|
*/
|
|
125
|
-
export declare function buildFormModel(schema: object | boolean
|
|
132
|
+
export declare function buildFormModel(schema: object | boolean, options?: {
|
|
133
|
+
t?: TranslateHook;
|
|
134
|
+
}): FormField;
|