@jsenv/navi 0.29.418 → 0.29.419
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 +1 -1
- package/dist/dev/jsenv_navi.js +192 -43
- package/dist/dev/jsenv_navi.js.map +10 -10
- package/dist/jsenv_navi.js +190 -41
- package/dist/jsenv_navi.js.map +10 -10
- package/docs/AI_INSTRUCTIONS.md +177 -261
- package/docs/actions.md +164 -251
- package/docs/autofocus.md +66 -83
- package/docs/control_object.md +117 -214
- package/docs/control_value.md +121 -185
- package/docs/create_and_edit.md +159 -254
- package/docs/css_architecture.md +196 -260
- package/docs/data_states.md +41 -44
- package/docs/dialog_shape.md +91 -134
- package/docs/drag_interactions.md +115 -174
- package/docs/drag_to_travel.md +10 -9
- package/docs/dynamic_import.md +119 -136
- package/docs/error_handling.md +75 -127
- package/docs/field_validation.md +49 -89
- package/docs/form_changed.md +61 -78
- package/docs/group.md +141 -0
- package/docs/i18n.md +31 -76
- package/docs/interactions.md +6 -4
- package/docs/list_action.md +36 -51
- package/docs/list_refresh.md +73 -115
- package/docs/mobile_touch.md +56 -62
- package/docs/navigation.md +377 -723
- package/docs/network_policy.md +61 -75
- package/docs/pan_zoom.md +62 -103
- package/docs/popup_backdrop.md +128 -168
- package/docs/popup_lift.md +179 -268
- package/docs/popup_open.md +380 -651
- package/docs/resource.md +228 -408
- package/docs/route_transitions.md +333 -482
- package/docs/safe_area.md +102 -89
- package/docs/scroll.md +175 -335
- package/docs/state_binding.md +2 -1
- package/docs/testid.md +18 -23
- package/docs/typography.md +111 -141
- package/docs/view_transitions.md +106 -104
- package/docs/z_index.md +61 -109
- package/package.json +2 -2
- package/docs/badge_list.md +0 -83
- package/docs/control_group.md +0 -168
- package/docs/mobile_layout_pitfalls.md +0 -145
package/docs/error_handling.md
CHANGED
|
@@ -42,24 +42,20 @@ validation message on the **requester** (the submit button, not the form):
|
|
|
42
42
|
- The message is a constraint on the control (`navi_action_error`) rather than a
|
|
43
43
|
render of its own, and it **auto-resets on the next action**: hitting submit
|
|
44
44
|
again clears it and re-submits, instead of the form being stuck invalid.
|
|
45
|
-
- `actionErrorEffect
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
does. `<Details>` uses that: its `<ActionRenderer>` shows the error inside the
|
|
51
|
-
open panel.
|
|
45
|
+
- `actionErrorEffect="throw"` re-throws the error from a layout effect so the
|
|
46
|
+
nearest `<ErrorBoundary>` takes it — an error thrown from an event handler
|
|
47
|
+
reaches no boundary. `"none"` means the control displays nothing because
|
|
48
|
+
something else does: `<Details>` uses it, its `<ActionRenderer>` showing the
|
|
49
|
+
error inside the open panel.
|
|
52
50
|
- `onActionError(error, event)` fires alongside whichever display was chosen —
|
|
53
51
|
it is for reacting to the failure, not for showing it (showing stays claimed
|
|
54
52
|
by the message or the boundary). Its siblings on every control with an
|
|
55
|
-
`action`
|
|
56
|
-
[actions.md](./actions.md#the-instance-a-control-runs).
|
|
53
|
+
`action` are in [actions.md](./actions.md#the-instance-a-control-runs).
|
|
57
54
|
|
|
58
55
|
#### A refusal that carries a link
|
|
59
56
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
the callout holds it, link included:
|
|
57
|
+
When the refusal has somewhere to go — "this slot is already taken by that game"
|
|
58
|
+
— the mapping returns an element and the callout holds it, link included:
|
|
63
59
|
|
|
64
60
|
```jsx
|
|
65
61
|
<Form
|
|
@@ -79,55 +75,39 @@ the callout holds it, link included:
|
|
|
79
75
|
>
|
|
80
76
|
```
|
|
81
77
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
78
|
+
The message arrives on the button that was pressed, survives whatever ran before
|
|
79
|
+
it (a `command` waits for its own action), and clears itself on the next
|
|
80
|
+
submit: a refusal with a destination does **not** need a render of its own in
|
|
81
|
+
the page. Being an element, the message:
|
|
86
82
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
global signals — a navi `<Link route>`, a store — works; anything expecting an
|
|
92
|
-
app `Context.Provider` above it does not, and has to be given what it needs
|
|
83
|
+
- is rendered as a **preact tree of its own** into the callout, not as part of
|
|
84
|
+
the app tree, so it receives no context from the app. Anything reading global
|
|
85
|
+
signals — a navi `<Link route>`, a store — works; anything expecting an app
|
|
86
|
+
`Context.Provider` above it does not, and has to be given what it needs
|
|
93
87
|
through props;
|
|
94
|
-
-
|
|
88
|
+
- has no one-line form, so nothing derives a `title` from it — the callout is
|
|
95
89
|
the only place it is shown. A string or an `Error` message still writes the
|
|
96
90
|
control's `title` when the control has none of its own.
|
|
97
91
|
|
|
98
|
-
|
|
99
|
-
|
|
92
|
+
Reference: `src/control/demos/form_error_mapping_jsx/demo.html`, a mapping
|
|
93
|
+
answering with a different component per error kind.
|
|
100
94
|
|
|
101
95
|
### A screen's data failed — a route action
|
|
102
96
|
|
|
103
97
|
Nobody clicked; the page simply cannot be drawn. It is the page, or a piece of
|
|
104
98
|
it, that is replaced — by an ancestor, never by a prop on the page. That is what
|
|
105
|
-
`useAsyncData`'s default means: **delegate
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
between a container `<Route>` and its branches — the boundary OUTSIDE the
|
|
109
|
-
`<Loading>`, since a page suspends first and fails second. The shape of that
|
|
110
|
-
tree, and why a wrapper brackets a subset of the branches, is in
|
|
99
|
+
`useAsyncData`'s default means: **delegate**, the wait to `<Loading>` and the
|
|
100
|
+
failure to `<ErrorBoundary>`, the boundary OUTSIDE the `<Loading>` since a page
|
|
101
|
+
suspends first and fails second. The shape of that tree is in
|
|
111
102
|
[navigation.md](./navigation.md#loading-data).
|
|
112
103
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
});
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
`game` and `error` arrive **together**: a refresh that failed does not unmake
|
|
124
|
-
the answer the action already had, so the screen keeps what it has and says the
|
|
125
|
-
failure on it, and `dismissError()` takes that message away without asking
|
|
126
|
-
anything again. That is the whole difference between the two shapes — a boundary
|
|
127
|
-
replaces the subtree by definition, this one writes on it — so reach for the
|
|
128
|
-
boundary when the failure means the screen cannot be drawn at all, and for this
|
|
129
|
-
when it means one thing about it is out of date. How the component draws the
|
|
130
|
-
states it is handed is [data_states.md](./data_states.md).
|
|
104
|
+
When the error is part of what the screen draws rather than a screen of its own,
|
|
105
|
+
`useAsyncData(action, { loading: true, error: true })` hands it over **beside**
|
|
106
|
+
the answer the action already had: the screen keeps what it has, says the
|
|
107
|
+
failure on it, and `dismissError()` takes the message away without asking
|
|
108
|
+
anything again. Reach for the boundary when the failure means the screen cannot
|
|
109
|
+
be drawn at all, for this when it means one thing about it is out of date (see
|
|
110
|
+
[data_states.md](./data_states.md#an-error-is-a-message-on-the-screen-not-the-screen)).
|
|
131
111
|
|
|
132
112
|
And one row rather than the whole list: `<List.Item error>` replaces the row's
|
|
133
113
|
content with the message — what the row stood for did not happen, so drawing it
|
|
@@ -137,24 +117,10 @@ as if it had would be a lie.
|
|
|
137
117
|
|
|
138
118
|
Not an error at all, and not to be modelled as one: a value the app or the
|
|
139
119
|
browser refuses is a **status on the control**, carried by navi's constraint
|
|
140
|
-
validation. Never throw to reject a value
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
```jsx
|
|
146
|
-
<Input constraints={[TEXT_SHAPE_CONSTRAINT]} />; // this control
|
|
147
|
-
registerGlobalConstraint(TEXT_SHAPE_CONSTRAINT); // every control
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
A constraint is an object, so it carries whatever it needs to decide — there is
|
|
151
|
-
nothing to pass through an attribute. The attributes the shipped constraints
|
|
152
|
-
read (`required`, `data-single-space`, `data-displayable`…) exist because those
|
|
153
|
-
constraints are global and need a per-field switch; a constraint written for
|
|
154
|
-
one call site does not.
|
|
155
|
-
|
|
156
|
-
Where such a rule belongs — a navi constraint, a @jsenv/validity rule the
|
|
157
|
-
server runs too, or both — is `docs/field_validation.md`.
|
|
120
|
+
validation. Never throw to reject a value. An app's own rule joins that set as a
|
|
121
|
+
constraint — `constraints` on one control, `registerGlobalConstraint` for every
|
|
122
|
+
control — and where such a rule belongs, a navi constraint, a @jsenv/validity
|
|
123
|
+
rule the server runs too, or both, is [field_validation.md](./field_validation.md).
|
|
158
124
|
|
|
159
125
|
## What a failing action does
|
|
160
126
|
|
|
@@ -176,24 +142,21 @@ awaiting another one fails with it:
|
|
|
176
142
|
```
|
|
177
143
|
|
|
178
144
|
The error is in `errorSignal` all the same: a screen reads it there whether or
|
|
179
|
-
not anything was waiting on the promise
|
|
180
|
-
`onError` passed to the run are
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
([dynamic_import.md](./dynamic_import.md#ahead-of-the-render-intent)). Taking
|
|
195
|
-
it is what obliges that place to ask the next question: did anything display
|
|
196
|
-
this error? (below).
|
|
145
|
+
not anything was waiting on the promise — `useAsyncData`, `errorSignal` and an
|
|
146
|
+
`onError` passed to the run are how a render asks for it. Code that lets such a
|
|
147
|
+
rejection float gets the runtime's own report — an `unhandledrejection`, the
|
|
148
|
+
jsenv overlay in dev — and that is the right answer for it: nothing had to guess
|
|
149
|
+
whether someone was waiting.
|
|
150
|
+
|
|
151
|
+
**Some runs have nobody to reject at**: one started from a signal effect because
|
|
152
|
+
its params changed, one started by the render that reads it (`{ run: true }`),
|
|
153
|
+
one whose failure the control that started it already draws, a routing whose
|
|
154
|
+
result every caller drops. navi takes their rejection itself without hiding the
|
|
155
|
+
error — it stays in `errorSignal` — and taking it is what obliges navi to ask
|
|
156
|
+
the next question: did anything display this error? (below). The prefetch on
|
|
157
|
+
intent is dropped without a word, since nothing on screen asked for it and the
|
|
158
|
+
arrival runs the action again
|
|
159
|
+
([dynamic_import.md](./dynamic_import.md#ahead-of-the-render-intent)).
|
|
197
160
|
|
|
198
161
|
Two things must never become a rejection, and do not. **A navigation**: the
|
|
199
162
|
routing's promise is what the browser calls the navigation itself, and rejecting
|
|
@@ -202,18 +165,15 @@ of an update**: several actions run for one update, so their outcomes are
|
|
|
202
165
|
gathered with `allSettled` — one failure is an outcome to record, not something
|
|
203
166
|
that takes the other five down.
|
|
204
167
|
|
|
205
|
-
|
|
206
|
-
know whether a screen will display it
|
|
207
|
-
renders
|
|
208
|
-
someone show this?" is a guess, and it is wrong for the most common case in the
|
|
209
|
-
app. So no decision is made there.
|
|
168
|
+
The failure does no more than that because, at the instant an action fails,
|
|
169
|
+
nothing can know whether a screen will display it: a route action runs before
|
|
170
|
+
its page renders, often before that page exists.
|
|
210
171
|
|
|
211
172
|
## Claiming an error: `__handled_by__`
|
|
212
173
|
|
|
213
174
|
Whatever displays the error sets `error.__handled_by__` (via
|
|
214
|
-
`markErrorAsDisplayedBy`
|
|
215
|
-
|
|
216
|
-
one thing and only one: **this error is on screen somewhere.**
|
|
175
|
+
`markErrorAsDisplayedBy`). That mark means one thing and only one: **this error
|
|
176
|
+
is on screen somewhere.**
|
|
217
177
|
|
|
218
178
|
It is the same mark the jsenv supervisor reads to keep its dev overlay out of
|
|
219
179
|
the way: a window `error` or `unhandledrejection` carrying it is not shown. An
|
|
@@ -237,32 +197,18 @@ unhandled error — window `error` event, jsenv overlay in dev — pointing at t
|
|
|
237
197
|
code that produced it.
|
|
238
198
|
|
|
239
199
|
**Being read is enough to call the report off**, displayed or not. Once a render
|
|
240
|
-
has the error, everything that can happen next is already covered
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
speaks for the failures waiting for the same answer alongside it — a url change
|
|
253
|
-
fails its route actions together — and none of them is reported. What is on
|
|
254
|
-
screen is the failure the render was stopped at, which is the same story. A
|
|
255
|
-
failure arriving _later_, after the page was replaced by what displays the first
|
|
256
|
-
one, is not covered: nothing tells it apart from an action nobody reads.
|
|
257
|
-
|
|
258
|
-
One error is never reported whoever looks at it: a **`NetworkPolicyError`**.
|
|
259
|
-
The app declared the state that produced it and the request never left, so there
|
|
260
|
-
is no bug to point at — it is data a screen shows. When one reaches window
|
|
261
|
-
anyway, navi cancels the event, which keeps the browser console and the jsenv
|
|
262
|
-
overlay out of it. A boundary displaying one still leaves a `console.error` line
|
|
263
|
-
in dev: `preact/debug` logs every error a boundary catches, and reads nothing
|
|
264
|
-
that would tell it the error is handled
|
|
265
|
-
([network_policy.md](./network_policy.md)).
|
|
200
|
+
has the error, everything that can happen next is already covered: it is
|
|
201
|
+
displayed (and marked), or it is thrown — and a thrown error either finds a
|
|
202
|
+
boundary that displays it, or reaches window on its own, since preact re-throws
|
|
203
|
+
what no boundary handled. A render stopped by a delegated throw also speaks for
|
|
204
|
+
the failures waiting on the same answer alongside it — a url change fails its
|
|
205
|
+
route actions together — so none of them is reported; a failure arriving later,
|
|
206
|
+
after the page was replaced by what displays the first one, still is.
|
|
207
|
+
|
|
208
|
+
A **`NetworkPolicyError`** is never reported, whoever looks at it: the app
|
|
209
|
+
declared the state that produced it and the request never left, so there is no
|
|
210
|
+
bug to point at (see [network_policy.md](./network_policy.md), with the one
|
|
211
|
+
`console.error` line `preact/debug` still writes in dev).
|
|
266
212
|
|
|
267
213
|
So what reaches the report is an error **nothing looked at** — an action nobody
|
|
268
214
|
reads, a prerun for a page never opened. And _when_ it is reported follows from
|
|
@@ -272,9 +218,8 @@ that:
|
|
|
272
218
|
- but a route action fails ON the url change, before its page exists — it is the
|
|
273
219
|
routing itself that brings what will display the error. So the deadline waits
|
|
274
220
|
for the document to stop moving and for the frame that paints what the routing
|
|
275
|
-
brought
|
|
276
|
-
|
|
277
|
-
macrotask deadline).
|
|
221
|
+
brought: anything faster tells an app displaying "you are offline" that it
|
|
222
|
+
displayed nothing.
|
|
278
223
|
|
|
279
224
|
Waiting longer costs nothing, precisely because a read is enough to call it off:
|
|
280
225
|
what is still unread by then was going to stay unread. The same error reaching
|
|
@@ -289,9 +234,9 @@ easy to miss when writing another one:
|
|
|
289
234
|
whether anything is displayed, turns a boundary into a bug swallower: a
|
|
290
235
|
`TypeError` it then re-throws reaches window marked as on screen, so the
|
|
291
236
|
overlay that would have pointed at it stays out of the way — a blank page AND
|
|
292
|
-
a silent one. navi's boundary marks
|
|
293
|
-
|
|
294
|
-
|
|
237
|
+
a silent one. navi's boundary marks just before rendering its fallback, and
|
|
238
|
+
re-throws untouched when it has no fallback — nothing to display means nothing
|
|
239
|
+
handled.
|
|
295
240
|
- **Reset on the document URL, not only on the rerun.** Re-running the failed
|
|
296
241
|
action is one way out; going somewhere else is the common one. A boundary that
|
|
297
242
|
only watches the action stays in place of every page after the failure,
|
|
@@ -300,12 +245,15 @@ easy to miss when writing another one:
|
|
|
300
245
|
The one reason to write your own: **filtering what you take.** navi's takes
|
|
301
246
|
everything its subtree throws. An app that wants its own bugs to stay visible
|
|
302
247
|
displays only what is data to it — an error carrying an HTTP status, a
|
|
303
|
-
`NetworkPolicyError` — and re-throws the rest **unmarked**, so the overlay still
|
|
304
|
-
its job. `markErrorAsDisplayedBy` and `errorIsDisplayed` are exported for
|
|
248
|
+
`NetworkPolicyError` — and re-throws the rest **unmarked**, so the overlay still
|
|
249
|
+
does its job. `markErrorAsDisplayedBy` and `errorIsDisplayed` are exported for
|
|
250
|
+
that:
|
|
305
251
|
|
|
306
252
|
```jsx
|
|
307
253
|
const PageErrorBoundary = ({ children, fallback }) => {
|
|
308
254
|
const [error, resetError] = useErrorBoundary();
|
|
255
|
+
// + the second rule: resetError() once the document URL (useDocumentUrl())
|
|
256
|
+
// is no longer the one the error was caught on
|
|
309
257
|
if (!error) {
|
|
310
258
|
return children;
|
|
311
259
|
}
|
package/docs/field_validation.md
CHANGED
|
@@ -10,8 +10,7 @@ them apart is the whole subject.
|
|
|
10
10
|
**What only a browser can answer.** A keystroke blocked before the value exists,
|
|
11
11
|
a callout placed next to the field, `required` on a radio group, `data-one-of`
|
|
12
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.
|
|
13
|
+
(typing? blur? submit?). This is navi's, and it stays navi's.
|
|
15
14
|
|
|
16
15
|
**« Is this value acceptable ».** A length, a set of allowed characters, a
|
|
17
16
|
value that renders nothing, a business rule. This is not a DOM question — a
|
|
@@ -26,20 +25,17 @@ a validity rule, and the constraint is only its browser-side caller.
|
|
|
26
25
|
## Constraints
|
|
27
26
|
|
|
28
27
|
A constraint is `{ name, check(field) }` — a bare `check` function is accepted
|
|
29
|
-
too. `check` returns `null` when the value
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
too. `check` returns `null` when the value passes, or the message to show — a
|
|
29
|
+
string, or `{ message, target }` when the callout belongs on another element
|
|
30
|
+
than the control itself.
|
|
32
31
|
|
|
33
32
|
Navi's own constraints are switched on by an attribute on the control —
|
|
34
|
-
standard when the platform has one, `data-*` when it does not.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
(
|
|
39
|
-
|
|
40
|
-
`data-single-space` is what ends up in the DOM (and in the devtools, and in a
|
|
41
|
-
test selector). The attribute form is accepted too, for a control written in
|
|
42
|
-
plain HTML.
|
|
33
|
+
standard when the platform has one, `data-*` when it does not. **They are
|
|
34
|
+
written as props, in camelCase**, which land on the control host as the
|
|
35
|
+
attribute the constraint reads — the conversion `element.dataset` does:
|
|
36
|
+
`singleSpace` is what you write, `data-single-space` is what ends up in the DOM
|
|
37
|
+
(the devtools, a test selector). The attribute form is accepted too, for a
|
|
38
|
+
control written in plain HTML.
|
|
43
39
|
|
|
44
40
|
| prop | attribute | refuses |
|
|
45
41
|
| ------------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------- |
|
|
@@ -52,9 +48,9 @@ plain HTML.
|
|
|
52
48
|
| `noEmoji` | `data-no-emoji` | an emoji, where a name, an identifier or a title does not want one |
|
|
53
49
|
| `maxLineBreaks` | `data-max-line-breaks` | a value holding more line breaks than that |
|
|
54
50
|
| `oneOf` | `data-one-of` | a value outside the option list its CSS selector points at |
|
|
55
|
-
| `sameAs` | `data-same-as` | a value differing from the field
|
|
51
|
+
| `sameAs` | `data-same-as` | a value differing from the field its CSS selector points at |
|
|
56
52
|
| `minDigit`, `minUpperLetter`, … | `data-min-digit`, … | a password missing a kind of character |
|
|
57
|
-
| `
|
|
53
|
+
| `minDuration` | `data-time-after`, `data-time-min-duration` | a `TimeRangeSpin`/`TimeRangeWheel` span ending before it starts, or too short |
|
|
58
54
|
|
|
59
55
|
```jsx
|
|
60
56
|
<Input required singleSpace noEmoji maxLength={80} />
|
|
@@ -75,39 +71,33 @@ the leading and trailing spaces are removed and a run of spaces becomes one:
|
|
|
75
71
|
<Input name="title" singleSpace="autoFix" />
|
|
76
72
|
```
|
|
77
73
|
|
|
78
|
-
- **At commit, never while typing
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
- **Nothing to say
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
cutting a text would.
|
|
74
|
+
- **At commit, never while typing**: when the field is left, and before an
|
|
75
|
+
action reads the value. Correcting on each keystroke would eat the space the
|
|
76
|
+
person is about to follow with a word, so an action run from typing (a
|
|
77
|
+
search, debounced or not) reads the value as typed.
|
|
78
|
+
- **Nothing to say**: the correction is validity's own rule, so no callout, and
|
|
79
|
+
a server re-checking with `singleSpace: true` accepts what it receives.
|
|
80
|
+
- **It lands everywhere the value lives** — the field, its ui state, the bound
|
|
81
|
+
`signal` — except in a `disabled` or `readOnly` field, whose value is the
|
|
82
|
+
app's and not the person's.
|
|
83
|
+
- **`singleSpace` is the only constraint that offers it**: a correction is worth
|
|
84
|
+
it only where it cannot change what the person meant, which a `maxLength`
|
|
85
|
+
silently cutting a text would.
|
|
91
86
|
|
|
92
87
|
Most constraints take a `<name>Message` prop to replace their sentence for one
|
|
93
88
|
field (`requiredMessage`, `singleSpaceMessage`, `oneOfMessage`…; `step` and the
|
|
94
89
|
time span constraints have none). To change it everywhere, override the i18n
|
|
95
|
-
key instead — see
|
|
90
|
+
key instead — see [Messages](#messages).
|
|
96
91
|
|
|
97
92
|
Two props sit beside them and belong to the browser alone, because they act
|
|
98
93
|
before there is a value to validate: `charGuard` blocks a keystroke that is not
|
|
99
|
-
in its character class
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
the keystroke and refusing the value are different jobs — the pair
|
|
107
|
-
`maxLengthGuard`/`maxLength` is the same split — and a guard brings the value
|
|
108
|
-
check along: `maxLengthGuard` is checked at submit as `maxLength`, `charGuard`
|
|
109
|
-
sets an `Input`'s `pattern` from its class unless one is given. `maxLength`
|
|
110
|
-
alone is the submit-only half.
|
|
94
|
+
in its character class (or one of validity's preset names: `charGuard="tel"`,
|
|
95
|
+
`"slug"`, `"noEmoji"`), `maxLengthGuard` blocks the one that would overflow and
|
|
96
|
+
truncates a paste. Both show what they refused in a callout rather than
|
|
97
|
+
silently swallowing it. Refusing the keystroke and refusing the value are
|
|
98
|
+
different jobs, and a guard brings the value check along: `maxLengthGuard` is
|
|
99
|
+
checked at submit as `maxLength`, `charGuard` sets an `Input`'s `pattern` from
|
|
100
|
+
its class unless one is given. `maxLength` alone is the submit-only half.
|
|
111
101
|
|
|
112
102
|
**A guard answers for the gesture, never for what the field already holds.** A
|
|
113
103
|
value can arrive already outside the class or already too long — a
|
|
@@ -119,21 +109,12 @@ is already there is the constraint's business, and it says so at submit.
|
|
|
119
109
|
|
|
120
110
|
## Reading the validity without submitting
|
|
121
111
|
|
|
122
|
-
`useConstraintValidityState(ref)` gives the control's validity as it stands
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
state.single_space.messageString; // the sentence
|
|
129
|
-
state.reported; // "max_length" — the one the callout says
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
Several constraints fail at once and only one sentence is shown: the one with
|
|
133
|
-
the highest priority — an `error` status first, then `required`, then the
|
|
134
|
-
platform's own constraints, then navi's and the app's, ties going to the first
|
|
135
|
-
registered. `reported` names it, so a summary drawn beside the field says the
|
|
136
|
-
same thing as the callout rather than picking a second one.
|
|
112
|
+
`useConstraintValidityState(ref)` gives the control's validity as it stands
|
|
113
|
+
(see its JSDoc). When several constraints fail at once, only one sentence is
|
|
114
|
+
shown: the one with the highest priority — an `error` status first, then
|
|
115
|
+
`required`, then the platform's own constraints, then navi's and the app's, ties
|
|
116
|
+
going to the first registered. `reported` names it, so a summary drawn beside
|
|
117
|
+
the field says the same thing as the callout rather than picking a second one.
|
|
137
118
|
|
|
138
119
|
`src/control/demos/validation/text_rules_demo.html` is that, one rule per row: a
|
|
139
120
|
value that breaks it, its message read live, and a submit to see the callout.
|
|
@@ -142,46 +123,25 @@ value that breaks it, its message read live, and a submit to see the callout.
|
|
|
142
123
|
|
|
143
124
|
Every sentence navi says is a key in `naviI18n`, and the validation ones are
|
|
144
125
|
`constraint.*`. An app changes one for its whole app by registering over it:
|
|
145
|
-
|
|
146
|
-
```js
|
|
147
|
-
import { naviI18n } from "@jsenv/navi";
|
|
148
|
-
|
|
149
|
-
naviI18n.add("constraint.single_space.consecutive", {
|
|
150
|
-
fr: "Pas deux espaces d'affilée.",
|
|
151
|
-
});
|
|
152
|
-
```
|
|
126
|
+
`naviI18n.add("constraint.single_space.consecutive", { fr: "…" })`.
|
|
153
127
|
|
|
154
128
|
The keys of everything validity owns are validity's own key prefixed with
|
|
155
129
|
`constraint.` — `single_space.start` is `constraint.single_space.start`,
|
|
156
130
|
`char_class.slug` is `constraint.char_class.slug`. That is deliberate: the
|
|
157
131
|
sentence a server returns and the sentence the field shows are looked up under
|
|
158
132
|
one name, so making them agree is registering one key, not maintaining a
|
|
159
|
-
translation table. See
|
|
160
|
-
how the registry itself works.
|
|
133
|
+
translation table. See [i18n.md](./i18n.md) for how the registry itself works.
|
|
161
134
|
|
|
162
135
|
## An app's own rules
|
|
163
136
|
|
|
164
137
|
An app puts its rules in the package its server reads too, and then decides, per
|
|
165
|
-
rule, who runs it
|
|
138
|
+
rule, who runs it — see validity's
|
|
139
|
+
[Rules of your own](../../../tooling/validity/README.md#rules-of-your-own) and
|
|
140
|
+
[Front and back](../../../tooling/validity/README.md#front-and-back-sharing-what-is-worth-sharing).
|
|
141
|
+
The navi side:
|
|
166
142
|
|
|
167
|
-
**Both sides.** The rule is a validity rule; navi wears it as a constraint
|
|
168
|
-
|
|
169
|
-
```js
|
|
170
|
-
// shared/src/text_rules.js — read by the server and by the front
|
|
171
|
-
export const MAX_WORDS_RULE = {
|
|
172
|
-
name: "maxWords",
|
|
173
|
-
applyOn: (maxWords, value) => {
|
|
174
|
-
if (maxWords === undefined || typeof value !== "string") {
|
|
175
|
-
return null;
|
|
176
|
-
}
|
|
177
|
-
const count = value.trim().split(/\s+/).length;
|
|
178
|
-
if (count <= maxWords) {
|
|
179
|
-
return null;
|
|
180
|
-
}
|
|
181
|
-
return { key: "max_words", params: { max: maxWords, count } };
|
|
182
|
-
},
|
|
183
|
-
};
|
|
184
|
-
```
|
|
143
|
+
**Both sides.** The rule is a validity rule; navi wears it as a constraint,
|
|
144
|
+
built once:
|
|
185
145
|
|
|
186
146
|
```js
|
|
187
147
|
// front — once, at module level: a constraint rebuilt on every render is a new
|
|
@@ -195,9 +155,9 @@ const MAX_WORDS_CONSTRAINT = constraintFromValidityRule(MAX_WORDS_RULE, {
|
|
|
195
155
|
<Textarea constraints={[MAX_WORDS_CONSTRAINT]} />;
|
|
196
156
|
```
|
|
197
157
|
|
|
198
|
-
The rule's key is looked up in `naviI18n` under `constraint
|
|
199
|
-
register it there — or pass `formatMessage` beside
|
|
200
|
-
through the app's own i18n instead.
|
|
158
|
+
The rule's key is looked up in `naviI18n` under `constraint.<key>`
|
|
159
|
+
(`constraint.max_words`), so register it there — or pass `formatMessage` beside
|
|
160
|
+
the parameters to say it through the app's own i18n instead.
|
|
201
161
|
|
|
202
162
|
**The server alone.** A rule needing the database, another user's data or a
|
|
203
163
|
secret stays in `createValidity({ rules })` and never reaches the front; its
|