@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/actions.md
CHANGED
|
@@ -22,7 +22,6 @@ them by hand — see [resource.md](./resource.md).
|
|
|
22
22
|
|
|
23
23
|
- [Params: `bindParams`, and calling the action](#params-bindparams-and-calling-the-action)
|
|
24
24
|
- [The answer is kept on the instance that ran](#the-answer-is-kept-on-the-instance-that-ran)
|
|
25
|
-
- [A debounced signal asks where it settles, not where it passed](#a-debounced-signal-asks-where-it-settles-not-where-it-passed)
|
|
26
25
|
- [The in-between states of a gesture are real params](#the-in-between-states-of-a-gesture-are-real-params)
|
|
27
26
|
- [Running: `run`, `rerun`, `prerun`, `reset`](#running-run-rerun-prerun-reset)
|
|
28
27
|
- [Aborting saves resources, it does not undo](#aborting-saves-resources-it-does-not-undo)
|
|
@@ -45,55 +44,43 @@ const getUser123 = getUser.bindParams({ id: 123 });
|
|
|
45
44
|
await getUser123.run();
|
|
46
45
|
```
|
|
47
46
|
|
|
48
|
-
Two `bindParams` with equal params give **the same instance** (deep equality)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
one instance of it. What this rests on is params being plain data: a `Set`, a
|
|
57
|
-
`Map`, an element, a function or a signal inside them is compared by reference,
|
|
58
|
-
and four callers then really do make four requests.
|
|
47
|
+
Two `bindParams` with equal params give **the same instance** (deep equality):
|
|
48
|
+
that is what shares state between two components asking for the same thing, and
|
|
49
|
+
deduplicates their requests, however each one derived its params — a `computed`
|
|
50
|
+
or a `useMemo` per caller included. What this rests on is params being plain
|
|
51
|
+
data: a `Set`, a `Map`, an element, a function, or a signal nested below a
|
|
52
|
+
param (`{ filter: { id: idSignal } }`) is compared by reference, and four
|
|
53
|
+
callers then really do make four requests. A signal given as a param's own
|
|
54
|
+
value (`{ id: idSignal }`) is read, and compared by what it holds.
|
|
59
55
|
|
|
60
56
|
An action is callable, and calling it is the short way to bind and run in one
|
|
61
|
-
go
|
|
57
|
+
go — for a run that is a **gesture** (a click handler, an event, a step in a
|
|
58
|
+
flow), where the params are known at that moment and the run is the point:
|
|
62
59
|
|
|
63
60
|
```js
|
|
64
61
|
getUser({ id: 123 }); // getUser.bindParams({ id: 123 }).rerun()
|
|
65
62
|
getUser(); // getUser.rerun()
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
Use it wherever a run is a **gesture** — a click handler, an event, a step in a
|
|
69
|
-
flow — where the params are known at that moment and the run is the point:
|
|
70
|
-
|
|
71
|
-
```js
|
|
72
63
|
const deleteGame = (game) => GAME.DELETE({ id: game.id });
|
|
73
64
|
```
|
|
74
65
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
66
|
+
Calling it `rerun()`s it: the run happens even if that instance already holds
|
|
67
|
+
data, which is what you want from a gesture and not from a component asking for
|
|
68
|
+
data. A run that fails **rejects**, or **throws synchronously** when the
|
|
69
|
+
callback was synchronous, so an action awaiting it fails with it — which is what
|
|
70
|
+
puts the error on the control that asked for the gesture (the runs with nobody
|
|
71
|
+
to reject at: [error_handling.md](./error_handling.md#what-a-failing-action-does)).
|
|
81
72
|
|
|
82
73
|
Use `bindParams` when what you need is the **instance**, not the run: to keep a
|
|
83
|
-
handle on it, or so that a run started in one place is visible from another
|
|
84
|
-
|
|
85
|
-
to draw its own busy and error states (see
|
|
74
|
+
handle on it, or so that a run started in one place is visible from another
|
|
75
|
+
(what a control gains from holding it:
|
|
86
76
|
[resource.md](./resource.md#a-function-calling-the-verb-or-the-instance)).
|
|
87
|
-
Note that calling the action `rerun()`s it — the run happens even if that
|
|
88
|
-
instance already holds data, which is what you want from a gesture and not what
|
|
89
|
-
you want from a component asking for data.
|
|
90
77
|
|
|
91
|
-
Params may be signals, and then the
|
|
78
|
+
Params may be signals, and then the instance follows them:
|
|
92
79
|
|
|
93
80
|
```js
|
|
94
81
|
const userAction = getUser.bindParams({ id: userIdSignal });
|
|
95
|
-
// a new id retargets it to the instance for that id;
|
|
96
|
-
//
|
|
82
|
+
// a new id retargets it to the instance for that id; a component reads it,
|
|
83
|
+
// and starts it, with useAsyncData(userAction, { run: true })
|
|
97
84
|
```
|
|
98
85
|
|
|
99
86
|
`{ debounce }` puts a delay between the signal and the instance, for params that
|
|
@@ -109,20 +96,18 @@ const [found, searching] = useAsyncData(searchAction, {
|
|
|
109
96
|
});
|
|
110
97
|
```
|
|
111
98
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
`
|
|
117
|
-
is
|
|
118
|
-
the previous answer — `loading` is what says a newer one is coming, don't
|
|
119
|
-
compare params by hand to find out.
|
|
99
|
+
The delay lives in the binding — no effect, nothing to own — and two call sites
|
|
100
|
+
passing the same signal and delay share the instance. During the delay it holds
|
|
101
|
+
the previous answer, and `loading` says a newer one is coming: don't compare
|
|
102
|
+
params by hand. It follows where the signal **settles** (`A → B → A` sends
|
|
103
|
+
nothing for `B`). `actionRunEffect`, which takes the action itself and a params
|
|
104
|
+
getter, is for a request that must go out whether or not something draws it.
|
|
120
105
|
|
|
121
106
|
### The answer is kept on the instance that ran
|
|
122
107
|
|
|
123
|
-
An action's state — `dataSignal`, `errorSignal`, `runningStateSignal` — belongs
|
|
124
|
-
the instance the params made, never to the action they were bound from. So
|
|
125
|
-
thing to read is whatever `bindParams` (or anything built on it) gave back:
|
|
108
|
+
An action's state — `dataSignal`, `errorSignal`, `runningStateSignal` — belongs
|
|
109
|
+
to the instance the params made, never to the action they were bound from. So
|
|
110
|
+
the thing to read is whatever `bindParams` (or anything built on it) gave back:
|
|
126
111
|
|
|
127
112
|
```js
|
|
128
113
|
const COUNTS = routeAction(ANY_PAGE, ADMIN_COUNTS.GET, () => true);
|
|
@@ -132,62 +117,41 @@ ADMIN_COUNTS.GET.dataSignal.value; // undefined, for ever
|
|
|
132
117
|
```
|
|
133
118
|
|
|
134
119
|
`routeAction` binds params — `() => true` is params too — so the answer lands on
|
|
135
|
-
the child, and the verb
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
Anything else derives from the instance:
|
|
142
|
-
|
|
143
|
-
```js
|
|
144
|
-
export const countsSignal = computed(() => COUNTS.dataSignal.value);
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
### A debounced signal asks where it settles, not where it passed
|
|
148
|
-
|
|
149
|
-
The delay publishes where the signal **settles**, never a value it only passed
|
|
150
|
-
through on the way. A question that goes `A → B → A` inside the delay is the
|
|
151
|
-
question `A`: no request goes out for `B`, and `loading` never went up. Only a
|
|
152
|
-
signal that is somewhere new once the delay elapses moves the binding.
|
|
153
|
-
|
|
154
|
-
That is load-bearing rather than a nicety, because settling is a **comparison,
|
|
155
|
-
not a countdown**: `paramsSettlingSignal` is true while the signal and the
|
|
156
|
-
binding differ and false when they agree. Move the binding to a value the signal
|
|
157
|
-
is not on and nothing is left that could make them agree again — the screen is
|
|
158
|
-
told a newer answer is coming, with no delay still running to bring one, and
|
|
159
|
-
only another write to the source frees it. So a value the source never settled
|
|
160
|
-
on does not cost a stale request, it costs a screen stuck on "loading".
|
|
120
|
+
the child, and the verb stays idle: nothing failed, nothing warns, the screen
|
|
121
|
+
simply has no numbers in it. The same holds for the instance a control runs
|
|
122
|
+
([below](#the-instance-a-control-runs)). A module-level signal derived from a
|
|
123
|
+
verb — `computed(() => ME.GET.dataSignal.value)` — only works for a verb
|
|
124
|
+
something runs **directly**, with no params; anything else derives from the
|
|
125
|
+
instance: `computed(() => COUNTS.dataSignal.value)`.
|
|
161
126
|
|
|
162
127
|
### The in-between states of a gesture are real params
|
|
163
128
|
|
|
164
129
|
The delay decides _when_ a question goes out, never _which_ one is coherent: it
|
|
165
130
|
sees every write, including the ones a gesture only passes through. Moving an
|
|
166
|
-
item
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
Make the intermediate not matter, rather than counting on the delay to hide it.
|
|
171
|
-
Two ways, both worth having on their own:
|
|
131
|
+
item by filling the new slot and then emptying the old one is two writes, and
|
|
132
|
+
between them the item is in both — a question nobody meant to ask, sent if the
|
|
133
|
+
delay lands there. Make the intermediate not matter rather than counting on the
|
|
134
|
+
delay to hide it:
|
|
172
135
|
|
|
173
136
|
- **`batch()` the writes that belong to one gesture**, so the intermediate state
|
|
174
|
-
never exists
|
|
175
|
-
|
|
176
|
-
- **Shape the question so states
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
changing the question — better than asking it late.
|
|
137
|
+
never exists — not for the delay, not for a render, not for a validation rule
|
|
138
|
+
reading the same signals.
|
|
139
|
+
- **Shape the question so equivalent states compare equal.** Params are compared
|
|
140
|
+
deeply, so a list whose order carries nothing asks a new question every time
|
|
141
|
+
it is rearranged; sent deduplicated and sorted, it stops changing.
|
|
180
142
|
|
|
181
143
|
## Running: `run`, `rerun`, `prerun`, `reset`
|
|
182
144
|
|
|
183
145
|
| Method | Does |
|
|
184
146
|
| ---------- | ------------------------------------------------------------------------ |
|
|
185
|
-
| `run()` | Asks for the data
|
|
147
|
+
| `run()` | Asks for the data; running or completed, it joins what is there. |
|
|
186
148
|
| `rerun()` | Runs again whatever state it is in — a refresh, an explicit "check now". |
|
|
187
149
|
| `prerun()` | Same as `run()`, in the background: nothing asked for it on screen yet. |
|
|
188
150
|
| `reset()` | Aborts what is running and puts the action back to idle, data and all. |
|
|
189
151
|
| `abort()` | Calls off the run in flight, keeping the data it had. |
|
|
190
152
|
|
|
153
|
+
A "check now" wired to `run()` therefore checks nothing, silently.
|
|
154
|
+
|
|
191
155
|
### Aborting saves resources, it does not undo
|
|
192
156
|
|
|
193
157
|
`abort()` cancels what can still be cancelled — a `fetch` wired to the
|
|
@@ -205,83 +169,58 @@ const [user] = useAsyncData(userAction);
|
|
|
205
169
|
```
|
|
206
170
|
|
|
207
171
|
`useAsyncData` suspends until the data is there and throws on failure, leaving
|
|
208
|
-
both to the nearest `<Loading>` and `<ErrorBoundary>`;
|
|
209
|
-
|
|
210
|
-
mounted
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
state at once — `{ idle, loading, completed, aborted, error, data, params }` —
|
|
216
|
-
for a component that needs to look at it rather than render it. It reads that
|
|
217
|
-
very instance: to know what moves when a control is handed the action, see
|
|
218
|
-
[the instance a control runs](#the-instance-a-control-runs).
|
|
172
|
+
both to the nearest `<Loading>` and `<ErrorBoundary>`; `{ loading: true }` or
|
|
173
|
+
`{ error: true }` hands either over as a value instead, the component staying
|
|
174
|
+
mounted — what it then draws is [data_states.md](./data_states.md), where a
|
|
175
|
+
failure nobody takes goes is [error_handling.md](./error_handling.md).
|
|
176
|
+
`useActionStatus(action)` gives the whole state of that very instance at once —
|
|
177
|
+
`{ idle, loading, completed, aborted, error, data, params }` — for a component
|
|
178
|
+
that needs to look at it rather than render it.
|
|
219
179
|
|
|
220
180
|
**Reading does not run.** `useAsyncData` waits for data someone else asked for
|
|
221
181
|
— for a page, the route, through `routeAction`. A component reading an action
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
decided: a `routeAction`, a `<Button action>`, the `action` of the `<Link>` one
|
|
229
|
-
came in by — or the component owns its request: a folding panel, a slice a
|
|
230
|
-
button asks for. That second one is `{ run: true }`, said in the same line that
|
|
231
|
-
reads the data:
|
|
182
|
+
nobody ran suspends, and `<Loading>` draws nothing for an idle action: the
|
|
183
|
+
subtree stays blank, for good. A `useEffect` that would `run()` it never fires,
|
|
184
|
+
since a suspended component has no effects. So either the action is started
|
|
185
|
+
**before** anything reads it — a `routeAction`, a `<Button action>`, the
|
|
186
|
+
`action` of the `<Link>` one came in by — or the component owns its request and
|
|
187
|
+
says so where it reads the data, which starts it from the render:
|
|
232
188
|
|
|
233
189
|
```jsx
|
|
234
190
|
// asks for it if nobody did, from the render that reads it
|
|
235
191
|
const [members] = useAsyncData(membersAction, { run: true });
|
|
236
192
|
```
|
|
237
193
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
when asked for — and running an action twice costs nothing, so nothing has to
|
|
243
|
-
be guarded.
|
|
244
|
-
|
|
245
|
-
Where that wait is caught is the caller's business, and a popup is no exception
|
|
246
|
-
— see [popup_open.md](./popup_open.md#a-popup-that-loads-data) for what that
|
|
247
|
-
costs when it is forgotten.
|
|
194
|
+
`loading` and `error` keep their meaning, and running an action twice costs
|
|
195
|
+
nothing, so nothing has to be guarded. Where that wait is caught is the caller's
|
|
196
|
+
business, a popup included
|
|
197
|
+
([popup_open.md](./popup_open.md#a-popup-that-loads-data)).
|
|
248
198
|
|
|
249
199
|
**`{ run: true }` is the fallback, not the shape to reach for.** A route action
|
|
250
200
|
is asked for when the ADDRESS changes, with everything else that address needs;
|
|
251
|
-
this one cannot start before the component that draws it exists
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
Outside a component — a request the app owns rather than a component —
|
|
258
|
-
`actionRunEffect(action, () => …)` is that same run declared once: it runs on
|
|
259
|
-
the first truthy params, reruns when they change, aborts when they go false. It
|
|
260
|
-
is the very machinery `routeAction` is built on.
|
|
201
|
+
this one cannot start before the component that draws it exists — a render late
|
|
202
|
+
at best, a gesture late in a popup. It is for a parameter chosen inside the
|
|
203
|
+
component and dying with it. Outside a component,
|
|
204
|
+
`actionRunEffect(action, () => …)` is that same run declared once — it runs on
|
|
205
|
+
the first truthy params, reruns when they change, aborts when they go false —
|
|
206
|
+
and the machinery `routeAction` is built on.
|
|
261
207
|
|
|
262
208
|
**Which of the two it is, is decided by the parameter, not by what draws it.**
|
|
263
209
|
If the parameter is in the address — a path param, a search param bound to a
|
|
264
210
|
`stateSignal` — the data belongs to the screen, and it is a `routeAction`, even
|
|
265
|
-
when a popup is the only thing that draws it
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
cannot survive: `POST /users/:id/invitations` hung on `/users/:id` mints a token
|
|
279
|
-
per reload, for ever, and nothing at the call site says so. So a run that writes
|
|
280
|
-
belongs to the component that decided to write, whatever its parameter, and
|
|
281
|
-
`{ run: true }` is what says it. That is the shape of a request prepared for a
|
|
282
|
-
gesture — a token a share button must already hold when it is pressed, since the
|
|
283
|
-
OS share sheet only opens inside the gesture and never after an `await`: it runs
|
|
284
|
-
at mount because the press cannot wait for it, not because the address changed.
|
|
211
|
+
when a popup is the only thing that draws it: being open can be bound too, and
|
|
212
|
+
the data of a popup that binds it is a route action like any other. Getting this
|
|
213
|
+
wrong is invisible at the call site and expensive on screen: the request leaves
|
|
214
|
+
with the gesture instead of with the screen, one waterfall behind everything
|
|
215
|
+
else the address needed.
|
|
216
|
+
|
|
217
|
+
**A route action is a read.** An address asks again on every arrival — a
|
|
218
|
+
reload, a pasted link, a step back — which is what a read is for and what a
|
|
219
|
+
write cannot survive: `POST /users/:id/invitations` hung on `/users/:id` mints a
|
|
220
|
+
token per reload, for ever. A run that writes belongs to the component that
|
|
221
|
+
decided to write, whatever its parameter, and `{ run: true }` says it — a token
|
|
222
|
+
a share button must already hold when pressed, for instance, since the OS share
|
|
223
|
+
sheet only opens inside the gesture, never after an `await`.
|
|
285
224
|
|
|
286
225
|
`{ onLoad }` is what the screen does with the data **once, when it becomes
|
|
287
226
|
known** — seed the fields someone is about to edit, focus something, remember
|
|
@@ -297,49 +236,41 @@ const [game] = useAsyncData(gameAction, {
|
|
|
297
236
|
```
|
|
298
237
|
|
|
299
238
|
It fires once per set of params, never again for a rerun that brings the same
|
|
300
|
-
thing back
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
render that got the data — which is what lets a `<Form pristineKey>` take its
|
|
305
|
-
reference on filled fields (see
|
|
306
|
-
[create_and_edit.md](./create_and_edit.md#the-edit-screen-opens-before-its-values)).
|
|
307
|
-
|
|
308
|
-
Controls take the action itself and wire the rest: `<Button action>` runs it on
|
|
309
|
-
click, shows its loading state, and puts its error where the user can see it.
|
|
310
|
-
That is the reason to pass an action instance rather than an arrow calling the
|
|
311
|
-
callback:
|
|
312
|
-
|
|
313
|
-
```jsx
|
|
314
|
-
// ✓ loading, error and disabled states come from the action
|
|
315
|
-
<Button action={GAME_CANDIDATES.POST.bindParams({ id: game.id, user_id })}>
|
|
316
|
-
Accept
|
|
317
|
-
</Button>
|
|
318
|
-
```
|
|
239
|
+
thing back — a save, a refresh, a poll would otherwise overwrite what the person
|
|
240
|
+
is writing — and from a layout effect, so a `<Form pristineKey>` takes its
|
|
241
|
+
reference on filled fields
|
|
242
|
+
([create_and_edit.md](./create_and_edit.md#the-edit-screen-opens-before-its-values)).
|
|
319
243
|
|
|
320
244
|
## The instance a control runs
|
|
321
245
|
|
|
322
|
-
A control
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
246
|
+
A control takes the action and wires the rest — `<Button action>` runs it on
|
|
247
|
+
click, draws its wait, puts its error where the user can see it — whether it is
|
|
248
|
+
handed an action or a plain function, which it wraps into one (what that wrapper
|
|
249
|
+
lacks against a resource verb's instance:
|
|
250
|
+
[resource.md](./resource.md#a-function-calling-the-verb-or-the-instance)). It
|
|
251
|
+
binds what it is given to its own UI state and runs the result, so which
|
|
252
|
+
instance runs depends on the value the control carries:
|
|
253
|
+
|
|
254
|
+
- a **button outside any form or group** contributes no params, so what it runs
|
|
255
|
+
**is** the instance it was handed: `useActionStatus` on that instance sees the
|
|
256
|
+
click, the run, the data.
|
|
257
|
+
- a **button inside a `<Form>` or another group**, with no `value` of its own,
|
|
258
|
+
carries the value around it — a submit carries its form's — and runs the
|
|
259
|
+
child instance bound to that value, as the controls below do.
|
|
328
260
|
- a control that **carries a value** — an input, a form — runs the child
|
|
329
261
|
instance bound to that value: the value merged over the params the caller
|
|
330
262
|
bound, and a value that is not an object **replaces them outright**. A
|
|
331
|
-
checkbox handed `ACTION.bindParams({ id })` runs `ACTION(true)`, and `id`
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
`action={() => ACTION({ id, sure: true })}`.
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
that is:
|
|
263
|
+
checkbox handed `ACTION.bindParams({ id })` runs `ACTION(true)`, and `id` is
|
|
264
|
+
gone — the call fails wherever it needed it, far from the binding that lost
|
|
265
|
+
it. Bind params to a control that holds a value only when the value completes
|
|
266
|
+
them (a form's object over a bound `{ id }`); when it would replace them, pass
|
|
267
|
+
a function and keep the params in its closure:
|
|
268
|
+
`action={() => ACTION({ id, sure: true })}`.
|
|
269
|
+
|
|
270
|
+
In those last two cases the params are not the caller's, so neither is the
|
|
271
|
+
status: the instance the caller holds stays idle while the control's run moves.
|
|
272
|
+
Read the effect where it lands (the store, for a resource verb), or listen to
|
|
273
|
+
the run itself — every control with an `action` reports the run it performs:
|
|
343
274
|
|
|
344
275
|
```jsx
|
|
345
276
|
<Button
|
|
@@ -358,25 +289,26 @@ displayed (see [error_handling.md](./error_handling.md)); an abort to
|
|
|
358
289
|
|
|
359
290
|
## `action` or `uiAction`
|
|
360
291
|
|
|
361
|
-
Both fire when a control's value changes — and on a control that holds no
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
292
|
+
Both fire when a control's value changes — and on a control that holds no value,
|
|
293
|
+
a `<Button>` or a `<Link>`, the press itself is that occasion: each runs once per
|
|
294
|
+
press. They are not two ways of writing the same thing:
|
|
295
|
+
|
|
296
|
+
| | `uiAction(value, event)` | `action` |
|
|
297
|
+
| ----------------- | ------------------------------------------ | ---------------------------------------------------- |
|
|
298
|
+
| what it is | a plain callback | an action bound to the control's value |
|
|
299
|
+
| while it runs | nothing | the control is busy (`aria-busy`, its loading state) |
|
|
300
|
+
| if it fails | an unhandled rejection — nothing on screen | an error callout on the control |
|
|
301
|
+
| a popup around it | closes | refuses to close until it is done |
|
|
365
302
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
| while it runs | nothing | the control is busy (`aria-busy`, its loading state) |
|
|
370
|
-
| if it fails | an unhandled rejection — nothing on screen | an error callout on the control, and the control goes back to what it held |
|
|
371
|
-
| a popup around it | closes | refuses to close until it is done |
|
|
303
|
+
A failed `action` also puts back what the control held where `resetOnError` is
|
|
304
|
+
on — by default a checkbox, a radio, their groups, a picker, a selectable list
|
|
305
|
+
and `<Details>`; a text input and a form keep the value that was refused.
|
|
372
306
|
|
|
373
307
|
`uiAction` is a notification: the value has changed, here it is. Use it for what
|
|
374
308
|
cannot fail — logging, moving something else on screen, keeping a local
|
|
375
|
-
variable.
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
an action instance: a plain function is wrapped into one, bound to the control's
|
|
379
|
-
value, so the callback receives what the control now holds.
|
|
309
|
+
variable. Anything that can fail or take time is an `action`, and it does not
|
|
310
|
+
have to be an action instance: a plain function is wrapped into one, bound to
|
|
311
|
+
the control's value, so the callback receives what the control now holds.
|
|
380
312
|
|
|
381
313
|
```jsx
|
|
382
314
|
// ✓ the box shows it is saving, says so if the save fails, and goes back to
|
|
@@ -390,10 +322,8 @@ value, so the callback receives what the control now holds.
|
|
|
390
322
|
|
|
391
323
|
The give-away is an `async` `uiAction`, or one that calls something that writes:
|
|
392
324
|
`uiAction` never waits for what it starts, so nobody is left holding the result.
|
|
393
|
-
|
|
394
325
|
To merely REMEMBER the value rather than send it, neither is the answer: bind a
|
|
395
|
-
signal and drop the callback
|
|
396
|
-
[control_value.md](./control_value.md).
|
|
326
|
+
signal and drop the callback — see [control_value.md](./control_value.md).
|
|
397
327
|
|
|
398
328
|
## A press that opens something and waits for the answer
|
|
399
329
|
|
|
@@ -404,18 +334,21 @@ one that asks something of a control near it is a `command` (a value proposed is
|
|
|
404
334
|
Reaching for a plain `onClick` usually means one of those was missed.
|
|
405
335
|
|
|
406
336
|
An `action` and a `command` on the same press are one gesture in two halves, and
|
|
407
|
-
the command is the second one: it waits for the action and runs only if it
|
|
408
|
-
succeeded
|
|
337
|
+
the command is the second one: **it waits for the action and runs only if it
|
|
338
|
+
succeeded.** "Leave the group, then go back to the list" therefore never leaves
|
|
409
339
|
anyone on a list they are still in — a failure or an abort drops the command and
|
|
410
340
|
the error stays on the control, where the user is looking. `<Picker
|
|
411
|
-
type="confirm">` follows the same rule with its `action` and `command`, and
|
|
412
|
-
|
|
341
|
+
type="confirm">` follows the same rule with its `action` and `command`, and so
|
|
342
|
+
does `<Form command>`, which follows a submission only once it went through.
|
|
413
343
|
|
|
414
344
|
### Deleting something, then leaving the page it was on
|
|
415
345
|
|
|
416
|
-
The everyday form of that pair,
|
|
417
|
-
|
|
418
|
-
|
|
346
|
+
The everyday form of that pair, where what is destroyed is asked about first. A
|
|
347
|
+
short question is asked on the button itself: `<Button confirm>` replaces it with
|
|
348
|
+
the question and a Confirmer/Annuler pair, and the second press does what the
|
|
349
|
+
button was for, its wait and its error on that button. A question long enough to
|
|
350
|
+
want a popup, or a row with no room for it, is a `<Picker type="confirm">`, whose
|
|
351
|
+
work runs back on the trigger:
|
|
419
352
|
|
|
420
353
|
```jsx
|
|
421
354
|
<Picker
|
|
@@ -430,23 +363,18 @@ is a popup the picker already owns, and the work runs back on the trigger.
|
|
|
430
363
|
Yes closes the question and the request goes from the trigger — busy while it
|
|
431
364
|
runs, an error callout on it if it fails — and the list is reached only once the
|
|
432
365
|
group is really gone. A failure leaves the reader on the page of a group they
|
|
433
|
-
are still in, which is the truth, with the reason in front of them.
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
the
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
A function calling the verb, not the instance: the trigger is busy and draws the
|
|
447
|
-
error either way, and `bindParams` earns its place only when something else on
|
|
448
|
-
screen must see this very run — see
|
|
449
|
-
[resource.md](./resource.md#a-function-calling-the-verb-or-the-instance).
|
|
366
|
+
are still in, which is the truth, with the reason in front of them. Navigating
|
|
367
|
+
first and awaiting after — `navTo(...)` then `await leave(...)` — buys nothing
|
|
368
|
+
(the response and the navigation land in the same task) and, on a failure,
|
|
369
|
+
leaves the reader in a list that still holds the group they think they left,
|
|
370
|
+
told nothing.
|
|
371
|
+
|
|
372
|
+
Where to go afterwards is the app's decision: staying on the page to say it is
|
|
373
|
+
done, or letting the route land on its empty state, are as good as leaving for
|
|
374
|
+
the list. What the `command` settles is only that the success is what decides.
|
|
375
|
+
(The `action` above is a function calling the verb; what that gives up against
|
|
376
|
+
the verb's instance is in
|
|
377
|
+
[resource.md](./resource.md#a-function-calling-the-verb-or-the-instance).)
|
|
450
378
|
|
|
451
379
|
On a link — a `Link`, a `<Button href>` or `<Button route>` — the three fire on
|
|
452
380
|
the press, before the navigation, and the navigation waits for none of them: a
|
|
@@ -455,39 +383,24 @@ writes a draft synchronously is found by the next page, and a request goes on
|
|
|
455
383
|
its own while the page changes. Work that decides the destination is not a
|
|
456
384
|
link's: it navigates itself, from a `<Button action>`.
|
|
457
385
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
a popup, and an `action` that runs on what the popup settled.
|
|
463
|
-
|
|
464
|
-
```jsx
|
|
465
|
-
<Picker variant="icon" rightSlotIcon={<DisketteSvg />} action={onCreated}>
|
|
466
|
-
<GuestSavePrompt kind="player" name={guest.name} />
|
|
467
|
-
</Picker>
|
|
468
|
-
```
|
|
469
|
-
|
|
470
|
-
Written this way, what the popup needs to know travels as props rather than
|
|
471
|
-
through the press, and the popup is built the first time it opens rather than
|
|
472
|
-
once per row. See
|
|
473
|
-
[popup_open.md](./popup_open.md#a-press-that-opens-a-popup-and-acts-on-it).
|
|
386
|
+
A press that opens something and then does something with what came of it —
|
|
387
|
+
"save this guest", pressed on a row — is not a fourth case: it is a `Picker`, a
|
|
388
|
+
trigger, a popup, and an `action` that runs on what the popup settled (see
|
|
389
|
+
[popup_open.md](./popup_open.md#a-press-that-opens-a-popup-and-acts-on-it)).
|
|
474
390
|
|
|
475
391
|
What is left for an `onClick` is what no value can express — imperative work
|
|
476
|
-
with nothing to open and nothing to send.
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
[interactions.md](./interactions.md#an-affordance-inside-somebody-elses-box-selfinteractions))
|
|
480
|
-
so the usual objection — an `onClick` fires on a read-only control — does not
|
|
481
|
-
apply there. Everywhere else it does.
|
|
392
|
+
with nothing to open and nothing to send. The usual objection to it — it fires
|
|
393
|
+
on a read-only control — holds everywhere but on a control with
|
|
394
|
+
`selfInteractions`, whose gate a caller's `onClick` runs inside
|
|
395
|
+
([interactions.md](./interactions.md#an-affordance-inside-somebody-elses-box-selfinteractions)).
|
|
482
396
|
|
|
483
397
|
## `uiAction` mirrors the state, it does not report a gesture
|
|
484
398
|
|
|
485
|
-
`uiAction` fires whenever the control's state changes, whoever changed it
|
|
486
|
-
button and a link
|
|
487
|
-
|
|
488
|
-
typing is one cause among several: a `value` prop coming back down after a
|
|
399
|
+
`uiAction` fires whenever the control's state changes, whoever changed it (a
|
|
400
|
+
button and a link hold no state to mirror: there, each press is one call). The
|
|
401
|
+
user typing is one cause among several: a `value` prop coming back down after a
|
|
489
402
|
render, a popup control propagating its choice up to the picker holding it, a
|
|
490
|
-
group cascading a value into its children — all of them reach `uiAction
|
|
403
|
+
group cascading a value into its children — all of them reach `uiAction`, so
|
|
491
404
|
that a signal or a local variable listening to it never drifts out of sync.
|
|
492
405
|
Reading it as "the user did something" is the natural mistake.
|
|
493
406
|
|