@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 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
@@ -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';
@@ -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.46.5",
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.46.5",
51
- "@jarenjs/formats": "^0.46.5",
52
- "@jarenjs/json": "^0.46.5",
53
- "@jarenjs/validate": "^0.46.5"
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
@@ -39,6 +39,7 @@ export {
39
39
  export {
40
40
  formsMessagesEn,
41
41
  formChromeLabels,
42
+ formatDisplayName,
42
43
  compileMessageTemplate,
43
44
  compileMessageCatalog,
44
45
  } from './messages.js';
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