@jsenv/navi 0.29.417 → 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 +347 -83
- package/dist/dev/jsenv_navi.js.map +12 -12
- package/dist/jsenv_navi.js +345 -81
- package/dist/jsenv_navi.js.map +12 -12
- package/docs/AI_INSTRUCTIONS.md +177 -258
- 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 +291 -454
- package/docs/drag_to_travel.md +152 -748
- 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 +170 -326
- 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 -473
- package/docs/safe_area.md +102 -89
- package/docs/scroll.md +175 -317
- package/docs/state_binding.md +40 -84
- 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/create_and_edit.md
CHANGED
|
@@ -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.
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
28
|
-
|
|
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.**
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
- **A draft is theirs until it is sent, and not a minute longer.**
|
|
34
|
-
|
|
35
|
-
|
|
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.**
|
|
39
|
-
|
|
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.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
52
|
-
|
|
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:
|
|
67
|
-
level:
|
|
68
|
-
players:
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
99
|
-
|
|
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 —
|
|
121
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
|
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
|
|
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,
|
|
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.
|
|
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
|
|
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
|
-
|
|
211
|
-
|
|
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,
|
|
225
|
-
|
|
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
|
|
229
|
-
|
|
230
|
-
|
|
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`
|
|
247
|
-
and
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
the screen
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
- **an addition, not a filter
|
|
333
|
-
in it
|
|
334
|
-
- **
|
|
335
|
-
- **an
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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,
|
|
356
|
-
|
|
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
|
|
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
|
|
372
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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
|
|
422
|
-
[navigation.md](./navigation.md#tabs-that-are-not-routes)).
|
|
423
|
-
|
|
424
|
-
|
|
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
|
|
434
|
-
|
|
435
|
-
|
|
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
|
|
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"
|
|
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
|
-
|
|
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
|
-
|
|
464
|
-
|
|
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
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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
|
|
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
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
- **`include` for a pre-filled picker
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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
|
|