@jsenv/navi 0.29.110 → 0.29.111
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/dist/jsenv_navi.js +364 -173
- package/dist/jsenv_navi.js.map +24 -20
- package/docs/AI_INSTRUCTIONS.md +16 -2
- package/docs/error_handling.md +3 -0
- package/docs/field_validation.md +193 -0
- package/docs/typography.md +14 -0
- package/package.json +2 -2
package/docs/AI_INSTRUCTIONS.md
CHANGED
|
@@ -88,6 +88,17 @@ consistency across the app, not from any single call site.
|
|
|
88
88
|
before displaying an error by hand, before writing an error boundary of your
|
|
89
89
|
own, and before concluding that a dev overlay over a page that already shows
|
|
90
90
|
its error is a crash.
|
|
91
|
+
- `docs/field_validation.md` — what a control refuses and who decides it: the
|
|
92
|
+
split between what only a browser can answer (a blocked keystroke, where the
|
|
93
|
+
callout lands, when the message appears) and « is this value acceptable »,
|
|
94
|
+
which is @jsenv/validity's and which a server asks about the same value. The
|
|
95
|
+
constraint attributes navi ships, the `charGuard`/`maxLengthGuard` props that
|
|
96
|
+
act before there is a value, how a message key is overridden, and the three
|
|
97
|
+
places an app's own rule can live — both sides via
|
|
98
|
+
`constraintFromValidityRule`, the server alone, the browser alone. Read it
|
|
99
|
+
before writing a constraint of your own: if the sentence would make sense in a
|
|
100
|
+
server's response, the rule belongs in validity and the constraint is only its
|
|
101
|
+
browser-side caller.
|
|
91
102
|
- `docs/css_architecture.md` — how Navi's CSS layering works, and the
|
|
92
103
|
supported ways to override component styles (props > CSS variables > direct
|
|
93
104
|
rule overrides, in that preference order).
|
|
@@ -203,8 +214,11 @@ consistency across the app, not from any single call site.
|
|
|
203
214
|
not it), what makes truncation actually happen in a flex row, and the two
|
|
204
215
|
opposite shapes of "icon | text | icon" — the end icon kept outside the
|
|
205
216
|
truncating text, or `attachLastChild` so it never lands alone on a line —
|
|
206
|
-
and why an emoji belongs only in free text, rendered with `emojiAsIcon
|
|
207
|
-
|
|
217
|
+
and why an emoji belongs only in free text, rendered with `emojiAsIcon` —
|
|
218
|
+
except in a control one types in, where there is no markup to wrap a glyph in
|
|
219
|
+
and the fixed line height is what keeps the rows even.
|
|
220
|
+
Read it before writing an overflowing label or a row with a trailing icon,
|
|
221
|
+
and before touching a text control's `line-height`.
|
|
208
222
|
- `docs/i18n.md` — where the texts an app displays live: `interpolateText` /
|
|
209
223
|
`<Interpolate>` for one sentence, `createI18n` for the app's registry,
|
|
210
224
|
`naviI18n` for navi's own texts. Read it before writing a user-visible
|
package/docs/error_handling.md
CHANGED
|
@@ -108,6 +108,9 @@ read (`required`, `data-single-space`, `data-displayable`…) exist because thos
|
|
|
108
108
|
constraints are global and need a per-field switch; a constraint written for
|
|
109
109
|
one call site does not.
|
|
110
110
|
|
|
111
|
+
Where such a rule belongs — a navi constraint, a @jsenv/validity rule the
|
|
112
|
+
server runs too, or both — is `docs/field_validation.md`.
|
|
113
|
+
|
|
111
114
|
## What a failing action does
|
|
112
115
|
|
|
113
116
|
It writes the error into `errorSignal`, moves to `FAILED`, and stops.
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# Field validation
|
|
2
|
+
|
|
3
|
+
What a control refuses, who decides it, and what is left for a server to say.
|
|
4
|
+
|
|
5
|
+
## The split
|
|
6
|
+
|
|
7
|
+
A control refuses a value for two very different kinds of reason, and keeping
|
|
8
|
+
them apart is the whole subject.
|
|
9
|
+
|
|
10
|
+
**What only a browser can answer.** A keystroke blocked before the value exists,
|
|
11
|
+
a callout placed next to the field, `required` on a radio group, `data-one-of`
|
|
12
|
+
reading an option list out of the document, the moment a message appears
|
|
13
|
+
(typing? blur? submit?).
|
|
14
|
+
This is navi's, and it stays navi's.
|
|
15
|
+
|
|
16
|
+
**« Is this value acceptable ».** A length, a set of allowed characters, a
|
|
17
|
+
value that renders nothing, a business rule. This is not a DOM question — a
|
|
18
|
+
server asks the same one about the same value — so it lives in
|
|
19
|
+
[@jsenv/validity](../../../tooling/validity/README.md) and navi consumes it.
|
|
20
|
+
|
|
21
|
+
The consequence that matters when writing an app: **do not write a constraint
|
|
22
|
+
for something a rule already answers**. If the sentence you are about to write
|
|
23
|
+
in a `check()` would make sense in a server's response, the knowledge belongs in
|
|
24
|
+
a validity rule, and the constraint is only its browser-side caller.
|
|
25
|
+
|
|
26
|
+
## Constraints
|
|
27
|
+
|
|
28
|
+
A constraint is `{ name, check(field) }`. `check` returns `null` when the value
|
|
29
|
+
passes, or the message to show — a string, or `{ message, target }` when the
|
|
30
|
+
callout belongs on another element than the control itself.
|
|
31
|
+
|
|
32
|
+
Navi's own constraints are switched on by an attribute on the control —
|
|
33
|
+
standard when the platform has one, `data-*` when it does not.
|
|
34
|
+
|
|
35
|
+
**They are written as props, in camelCase.** A constraint declares the attribute
|
|
36
|
+
it reads (`data-no-emoji`) and a control accepts the prop that stands for it
|
|
37
|
+
(`noEmoji`), putting it on the control host under the attribute name — the same
|
|
38
|
+
conversion `element.dataset` does. So `singleSpace` is what you write and
|
|
39
|
+
`data-single-space` is what ends up in the DOM (and in the devtools, and in a
|
|
40
|
+
test selector). The attribute form is accepted too, for a control written in
|
|
41
|
+
plain HTML.
|
|
42
|
+
|
|
43
|
+
| prop | attribute | refuses |
|
|
44
|
+
| ------------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------- |
|
|
45
|
+
| `required`, `pattern` | same | what the platform's attributes mean |
|
|
46
|
+
| `minLength`, `maxLength` | same | a string too short or too long |
|
|
47
|
+
| `min`, `max`, `step` | same | a number, date, time or duration out of range |
|
|
48
|
+
| `singleSpace` | `data-single-space` | a leading or trailing space, two in a row |
|
|
49
|
+
| `displayable` | `data-displayable` | zalgo, a value showing nothing, blank lines in series, a joiner joining nothing |
|
|
50
|
+
| `maxStackedMarks` | `data-max-stacked-marks` | (parameter of `displayable`) |
|
|
51
|
+
| `noEmoji` | `data-no-emoji` | an emoji, where a name, an identifier or a title does not want one |
|
|
52
|
+
| `maxLineBreaks` | `data-max-line-breaks` | a value holding more line breaks than that |
|
|
53
|
+
| `oneOf` | `data-one-of` | a value outside the option list its CSS selector points at |
|
|
54
|
+
| `sameAs` | `data-same-as` | a value differing from the field it names |
|
|
55
|
+
| `minDigit`, `minUpperLetter`, … | `data-min-digit`, … | a password missing a kind of character |
|
|
56
|
+
| `timeAfter`, `timeMinDuration` | `data-time-after`, `data-time-min-duration` | a time span that ends before it starts, or is too short |
|
|
57
|
+
|
|
58
|
+
```jsx
|
|
59
|
+
<Input required singleSpace noEmoji maxLength={80} />
|
|
60
|
+
<Textarea displayable singleSpace maxLineBreaks={4} />
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
A boolean switch (`displayable`, `singleSpace`, `noEmoji`) passed as `false` is
|
|
64
|
+
off, so `noEmoji={settings.strictNames}` says what it looks like it says. The
|
|
65
|
+
others carry a value — a count, a selector — and are off by being absent.
|
|
66
|
+
|
|
67
|
+
Each constraint has a `<name>Message` prop (and a `data-<name>-message`
|
|
68
|
+
attribute) to replace its sentence for one field. To change it everywhere,
|
|
69
|
+
override the i18n key instead — see below.
|
|
70
|
+
|
|
71
|
+
Two props sit beside them and belong to the browser alone, because they act
|
|
72
|
+
before there is a value to validate: `charGuard` blocks a keystroke that is not
|
|
73
|
+
in its character class, `maxLengthGuard` blocks the one that would overflow (and
|
|
74
|
+
truncates a paste). Both show what they refused in a callout rather than
|
|
75
|
+
silently swallowing it.
|
|
76
|
+
|
|
77
|
+
`charGuard` takes a character class or one of validity's preset names, so a
|
|
78
|
+
field can refuse the keystroke with the same knowledge the value is checked
|
|
79
|
+
against: `charGuard="tel"`, `charGuard="slug"`, `charGuard="noEmoji"`. Refusing
|
|
80
|
+
the keystroke and refusing the value are different jobs — the pair
|
|
81
|
+
`maxLengthGuard`/`maxLength` is the same split — and a field usually wants both.
|
|
82
|
+
|
|
83
|
+
**A guard answers for the gesture, never for what the field already holds.** A
|
|
84
|
+
value can arrive already outside the class or already too long — a
|
|
85
|
+
`defaultValue`, a signal, a value written from elsewhere — and a guard that
|
|
86
|
+
re-judged the whole value would refuse every keystroke over it, blaming the
|
|
87
|
+
person for a character they did not type, and refuse the deletion that would
|
|
88
|
+
have fixed it. So a change is refused only when it makes the value worse. What
|
|
89
|
+
is already there is the constraint's business, and it says so at submit.
|
|
90
|
+
|
|
91
|
+
## Reading the validity without submitting
|
|
92
|
+
|
|
93
|
+
`useConstraintValidityState(ref)` gives the control's validity as it stands,
|
|
94
|
+
re-read whenever it changes:
|
|
95
|
+
|
|
96
|
+
```js
|
|
97
|
+
const state = useConstraintValidityState(inputRef);
|
|
98
|
+
state.valid; // false
|
|
99
|
+
state.single_space.messageString; // the sentence
|
|
100
|
+
state.reported; // "max_length" — the one the callout says
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Several constraints fail at once and only one sentence is shown: the one with
|
|
104
|
+
the highest priority — an `error` status first, then `required`, then the
|
|
105
|
+
platform's own constraints, then navi's and the app's, ties going to the first
|
|
106
|
+
registered. `reported` names it, so a summary drawn beside the field says the
|
|
107
|
+
same thing as the callout rather than picking a second one.
|
|
108
|
+
|
|
109
|
+
`src/control/demos/validation/text_rules_demo.html` is that, one rule per row: a
|
|
110
|
+
value that breaks it, its message read live, and a submit to see the callout.
|
|
111
|
+
|
|
112
|
+
## Messages
|
|
113
|
+
|
|
114
|
+
Every sentence navi says is a key in `naviI18n`, and the validation ones are
|
|
115
|
+
`constraint.*`. An app changes one for its whole app by registering over it:
|
|
116
|
+
|
|
117
|
+
```js
|
|
118
|
+
import { naviI18n } from "@jsenv/navi";
|
|
119
|
+
|
|
120
|
+
naviI18n.add("constraint.single_space.consecutive", {
|
|
121
|
+
fr: "Pas deux espaces d'affilée.",
|
|
122
|
+
});
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The keys of everything validity owns are validity's own key prefixed with
|
|
126
|
+
`constraint.` — `single_space.start` is `constraint.single_space.start`,
|
|
127
|
+
`char_class.slug` is `constraint.char_class.slug`. That is deliberate: the
|
|
128
|
+
sentence a server returns and the sentence the field shows are looked up under
|
|
129
|
+
one name, so making them agree is registering one key, not maintaining a
|
|
130
|
+
translation table. See `src/control/rules/validity_bridge.js`, and `i18n.md` for
|
|
131
|
+
how the registry itself works.
|
|
132
|
+
|
|
133
|
+
## An app's own rules
|
|
134
|
+
|
|
135
|
+
An app puts its rules in the package its server reads too, and then decides, per
|
|
136
|
+
rule, who runs it.
|
|
137
|
+
|
|
138
|
+
**Both sides.** The rule is a validity rule; navi wears it as a constraint.
|
|
139
|
+
|
|
140
|
+
```js
|
|
141
|
+
// shared/src/text_rules.js — read by the server and by the front
|
|
142
|
+
export const MAX_WORDS_RULE = {
|
|
143
|
+
name: "maxWords",
|
|
144
|
+
applyOn: (maxWords, value) => {
|
|
145
|
+
if (maxWords === undefined || typeof value !== "string") {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
const count = value.trim().split(/\s+/).length;
|
|
149
|
+
if (count <= maxWords) {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
return { key: "max_words", params: { max: maxWords, count } };
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
```js
|
|
158
|
+
// front — once, at module level: a constraint rebuilt on every render is a new
|
|
159
|
+
// object on every check
|
|
160
|
+
import { constraintFromValidityRule } from "@jsenv/navi";
|
|
161
|
+
|
|
162
|
+
const MAX_WORDS_CONSTRAINT = constraintFromValidityRule(MAX_WORDS_RULE, {
|
|
163
|
+
maxWords: 40,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
<Textarea constraints={[MAX_WORDS_CONSTRAINT]} />;
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
The rule's key is looked up in `naviI18n` under `constraint.max_words`, so
|
|
170
|
+
register it there — or pass `formatMessage` beside the parameters to say it
|
|
171
|
+
through the app's own i18n instead.
|
|
172
|
+
|
|
173
|
+
**The server alone.** A rule needing the database, another user's data or a
|
|
174
|
+
secret stays in `createValidity({ rules })` and never reaches the front; its
|
|
175
|
+
refusal arrives with the response like any other error.
|
|
176
|
+
|
|
177
|
+
**The browser alone.** A rule about the gesture rather than the value — what a
|
|
178
|
+
keystroke may insert, a warning shown while typing — is a plain navi constraint,
|
|
179
|
+
written inline and never sent anywhere:
|
|
180
|
+
|
|
181
|
+
```js
|
|
182
|
+
<Input
|
|
183
|
+
constraints={[(field) => (field.uiState === "admin" ? "Reserved" : null)]}
|
|
184
|
+
/>
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
`registerGlobalConstraint(constraint)` is the same thing for every control at
|
|
188
|
+
once, for a rule that genuinely holds everywhere in the app.
|
|
189
|
+
|
|
190
|
+
The line to hold across all three: **the front may be laxer than the back, never
|
|
191
|
+
stricter**. A value the field accepted and the server refuses is a promise
|
|
192
|
+
broken after the fact — which is exactly what happens when the same rule is
|
|
193
|
+
written twice and the copies drift.
|
package/docs/typography.md
CHANGED
|
@@ -215,6 +215,20 @@ an emoji (see the dense cases in `text_emoji_demo.html`). A `lineHeight` is a
|
|
|
215
215
|
typographic choice for the text itself — a paragraph that wants air — never a
|
|
216
216
|
workaround for a glyph that grew too tall; that glyph gets `emojiAsIcon`.
|
|
217
217
|
|
|
218
|
+
**A control one types in is the exception, and there the line height is the
|
|
219
|
+
whole answer.** `emojiAsIcon` rewrites strings into markup, and there is no
|
|
220
|
+
markup inside a `<textarea>` or an `<input>`: the value is raw text the browser
|
|
221
|
+
draws itself, so no glyph in it can be wrapped in anything. Under
|
|
222
|
+
`line-height: normal` a line box takes the height of the tallest font it holds,
|
|
223
|
+
so the one line carrying an emoji stands taller than the ones around it — rows
|
|
224
|
+
of uneven height, and a box sized in `lh` (`minRows`/`maxRows`) that jumps as
|
|
225
|
+
soon as one is typed. `Textarea` therefore fixes its line height, and picks a
|
|
226
|
+
value tall enough to contain an emoji's own box so the glyph is not clipped
|
|
227
|
+
either: a tighter one would keep the rows even and cut the top off the emoji.
|
|
228
|
+
Do not remove it, and do not tighten it — `34_textarea_demo.html` shows the two
|
|
229
|
+
side by side. The rule above still holds everywhere the text is rendered rather
|
|
230
|
+
than typed.
|
|
231
|
+
|
|
218
232
|
## Text that must not move when its style changes
|
|
219
233
|
|
|
220
234
|
A label that becomes bold when its row is selected reflows everything around it.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsenv/navi",
|
|
3
|
-
"version": "0.29.
|
|
3
|
+
"version": "0.29.111",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Library of components including navigation to create frontend applications",
|
|
6
6
|
"repository": {
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@jsenv/dom": "0.17.30",
|
|
33
33
|
"@jsenv/humanize": "1.7.8",
|
|
34
|
-
"@jsenv/validity": "0.4.
|
|
34
|
+
"@jsenv/validity": "0.4.3"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@jsenv/assert": "4.5.7",
|