@jarenjs/forms 0.46.5 → 0.56.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 +55 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/messages.d.ts +28 -0
- package/package.json +5 -5
- package/src/index.js +1 -0
- package/src/messages.js +42 -0
package/README.md
CHANGED
|
@@ -21,13 +21,13 @@ import {
|
|
|
21
21
|
|
|
22
22
|
const schema = {
|
|
23
23
|
type: 'object',
|
|
24
|
-
title: 'Sign up',
|
|
25
24
|
properties: {
|
|
26
25
|
username: { type: 'string', minLength: 3, pattern: '^[a-z0-9_]+$' },
|
|
27
26
|
email: { type: 'string', format: 'email' },
|
|
28
27
|
age: { type: 'integer', minimum: 13 },
|
|
29
28
|
},
|
|
30
29
|
required: ['username', 'email'],
|
|
30
|
+
title: 'Sign up',
|
|
31
31
|
};
|
|
32
32
|
|
|
33
33
|
// 1. Build the field tree once
|
|
@@ -90,6 +90,27 @@ stylesheet is the second kind of host, and the website's data studio the first.
|
|
|
90
90
|
|
|
91
91
|
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.
|
|
92
92
|
|
|
93
|
+
### The same model, by code
|
|
94
|
+
|
|
95
|
+
The schema above is what `@jarenjs/linq/forms` emits, byte for byte:
|
|
96
|
+
|
|
97
|
+
```javascript
|
|
98
|
+
import * as f from '@jarenjs/linq/forms';
|
|
99
|
+
|
|
100
|
+
export const signup = f.object({
|
|
101
|
+
username: f.string().min(3).pattern('^[a-z0-9_]+$'),
|
|
102
|
+
email: f.string().format('email'),
|
|
103
|
+
age: f.integer().min(13).optional(),
|
|
104
|
+
}).open().title('Sign up');
|
|
105
|
+
|
|
106
|
+
const model = buildFormModel(signup.schema);
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Every rule of the next section is a `.form({ … })` on the member it
|
|
110
|
+
belongs to; the pen's document is
|
|
111
|
+
[FORMS-PEN.md](../linq/docs/FORMS-PEN.md), and nothing in this package
|
|
112
|
+
depends on it — the schema is the contract.
|
|
113
|
+
|
|
93
114
|
## Layer 1 — preemptive per-field validation
|
|
94
115
|
|
|
95
116
|
`validateField(field, value)` returns `[{ keyword, message }]` using `@jarenjs/core` directly:
|
|
@@ -119,6 +140,16 @@ One namespaced annotation keyword — safe under every metaschema, invisible to
|
|
|
119
140
|
} }
|
|
120
141
|
```
|
|
121
142
|
|
|
143
|
+
These annotations can be **written by code**: `@jarenjs/linq/forms` is
|
|
144
|
+
the schema pen plus `form({ visible, enabled, assert, computed,
|
|
145
|
+
message })` on every builder, with the rules captured as callbacks over
|
|
146
|
+
the same context (`c.root`, `c.value`, `c.pointer`) rather than typed
|
|
147
|
+
as path strings, and `assertOnSubmit()` answering the layer-3 twin
|
|
148
|
+
below in one call. The document above is what it emits, byte for byte
|
|
149
|
+
— see [the by-code twin](#the-same-model-by-code) and
|
|
150
|
+
[FORMS-PEN.md](../linq/docs/FORMS-PEN.md). This package depends on
|
|
151
|
+
none of it; the annotation is the contract.
|
|
152
|
+
|
|
122
153
|
Recognized members — unknown members are ignored for forward compatibility:
|
|
123
154
|
|
|
124
155
|
| Member | Kind | Meaning |
|
|
@@ -259,6 +290,29 @@ const errors = validateAllFields(model, data, catalog);
|
|
|
259
290
|
// errors['/name'][0].message === 'Dit veld is verplicht'
|
|
260
291
|
```
|
|
261
292
|
|
|
293
|
+
### A format's name, in the reader's language
|
|
294
|
+
|
|
295
|
+
A format's name is its wire name — `date-time`, `iso-time` — which is
|
|
296
|
+
English by construction and reads as gibberish inside a translated
|
|
297
|
+
sentence (*"Moet een geldige date-time zijn"*). A catalog that carries
|
|
298
|
+
`format/name/<format>` says how that format is called in its own
|
|
299
|
+
language, and `renderFormsMessage` swaps it in before `form/format`
|
|
300
|
+
renders. The error's own `params.format` stays the raw wire name, so the
|
|
301
|
+
same error re-rendered through a second catalog answers in *that*
|
|
302
|
+
language rather than repeating the first one's noun:
|
|
303
|
+
|
|
304
|
+
```javascript
|
|
305
|
+
const error = validateField(whenField, 'nope', catalog)[0];
|
|
306
|
+
error.message; // 'Moet een geldige datum en tijd zijn'
|
|
307
|
+
error.params.format; // 'date-time'
|
|
308
|
+
formatDisplayName(catalog, 'email'); // 'email' — already a word
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
`@jarenjs/locales` carries the six date and time names in every pack; a
|
|
312
|
+
format the catalog cannot name keeps its own, which is the readable
|
|
313
|
+
answer for the names that are already words and the only possible one for
|
|
314
|
+
a format nobody has heard of.
|
|
315
|
+
|
|
262
316
|
### MessageSpec in `x-form.message`
|
|
263
317
|
|
|
264
318
|
A rule's `message` may be a plain string (backward compatible — an inline
|
package/dist/types/index.d.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
export { buildFormModel, resolveSchema, getFieldKind, humanizeKey, escapePointerKey, } from './model.js';
|
|
16
16
|
export { compileFormRules, evaluateFormRules, formRulesToQueryAssertions, pruneHiddenValues, createRuleMemo, } from './rules.js';
|
|
17
17
|
export { validateField, validateAllFields, } from './validate.js';
|
|
18
|
-
export { formsMessagesEn, formChromeLabels, compileMessageTemplate, compileMessageCatalog, } from './messages.js';
|
|
18
|
+
export { formsMessagesEn, formChromeLabels, formatDisplayName, compileMessageTemplate, compileMessageCatalog, } from './messages.js';
|
|
19
19
|
export { createInitialData, createItemValue, parseFieldInput, parsePointer, getValueAtPointer, setValueAtPointer, appendItem, removeItemAt, } from './data.js';
|
|
20
20
|
export { FORM_FORMATS, getFormatInfo, } from './formats.js';
|
|
21
21
|
export { buildFormViewModel, } from './viewmodel.js';
|
package/dist/types/messages.d.ts
CHANGED
|
@@ -8,9 +8,37 @@ export { compileMessageTemplate, compileMessageCatalog, } from '@jarenjs/core/me
|
|
|
8
8
|
export declare const formsMessagesEn: Record<string, string | ((params: object, error?: object) => string)>;
|
|
9
9
|
/** The compiled built-in English catalog (module-level singleton). */
|
|
10
10
|
export declare const formsMessages: Readonly<Record<string, (params: object, error?: object) => string>>;
|
|
11
|
+
/**
|
|
12
|
+
* The display name of a format, as the catalog in hand spells it.
|
|
13
|
+
*
|
|
14
|
+
* A format's name is its wire name - `date-time`, `iso-time` - which is
|
|
15
|
+
* English by construction and reads as gibberish inside a translated
|
|
16
|
+
* sentence ("Moet een geldige date-time zijn"). A catalog that carries
|
|
17
|
+
* `format/name/<format>` says how that format is called in its own
|
|
18
|
+
* language; the date catalog of `@jarenjs/locales` carries the six date
|
|
19
|
+
* and time ones. A format the catalog has no name for keeps its own,
|
|
20
|
+
* which is the readable answer for the format names that are already
|
|
21
|
+
* words ("email", "hostname") and the only possible one for a format
|
|
22
|
+
* this repository has never heard of.
|
|
23
|
+
*
|
|
24
|
+
* @param {Readonly<Record<string, (params: object, error?: object) => string>>|undefined} catalog - A compiled catalog, or undefined for English
|
|
25
|
+
* @param {string} format - The format name
|
|
26
|
+
* @returns {string} The display name, or the format name itself
|
|
27
|
+
* @example
|
|
28
|
+
* formatDisplayName(dutch, 'date-time'); // 'datum en tijd'
|
|
29
|
+
* formatDisplayName(dutch, 'email'); // 'email'
|
|
30
|
+
*/
|
|
31
|
+
export declare function formatDisplayName(catalog: Readonly<Record<string, (params: object, error?: object) => string>> | undefined, format: string): string;
|
|
11
32
|
/**
|
|
12
33
|
* Resolve a message key through a caller catalog with built-in English
|
|
13
34
|
* fallback and render it.
|
|
35
|
+
*
|
|
36
|
+
* A format failure is the one message whose params are prepared here
|
|
37
|
+
* rather than at the call site: the error's own `params.format` stays
|
|
38
|
+
* the raw wire name, so re-rendering the same error through a second
|
|
39
|
+
* catalog answers in that catalog's language instead of repeating the
|
|
40
|
+
* first one's noun.
|
|
41
|
+
*
|
|
14
42
|
* @param {Readonly<Record<string, (params: object, error?: object) => string>>|undefined} catalog - A compiled catalog, or undefined for English
|
|
15
43
|
* @param {string} msgid - The message key
|
|
16
44
|
* @param {object} params - The structured params
|
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.56.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.56.0",
|
|
51
|
+
"@jarenjs/formats": "^0.56.0",
|
|
52
|
+
"@jarenjs/json": "^0.56.0",
|
|
53
|
+
"@jarenjs/validate": "^0.56.0"
|
|
54
54
|
}
|
|
55
55
|
}
|
package/src/index.js
CHANGED
package/src/messages.js
CHANGED
|
@@ -68,9 +68,47 @@ export const formsMessagesEn = {
|
|
|
68
68
|
/** The compiled built-in English catalog (module-level singleton). */
|
|
69
69
|
export const formsMessages = compileMessageCatalog(formsMessagesEn);
|
|
70
70
|
|
|
71
|
+
/** Shared empty params for the entries that interpolate nothing. */
|
|
72
|
+
const NO_PARAMS = Object.freeze({});
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The display name of a format, as the catalog in hand spells it.
|
|
76
|
+
*
|
|
77
|
+
* A format's name is its wire name - `date-time`, `iso-time` - which is
|
|
78
|
+
* English by construction and reads as gibberish inside a translated
|
|
79
|
+
* sentence ("Moet een geldige date-time zijn"). A catalog that carries
|
|
80
|
+
* `format/name/<format>` says how that format is called in its own
|
|
81
|
+
* language; the date catalog of `@jarenjs/locales` carries the six date
|
|
82
|
+
* and time ones. A format the catalog has no name for keeps its own,
|
|
83
|
+
* which is the readable answer for the format names that are already
|
|
84
|
+
* words ("email", "hostname") and the only possible one for a format
|
|
85
|
+
* this repository has never heard of.
|
|
86
|
+
*
|
|
87
|
+
* @param {Readonly<Record<string, (params: object, error?: object) => string>>|undefined} catalog - A compiled catalog, or undefined for English
|
|
88
|
+
* @param {string} format - The format name
|
|
89
|
+
* @returns {string} The display name, or the format name itself
|
|
90
|
+
* @example
|
|
91
|
+
* formatDisplayName(dutch, 'date-time'); // 'datum en tijd'
|
|
92
|
+
* formatDisplayName(dutch, 'email'); // 'email'
|
|
93
|
+
*/
|
|
94
|
+
export function formatDisplayName(catalog, format) {
|
|
95
|
+
if (typeof format !== 'string' || catalog === undefined) return format;
|
|
96
|
+
const render = catalog[`format/name/${format}`];
|
|
97
|
+
if (render === undefined) return format;
|
|
98
|
+
const name = render(NO_PARAMS);
|
|
99
|
+
return typeof name === 'string' && name !== '' ? name : format;
|
|
100
|
+
}
|
|
101
|
+
|
|
71
102
|
/**
|
|
72
103
|
* Resolve a message key through a caller catalog with built-in English
|
|
73
104
|
* fallback and render it.
|
|
105
|
+
*
|
|
106
|
+
* A format failure is the one message whose params are prepared here
|
|
107
|
+
* rather than at the call site: the error's own `params.format` stays
|
|
108
|
+
* the raw wire name, so re-rendering the same error through a second
|
|
109
|
+
* catalog answers in that catalog's language instead of repeating the
|
|
110
|
+
* first one's noun.
|
|
111
|
+
*
|
|
74
112
|
* @param {Readonly<Record<string, (params: object, error?: object) => string>>|undefined} catalog - A compiled catalog, or undefined for English
|
|
75
113
|
* @param {string} msgid - The message key
|
|
76
114
|
* @param {object} params - The structured params
|
|
@@ -80,6 +118,10 @@ export function renderFormsMessage(catalog, msgid, params) {
|
|
|
80
118
|
let render = catalog !== undefined ? catalog[msgid] : undefined;
|
|
81
119
|
if (render === undefined) render = formsMessages[msgid];
|
|
82
120
|
if (render === undefined) return msgid;
|
|
121
|
+
if (msgid === 'form/format') {
|
|
122
|
+
const format = formatDisplayName(catalog, /** @type {any} */ (params).format);
|
|
123
|
+
return render({ ...params, format });
|
|
124
|
+
}
|
|
83
125
|
return render(params);
|
|
84
126
|
}
|
|
85
127
|
|