@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.
Files changed (45) hide show
  1. package/README.md +1 -1
  2. package/dist/dev/jsenv_navi.js +192 -43
  3. package/dist/dev/jsenv_navi.js.map +10 -10
  4. package/dist/jsenv_navi.js +190 -41
  5. package/dist/jsenv_navi.js.map +10 -10
  6. package/docs/AI_INSTRUCTIONS.md +177 -261
  7. package/docs/actions.md +164 -251
  8. package/docs/autofocus.md +66 -83
  9. package/docs/control_object.md +117 -214
  10. package/docs/control_value.md +121 -185
  11. package/docs/create_and_edit.md +159 -254
  12. package/docs/css_architecture.md +196 -260
  13. package/docs/data_states.md +41 -44
  14. package/docs/dialog_shape.md +91 -134
  15. package/docs/drag_interactions.md +115 -174
  16. package/docs/drag_to_travel.md +10 -9
  17. package/docs/dynamic_import.md +119 -136
  18. package/docs/error_handling.md +75 -127
  19. package/docs/field_validation.md +49 -89
  20. package/docs/form_changed.md +61 -78
  21. package/docs/group.md +141 -0
  22. package/docs/i18n.md +31 -76
  23. package/docs/interactions.md +6 -4
  24. package/docs/list_action.md +36 -51
  25. package/docs/list_refresh.md +73 -115
  26. package/docs/mobile_touch.md +56 -62
  27. package/docs/navigation.md +377 -723
  28. package/docs/network_policy.md +61 -75
  29. package/docs/pan_zoom.md +62 -103
  30. package/docs/popup_backdrop.md +128 -168
  31. package/docs/popup_lift.md +179 -268
  32. package/docs/popup_open.md +380 -651
  33. package/docs/resource.md +228 -408
  34. package/docs/route_transitions.md +333 -482
  35. package/docs/safe_area.md +102 -89
  36. package/docs/scroll.md +175 -335
  37. package/docs/state_binding.md +2 -1
  38. package/docs/testid.md +18 -23
  39. package/docs/typography.md +111 -141
  40. package/docs/view_transitions.md +106 -104
  41. package/docs/z_index.md +61 -109
  42. package/package.json +2 -2
  43. package/docs/badge_list.md +0 -83
  44. package/docs/control_group.md +0 -168
  45. package/docs/mobile_layout_pitfalls.md +0 -145
@@ -1,10 +1,10 @@
1
1
  # Creating a resource, then editing it
2
2
 
3
3
  The loop almost every application has: a screen that creates something, the page
4
- of the thing just created, and a screen that edits it. It is worth a page of its
5
- own because what makes it hard is not the form — it is that **the two screens
6
- look like the same form and are not the same thing at all**: one holds a draft
7
- the person is writing, the other holds a resource the server owns.
4
+ of the thing just created, and a screen that edits it. What makes it hard is not
5
+ the form — it is that **the two screens look like the same form and are not the
6
+ same thing at all**: one holds a draft the person is writing, the other holds a
7
+ resource the server owns.
8
8
 
9
9
  Working example:
10
10
  [../src/control/demos/integration/create_then_edit/create_then_edit.html](../src/control/demos/integration/create_then_edit/create_then_edit.html)
@@ -24,48 +24,41 @@ and the failures can be looked at.
24
24
 
25
25
  ## What the loop owes the person
26
26
 
27
- The rules below are what the rest of this page is for. Each one is a decision
28
- about what the person is owed, not about navi.
27
+ Each rule is a decision about what the person is owed, not about navi; the rest
28
+ of this page is how it is kept.
29
29
 
30
- - **Creating lands on what was created.** Going back to the list after a
31
- creation asks the person to find their own thing to be sure it exists; the
32
- thing itself is the proof, and it is also where they were heading.
33
- - **A draft is theirs until it is sent, and not a minute longer.** Half a form
34
- filled must survive a reload (it is in the url), and must be gone the next
35
- time "create" is opened — a create screen showing the last thing created is a
36
- screen nobody trusts.
30
+ - **Creating lands on what was created.** The thing itself is the proof it
31
+ exists, and where the person was heading — not the list, where they would
32
+ have to find it.
33
+ - **A draft is theirs until it is sent, and not a minute longer.** It survives a
34
+ reload (it is in the url), and is gone the next time "create" is opened — a
35
+ create screen showing the last thing created is a screen nobody trusts.
37
36
  - **Saving goes back to the thing, and so does a press that had nothing to
38
- send.** The person is done either way; refusing to move because "nothing
39
- changed" makes them press again to find out why.
37
+ send.** Refusing to move because "nothing changed" makes the person press
38
+ again to find out why.
40
39
  - **Cancelling puts back what the server says**, not what was typed and
41
- abandoned. What leaving with unsaved changes does is a decision each screen
42
- takes: dropping them is right when they are a few fields the person chose to
43
- leave (that is what the shape below does, for free — the screen is thrown
44
- away), and worth a confirmation when they are half an hour of work.
45
- - **What was written shows up everywhere at once.** A name changed on one screen
46
- and stale on the list two seconds later reads as data loss. Some of that is
47
- free (the store), some of it is a request the backend has to answer (a list
48
- after a creation) — see [after a write](#after-a-write-what-goes-back-to-the-network).
40
+ abandoned. Dropping unsaved changes is right for a few fields the person chose
41
+ to leave (the shape below does it for free: the screen is thrown away), worth
42
+ a confirmation for half an hour of work — each screen decides.
43
+ - **What was written shows up everywhere at once.** A name stale on the list two
44
+ seconds later reads as data loss. Some of that is free (the store), some is a
45
+ request the backend answers (a list after a creation) — see
46
+ [after a write](#after-a-write-what-goes-back-to-the-network).
49
47
  - **A failure is shown where the thing was asked for**, with what was typed
50
48
  still there: an error on a form that emptied itself is worse than the failure.
51
- - **Every screen is a url.** Reload, back, a link sent to someone — the screens
52
- of this loop are places, and the movement between them says how they are
53
- related.
54
-
55
- Each of them has a mechanism below, and each mechanism is a line of code, not a
56
- framework.
49
+ - **Every screen is a url** — reload, back, a link sent to someone — and the
50
+ movement between them says how they are related.
57
51
 
58
52
  ## The routes
59
53
 
60
- Four places, and each of them is a url someone can open, reload or share:
61
-
62
54
  ```js
63
55
  const HOME_ROUTE = route("/");
64
56
  const NEW_GAME_ROUTE = route("/games/new", {
65
57
  searchParams: {
66
- name: nameSignal,
67
- level: levelSignal,
68
- players: playersSignal,
58
+ name: draftNameSignal,
59
+ level: draftLevelSignal,
60
+ players: draftPlayersSignal,
61
+ place: draftPlaceSignal,
69
62
  },
70
63
  });
71
64
  const GAME_ROUTE = route("/games/:gameId");
@@ -77,14 +70,10 @@ The create screen declares its fields as **search params**: a draft that
77
70
  survives a reload and travels in a link. The edit screen declares none — its
78
71
  values belong to the resource, not to the position.
79
72
 
80
- Two facts about matching decide the rest, and both surprise:
81
-
82
- - **Several routes match at once.** A route matches by prefix, so `/` is still
83
- matching on `/games/2/edit`, and `/games/new` is also a `/games/:gameId`.
84
- That is what keeps a section active while one is inside it.
85
- - **The first that matches wins**, in the order it is written — the `<Route>`
86
- children, and the pages of a travel row alike. So they go from the most
87
- precise to the widest:
73
+ `/games/new` is also a `/games/:gameId`, and the first branch that matches wins,
74
+ in the order it is written — `<Route>` children and the pages of a travel row
75
+ alike — so they go from the most precise to the widest (how a pattern matches:
76
+ [navigation.md](./navigation.md#rendering-routes)):
88
77
 
89
78
  ```jsx
90
79
  <Route>
@@ -95,8 +84,8 @@ Two facts about matching decide the rest, and both surprise:
95
84
  </Route>
96
85
  ```
97
86
 
98
- Same thing for what a route LOADS: the loader of `/games/:gameId` must step
99
- aside on `/games/new`, which is a page and not a game.
87
+ The loader of `/games/:gameId` steps aside on `/games/new`, which is a page and
88
+ not a game:
100
89
 
101
90
  ```js
102
91
  const GAME_OF_ROUTE = routeAction(
@@ -117,8 +106,8 @@ const GAME_OF_ROUTE = routeAction(
117
106
  ## The resource
118
107
 
119
108
  Declare the REST callbacks once ([resource.md](./resource.md)) and the store
120
- does the rest — this is what makes the detail page show the new name **the
121
- moment the PUT answers**, with nobody reloading anything:
109
+ does the rest — the detail page shows the new name **the moment the PUT
110
+ answers**, with nobody reloading anything:
122
111
 
123
112
  ```js
124
113
  const GAME = resource("game", {
@@ -129,41 +118,29 @@ const GAME = resource("game", {
129
118
  });
130
119
  ```
131
120
 
132
- Two things to know or the screen stays empty:
133
-
134
- - **Reading an action does not start it.** `useAsyncData` waits for data
135
- someone else asked for; what asks is the route (`routeAction`). A component
136
- reading an action nobody runs suspends forever, and the whole `<Loading>`
137
- subtree stays blank — and an effect in that component cannot rescue it, a
138
- suspended component has no effects. What to do instead is under "Reading an
139
- action" in [actions.md](./actions.md#reading-an-action).
140
- - **Handle the error where it happens**, or hand it to an `<ErrorBoundary>`:
141
- `useAsyncData(action, { loading: true, error: true })` returns
142
- `[data, loading, error]`, which is what lets a page draw its own "the server
143
- refused" with a "try again" that calls `action.rerun()`. Which of the two, and
144
- what happens to an error no screen takes:
145
- [error_handling.md](./error_handling.md).
121
+ Reading an action does not start it — the route does (`routeAction`), and a
122
+ component reading an action nobody runs stays blank for good
123
+ ([actions.md](./actions.md#reading-an-action)). Handle the error where it
124
+ happens (`{ loading: true, error: true }` returns `[data, loading, error]`, for
125
+ a page drawing its own "try again") or hand it to an `<ErrorBoundary>`
126
+ ([error_handling.md](./error_handling.md)).
146
127
 
147
128
  ## Two screens, two states
148
129
 
149
- The same three fields, and two different things behind them:
130
+ The same fields, and two different things behind them:
150
131
 
151
132
  - the **create** screen holds a **draft** — nobody else's, not saved anywhere,
152
133
  worth keeping while it is being written. It belongs in the url
153
134
  (`searchParams`), which is what makes it survive a reload and travel in a
154
135
  link;
155
136
  - the **edit** screen holds **what the server has**, loaded, and proposes
156
- changes to it. It is the screen's own state, alive as long as the screen is.
137
+ changes to it: the screen's own state, alive as long as the screen is.
157
138
 
158
- So the fields are written once, and know nothing about which screen they are in:
139
+ So the fields are written once, know nothing about which screen they are in,
140
+ and each screen hands them its own signals:
159
141
 
160
142
  ```jsx
161
- const GameFormFields = ({
162
- nameSignal,
163
- levelSignal,
164
- playersSignal,
165
- loading,
166
- }) => (
143
+ const GameFormFields = ({ nameSignal, levelSignal, loading }) => (
167
144
  <>
168
145
  <Input name="name" signal={nameSignal} loading={loading} required />
169
146
  <Select name="level" signal={levelSignal} loading={loading}>
@@ -171,17 +148,9 @@ const GameFormFields = ({
171
148
  </Select>
172
149
  </>
173
150
  );
174
- ```
175
-
176
- Each screen hands it its own:
177
151
 
178
- ```jsx
179
152
  // create: the draft, which lives in the url
180
- <GameFormFields
181
- nameSignal={draftNameSignal}
182
- levelSignal={draftLevelSignal}
183
- playersSignal={draftPlayersSignal}
184
- />;
153
+ <GameFormFields nameSignal={draftNameSignal} levelSignal={draftLevelSignal} />;
185
154
 
186
155
  // edit: this screen's own, filled when the game arrives
187
156
  const nameSignal = useSignal(undefined);
@@ -190,8 +159,7 @@ const nameSignal = useSignal(undefined);
190
159
  **Do not let the two share one set of signals.** It is the mistake this shape
191
160
  exists to prevent, and it does not look like one: bind both screens to the same
192
161
  `nameSignal`, edit a game, then press "create" — the game you just edited is
193
- sitting in the create form, and in the url. A draft and a resource are not the
194
- same state; one is on the screen, the other is at the backend.
162
+ sitting in the create form, and in the url.
195
163
 
196
164
  The draft's other half is the end of its life:
197
165
 
@@ -205,30 +173,24 @@ action={async (values) => {
205
173
  ```
206
174
 
207
175
  `undefined`, not `""`: a state signal put back to undefined returns to its
208
- default and leaves the url — see [control_value.md](./control_value.md).
176
+ default and leaves the url (see
177
+ [control_value.md](./control_value.md#signal--defaultvalue-the-answer-and-where-it-starts)).
178
+ **Clear after navigating, not before**: those signals are read by the screen
179
+ being left — its list of places is asked for with the place the draft holds —
180
+ and emptying them while it is still up asks for that list again, for nobody.
209
181
 
210
- **Clear after navigating, not before.** Those signals are read by things on the
211
- screen being left — the list of places is asked for with the place the draft
212
- holds — so emptying them while that screen is still up asks for the list again,
213
- for a screen nobody is looking at any more. Navigate first and the clearing is
214
- what it should be: tidying up behind oneself.
215
-
216
- > One signal for the whole form works too: `<Form signal={gameSignal}>` fills
217
- > its named children from the object, follows it when something else writes it,
218
- > and writes back the whole object as the fields change. Reach for it when the
219
- > values arrive as one object and no field needs a url of its own — the fields
220
- > then take nothing but their `name`.
182
+ One signal for the whole form works too, when the values arrive as one object
183
+ (see [control_value.md](./control_value.md#what-a-signal-holds-control-by-control)).
221
184
 
222
185
  ## The edit screen opens before its values
223
186
 
224
- The resource arrives a request after the screen. Two shapes, and both are
225
- right — the question is what the person should be looking at meanwhile:
187
+ The resource arrives a request after the screen. Two shapes, both right — the
188
+ question is what the person looks at meanwhile.
226
189
 
227
190
  **The screen waits, showing the form.** The fields are there, empty and busy
228
- (`loading` on a control marks it `aria-busy` and shows it), and they fill in
229
- when the resource lands. Nothing blinks in and out, and a long screen does not
230
- collapse to a spinner. What it costs is one thing to say — **when the filling is
231
- done**:
191
+ (`loading` on a control marks it `aria-busy` and shows it), and fill in when the
192
+ resource lands: nothing blinks, and a long screen does not collapse to a
193
+ spinner. What it costs is saying **when the filling is done**:
232
194
 
233
195
  ```jsx
234
196
  const [game, loading, error] = useAsyncData(GAME_OF_ROUTE, {
@@ -243,66 +205,40 @@ const [game, loading, error] = useAsyncData(GAME_OF_ROUTE, {
243
205
  <Form pristineKey={game?.id}>;
244
206
  ```
245
207
 
246
- `onLoad` is what the screen does with the data **once, when it becomes known**,
247
- and the two hard parts are already answered by it:
248
-
249
- - **How often.** Not every time the data arrives — a successful PUT, a list
250
- reloading, a poll all hand the same game back, and copying it again would
251
- overwrite what the person is in the middle of writing. It fires once per set
252
- of params, which is the action's own answer to "is this another thing, or the
253
- same one again". Written by hand this is a `useEffect` keyed on `game?.id`,
254
- and `[game]` is the natural, wrong, thing to write.
255
- - **When.** From a layout effect, so what it writes belongs to the same tick as
256
- the render that got the data. That is what lets `pristineKey` be the id
257
- itself: the form takes its reference again at the end of that tick, and by
258
- then the fields are filled. A copy written in a passive effect (after the
259
- paint) would be too late — the screen would open **already changed**, and Save
260
- would send the resource back to the server untouched.
261
-
262
- The rest of `pristineKey` is in [form_changed.md](./form_changed.md).
263
-
264
- **Or the screen waits, showing nothing of the form**: render a skeleton until
265
- the resource is there, then the form with its values already in the fields.
266
- Nothing to announce then — the form holds them from its first render — but the
267
- screen has to be one that can be blanked without the person losing their place.
208
+ `onLoad` fires once per set of params: a PUT, a list reloading, a poll all hand
209
+ the same game back, and copying it again would overwrite what the person is
210
+ writing. It fires from a layout effect, so the fields are filled by the time
211
+ `pristineKey`, the id itself, takes the reference (see
212
+ [actions.md](./actions.md#reading-an-action),
213
+ [form_changed.md](./form_changed.md#a-screen-filled-after-it-opened-pristinekey)).
214
+ By hand this is a `useEffect` keyed on `game?.id` — `[game]` is the natural,
215
+ wrong, thing to write — and a passive effect is too late: the screen opens
216
+ **already changed**, and Save sends the resource back untouched.
217
+
218
+ **Or the screen waits, showing nothing of the form**: a skeleton until the
219
+ resource is there, then the form holding its values from its first render —
220
+ for a screen that can be blanked without the person losing their place.
268
221
 
269
222
  Either way, cancelling is a link away: the screen is thrown away with what was
270
223
  typed in it, and coming back re-reads the resource. There is nothing to restore.
271
224
 
272
225
  ## A field that picks from a list too big to load
273
226
 
274
- A place, a player, a category: the field is a picker, and its popup holds a list
275
- the backend answers with. The list is **one page** — the nearest, the most
276
- recent, whatever a `LIMIT` returned. And the screen is pre-filled from the url:
277
-
278
- ```
279
- /games/new?place=halle-des-sports
280
- ```
281
-
282
- That is what pre-filling is: a link from a place's page, a back button, a
283
- reload, a shared invitation. The signal is the selection, and the url is its
284
- memory. But the url carries an **identifier and nothing else** — no name — so
285
- the screen looks for it in the list it has:
286
-
287
- ```js
288
- const selected = places.find((place) => place.id === placeId);
289
- ```
290
-
291
- Nothing guarantees that `find`. The place may be far down the ranking, created
292
- by someone else a minute ago, or named in a link built elsewhere. Nothing is
293
- broken — the screen falls back on the identifier — but it then shows
294
- `halle-des-sports` where it promised "Halle des sports", which is the opposite
295
- of what pre-filling was for. Rare, never seen in development, and always at
296
- someone else's.
297
-
298
- Two ways out, and only one of them keeps the feature:
299
-
300
- - **Clear the signals when the screen opens.** No selection, no selection to
301
- display. This throws away pre-filling, which is the reason those params exist:
302
- removing a feature is not fixing its edge case.
303
- - **Ask for the list SAYING what you already hold.** What the screen holds
304
- travels with the request, and the backend guarantees that item is in the
305
- answer — on top of its page, whatever its rank.
227
+ A place, a player, a category: the field is a picker whose popup holds **one
228
+ page** of what the backend has — the nearest, the most recent, whatever a
229
+ `LIMIT` returned. And the screen is pre-filled from the url
230
+ (`/games/new?place=halle-des-sports`): a link from a place's page, a back
231
+ button, a reload, a shared invitation. The url carries an **identifier and
232
+ nothing else**, so the screen looks for it in the list it has —
233
+ `places.find((place) => place.id === placeId)` — and nothing guarantees that
234
+ `find`: the place may be far down the ranking, created a minute ago, or named in
235
+ a link built elsewhere. The screen then shows `halle-des-sports` where it
236
+ promised "Halle des sports".
237
+
238
+ Clearing the signals when the screen opens would throw pre-filling away with its
239
+ edge case. The way out that keeps it: **ask for the list SAYING what you already
240
+ hold**, and the backend guarantees that item is in the answer, whatever its
241
+ rank.
306
242
 
307
243
  ```js
308
244
  const PLACES_OF_SCREEN = routeAction(
@@ -321,43 +257,35 @@ const PLACES_OF_SCREEN = routeAction(
321
257
  );
322
258
  ```
323
259
 
324
- Reading the selection in the params is also what makes the list **reload when
325
- the selection changes** — the action reruns on its own. Which is why the edit
326
- screen returns `null` until the resource is there: asking for the list before
327
- knowing what it must contain is asking for it twice, and the first answer is the
328
- one that cannot show the name.
329
-
330
- What `include` is, on the backend side, is the whole subject:
331
-
332
- - **an addition, not a filter.** The page stays the page; the asked-for item is
333
- in it as well if it was not already, and once if it was;
334
- - **it takes what a url can hold** — a slug as much as an id;
335
- - **an `include` that designates nothing is not an error.** The answer is simply
336
- the page, and the screen falls back on its "not found" case — which then says
337
- something true (that place is gone) instead of being an artefact of
338
- pagination;
339
- - it takes **several** values when several fields are pre-filled:
260
+ Reading the selection in the params also makes the list **reload when the
261
+ selection changes**. And the edit screen returns `null` until the resource is
262
+ there: asked before knowing what it must contain, the list is asked for twice,
263
+ and the first answer cannot show the name.
264
+
265
+ A paginated list is not "the first N", it is "the first N **plus what the caller
266
+ already holds**". On the backend side, `include` is:
267
+
268
+ - **an addition, not a filter** — the page stays the page, with the asked-for
269
+ item in it once;
270
+ - **what a url can hold** — a slug as much as an id;
271
+ - **never an error when it designates nothing**: the answer is the page, and the
272
+ screen's "not found" case then says something true (that place is gone)
273
+ instead of being an artefact of pagination;
274
+ - **several values** when several fields are pre-filled:
340
275
  `GET /users?include=42,57`.
341
276
 
342
- > If a url signal designates an item of a paginated list, the identifier it
343
- > carries must travel with the list request, and the backend must guarantee its
344
- > presence in the answer. A paginated list is not "the first N", it is "the first
345
- > N **plus what the caller already holds**".
346
-
347
- The other half of the same problem is answered by the resource rather than by
348
- the list: **what comes back with a resource carries its own label**. The game's
349
- own page shows "Lieu: Halle des sports" with no list at all, because the GET
350
- answers with the name next to the id. Only the url is reduced to an identifier,
351
- and that is exactly where `include` earns its place.
277
+ The other half is answered by the resource: **what comes back with a resource
278
+ carries its own label**. The game's page shows "Lieu: Halle des sports" with no
279
+ list at all, because the GET answers with the name next to the id. Only the url
280
+ is reduced to an identifier, and that is where `include` earns its place.
352
281
 
353
282
  ## Where each screen goes next
354
283
 
355
- The two screens navigate for opposite reasons, and that is why they say it in
356
- two different places:
284
+ The two screens navigate for opposite reasons, so they say it in two different
285
+ places ([form_changed.md](./form_changed.md#what-follows-a-send)):
357
286
 
358
287
  - **Create**: the page to land on is the one the server just made, and its id
359
- comes back with the response. Nothing can be declared before the send, so the
360
- action navigates:
288
+ comes back with the response, so the action navigates:
361
289
 
362
290
  ```jsx
363
291
  <Form
@@ -368,9 +296,8 @@ two different places:
368
296
  >
369
297
  ```
370
298
 
371
- - **Edit**: the destination is known before the send — and it has to be, because
372
- a press with **nothing to send** must leave too. That is `command`, which runs
373
- whether or not there was an action to run:
299
+ - **Edit**: the destination is known before the send — and has to be, because a
300
+ press with **nothing to send** must leave too. That is `command`:
374
301
 
375
302
  ```jsx
376
303
  <Form
@@ -380,12 +307,11 @@ two different places:
380
307
  ```
381
308
 
382
309
  Do not hold that submit back with `readOnlyWhileFormUnchanged`: the press
383
- still does something — it leaves. Holding it back is for a form that goes
384
- nowhere, where the press would visibly do nothing at all.
310
+ still does something — it leaves.
385
311
 
386
312
  ## After a write: what goes back to the network
387
313
 
388
- Creating one game, measured on the demo:
314
+ Creating one game:
389
315
 
390
316
  ```
391
317
  POST /games the creation
@@ -393,87 +319,69 @@ GET /games the list re-reads itself
393
319
  GET /games/2 the page of what was just created
394
320
  ```
395
321
 
396
- Each of the two GETs is a decision, and neither is an accident:
397
-
398
- - **The list re-reads itself** because whether a new item belongs to a list
399
- depends on filters, pagination and sort — the backend knows, the client does
400
- not (`rerunOn.GET_MANY: ["POST"]`, and the whole table of defaults is in
401
- [list_refresh.md](./list_refresh.md)). A `PUT` does **not** re-read it: the
402
- store carries the new values into every list already holding that item, which
403
- is why the name changed on the game's page shows up on the list without a
404
- request.
405
- - **The detail GET is not saved by the store.** The action for that id had never
406
- run, and the store holding the item is not the same thing as an action having
407
- its data. It is also often not redundant: a detail representation is richer
408
- than what a write answers — here the GET adds the place's name, the POST does
409
- not, and a screen trusting the POST would show "Lieu: —". Skipping it could
410
- only ever be a per-resource decision ("my POST answers the same shape as my
411
- GET").
412
-
413
- Coming back to that page later in the same session costs **nothing**: an action
414
- that has completed is not run again for the same params. That, and not any
415
- cache, is what makes a screen already visited open instantly — and what a
416
- `rerun()` is for when something must genuinely be read again.
322
+ The list re-reads itself after a `POST` — whether a new item belongs to it is
323
+ the backend's to say — and not after a `PUT`, whose values the store carries
324
+ into every list holding that item (see
325
+ [list_refresh.md](./list_refresh.md#rerunon-verb-by-verb)). **The detail GET is
326
+ not saved by the store**: the action for that id had never run, and the store
327
+ holding the item is not an action having its data. Nor is it redundant: a
328
+ detail representation is richer than what a write answers — here the GET adds
329
+ the place's name, and a screen trusting the POST would show "Lieu: —". Skipping
330
+ it could only ever be a per-resource decision ("my POST answers the same shape
331
+ as my GET").
332
+
333
+ Coming back to that page later in the session costs **nothing**: a completed
334
+ action is not run again for the same params — that, not a cache, opens a visited
335
+ screen instantly; `rerun()` is for what must genuinely be read again.
417
336
 
418
337
  ## Movement between them
419
338
 
420
339
  The screens are places, so the movement between them is `RouteTravel` — not
421
- `SlideContainer`, which is for positions that are not routes (see
422
- [navigation.md](./navigation.md#tabs-that-are-not-routes)).
423
-
424
- A row says two things at once, and they are said apart. Its `<Route>` children
425
- are ordered by matching precision (above); `routes` is the order of the
426
- **journey** — what "one step that way" means:
340
+ `SlideContainer`, which is for positions that are not routes
341
+ ([navigation.md](./navigation.md#tabs-that-are-not-routes)). Its `<Route>`
342
+ children are ordered by matching precision (above); `routes` gives the order of
343
+ the **journey** ([navigation.md](./navigation.md#tabs-that-travel-routetravel)):
427
344
 
428
345
  ```jsx
429
346
  <RouteTravel routes={[NEW_GAME_ROUTE, GAME_ROUTE]}>
430
347
  ```
431
348
 
432
349
  Creating sits to the left of the game, so arriving on what was just created goes
433
- right ("here is what I just made"). **A page left out of `routes` does not
434
- travel at all**: the list is not a step along this row — one opens the create
435
- screen from it, one does not slide there — so it is absent, and that move has no
436
- animation. Leaving a page out is how a movement is refused; there is no "no
437
- transition" to ask for.
350
+ right. **A page left out of `routes` does not travel**: one opens the create
351
+ screen from the list, one does not slide there, so the list is absent and that
352
+ move plays nothing — leaving a page out is how a movement is refused.
438
353
 
439
354
  A pair with a movement of its own gets a row of its own, on its own axis. The
440
355
  game and its edit screen are the same thing seen two ways, so they travel
441
- vertically inside the position the outer row holds for them:
356
+ vertically inside the place the outer row holds for them — editing above
357
+ (the column's order is the children's), coming down over the game, sent back up
358
+ by saving:
442
359
 
443
360
  ```jsx
444
361
  const GameArea = () => (
445
- <RouteTravel axis="y" routes={[EDIT_GAME_ROUTE, GAME_ROUTE]}>
362
+ <RouteTravel axis="y">
446
363
  <Route>
447
364
  <Route route={EDIT_GAME_ROUTE} element={EditGamePage} />
448
365
  <Route route={GAME_ROUTE} element={GamePage} />
449
366
  </Route>
450
367
  </RouteTravel>
451
368
  );
452
- ```
453
369
 
454
- Editing sits above the game on that column, so it comes down over it and saving
455
- sends it back up. The outer row does not move for it: both urls are the same position
456
- there, which is what makes the two rows independent —
457
-
458
- ```jsx
370
+ // in the outer row: the same element on both branches
459
371
  <Route route={EDIT_GAME_ROUTE} element={GameArea} />
460
372
  <Route route={GAME_ROUTE} element={GameArea} />
461
373
  ```
462
374
 
463
- — the same element on both branches, so the inner row stays mounted across the
464
- two and has something to travel between.
465
-
466
- The gesture, the back button and a link pressed all move the same way.
375
+ The outer row does not move for it — the edit url is not one of its pages — and
376
+ the same element on both branches keeps the inner row mounted, with something
377
+ to travel between.
467
378
 
468
379
  ## The same loop in a dialog
469
380
 
470
- Everything above assumes each screen is a url, because that is what the loop
471
- deserves when the thing being created is what the person came for. Half the
472
- creations in an application are not that: a radar, a note, a member — a
473
- **secondary resource**, edited without leaving the page one is reading. It has no
474
- page of its own, and giving it one to change a name would take the person off the
475
- page they were on. That loop happens in a dialog, and the reasoning above still
476
- holds; only its mechanisms change.
381
+ Each screen is a url when the thing created is what the person came for. A
382
+ **secondary resource** — a radar, a note, a member — is edited without leaving
383
+ the page one is reading, so its loop happens in a dialog: the reasoning holds,
384
+ only its mechanisms change.
477
385
 
478
386
  | the page says | the dialog says |
479
387
  | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
@@ -483,23 +391,20 @@ holds; only its mechanisms change.
483
391
  | `RouteTravel` between the two screens | nothing: a dialog has no neighbour |
484
392
  | `onLoad` + `pristineKey`: the resource arrives after | it is already in hand — the dialog is opened FROM the list |
485
393
 
486
- What does not change, and is the reason this section is four lines rather than a
487
- page of its own:
394
+ What does not change:
488
395
 
489
396
  - **Two states, not one.** "Do not let the two share one set of signals" applies
490
- word for word. The reset is a `key` on the form rather than emptying url
491
- signals, but the mistake avoided is the same: opening "new" and finding what
492
- was just edited.
493
- - **Two modes driving a third component.** The fields are written once and do not
494
- know which mode they are in; the caller hands them their state. That is the
495
- transposable half of the whole page.
496
- - **`include` for a pre-filled picker.** A radar's place is exactly the case
497
- described [above](#a-field-that-picks-from-a-list-too-big-to-load), and a popup
498
- does not make it less true.
499
- - **The failure shows where it was asked for**, with what was typed still there —
500
- which in a dialog also means the dialog must still be open, so the close waits
501
- for the action (see
502
- [popup_open.md](./popup_open.md#closing-when-a-button-also-runs-an-action)).
397
+ word for word; the reset is a `key` on the form — naming WHICH record the
398
+ dialog is opened on, not following a record that moved, which is a
399
+ [`defaultValue`'s job](./control_value.md#a-defaultvalue-follows-what-it-was-read-from)
400
+ — rather than emptying url signals.
401
+ - **Two modes driving a third component**: the fields are written once, and the
402
+ caller hands them their state.
403
+ - **`include` for a pre-filled picker** — a radar's place is exactly the case
404
+ [above](#a-field-that-picks-from-a-list-too-big-to-load).
405
+ - **The failure shows where it was asked for**, with what was typed still there
406
+ — so the dialog must still be open, and the close waits for the action
407
+ ([popup_open.md](./popup_open.md#closing-when-a-button-also-runs-an-action)).
503
408
 
504
409
  ## See also
505
410