@jsenv/navi 0.29.418 → 0.29.420
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 +584 -196
- package/dist/dev/jsenv_navi.js.map +35 -35
- package/dist/jsenv_navi.js +563 -187
- package/dist/jsenv_navi.js.map +35 -35
- 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 +169 -254
- package/docs/css_architecture.md +196 -260
- package/docs/data_states.md +39 -43
- 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 +66 -81
- 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/dynamic_import.md
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
# Code loaded on demand
|
|
2
2
|
|
|
3
|
-
What an app wants from an `import()` is that **a screen's code arrives the way
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
with. Running it from the hook itself is the fallback, as for data.
|
|
3
|
+
What an app wants from an `import()` is that **a screen's code arrives the way its
|
|
4
|
+
data does**: one wait, one failure, one place that shows both. A module fetched on
|
|
5
|
+
demand is a request like any other — it takes time, it fails when the network is
|
|
6
|
+
gone or a deploy happened in between — and nothing on screen should have to know
|
|
7
|
+
whether what it waits for is bytes of data or bytes of code.
|
|
8
|
+
|
|
9
|
+
So navi has nothing lazy-shaped: **the import is an action**, started by what asks
|
|
10
|
+
for it — the address through `routeAction`, a state through `actionRunEffect` —
|
|
11
|
+
and read by the hook a screen already reads its data with. Running it from the
|
|
12
|
+
hook itself is the fallback, as for data.
|
|
14
13
|
|
|
15
14
|
## A page: its code is a route action
|
|
16
15
|
|
|
17
16
|
The address asks for the page's data through a route action; it asks for the
|
|
18
|
-
page's code the same way, and the two are in flight together from the url
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
page's code the same way, and the two are in flight together from the url change.
|
|
18
|
+
The page component in the entry reads the code like data — with `loading: true`
|
|
19
|
+
and `error: true`, drawing the page's own pending screen and its own unavailable
|
|
20
|
+
screen where the page will stand:
|
|
22
21
|
|
|
23
22
|
```jsx
|
|
23
|
+
// after setupRoutes(): a route's paramsSignal is null until then
|
|
24
24
|
const GAME_PAGE_ACTION = routeAction(GAME_ROUTE, GAME.GET, () => ({
|
|
25
25
|
gameId: GAME_ROUTE.paramsSignal.value.gameId,
|
|
26
26
|
}));
|
|
@@ -45,51 +45,43 @@ const GamePage = () => {
|
|
|
45
45
|
<Route route={GAME_ROUTE} element={GamePage} />;
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
-
Why a branch rather than a `<Loading>` around it: suspending takes the
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
48
|
+
Why a branch rather than a `<Loading>` around it: suspending takes the subtree
|
|
49
|
+
away, puts the boundary's fallback in its place, then mounts the page again — a
|
|
50
|
+
cost worth paying when a page already on screen re-reads its data (see
|
|
51
|
+
[data_states.md](./data_states.md)), and worth nothing here, where the page is not
|
|
52
|
+
there yet. `loading: true` never suspends: one component, no wrapper, a pending
|
|
53
|
+
screen in the page's own shape, and the failure beside it. What is delegated stays
|
|
54
|
+
delegated — the page's data, read inside `game_page.jsx` with
|
|
55
|
+
`useAsyncData(GAME_PAGE_ACTION)`, still waits in the `<Loading>` written between
|
|
56
|
+
the routes ([navigation.md](./navigation.md#loading-data)). That page module
|
|
57
|
+
imports the routes module back for `GAME_PAGE_ACTION`; a dynamic import is not a
|
|
58
|
+
load-order cycle.
|
|
59
59
|
|
|
60
60
|
Everything the data layer decides then holds for the code:
|
|
61
61
|
|
|
62
|
-
- **One failure.** A rejected import fails the run like a failed read
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
- **One failure.** A rejected import fails the run like a failed read, drawn by the
|
|
63
|
+
branch above, or by the nearest `<ErrorBoundary>` when the page reads its code
|
|
64
|
+
without `error: true`.
|
|
65
65
|
- **Fetched once.** A route action left behind is aborted, not reset, and a
|
|
66
|
-
completed one is not run again: coming back to the page renders
|
|
67
|
-
|
|
68
|
-
- **Fetched ahead
|
|
69
|
-
on intent (below).
|
|
70
|
-
- **Counted.** The routing is busy until the code is there, like for the
|
|
71
|
-
data.
|
|
72
|
-
|
|
73
|
-
The page module itself is ordinary — it reads its data with
|
|
74
|
-
`useAsyncData(GAME_PAGE_ACTION)` like any page — and imports the routes module
|
|
75
|
-
back for it; a dynamic import is not a load-order cycle.
|
|
66
|
+
completed one is not run again: coming back to the page renders synchronously,
|
|
67
|
+
only the data is asked for per params.
|
|
68
|
+
- **Fetched ahead**, on intent ([below](#ahead-of-the-render-intent)).
|
|
76
69
|
|
|
77
70
|
## A component inside a page that stays: who asks for its code
|
|
78
71
|
|
|
79
|
-
A plan drawn on demand inside a page that stays
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
and holding its answer across mounts:
|
|
72
|
+
A plan drawn on demand inside a page that stays draws its wait and its failure
|
|
73
|
+
where its frame is, with `loading: true` and `error: true`, as a page does. What
|
|
74
|
+
differs is **who starts the import**, and the order of preference is the one
|
|
75
|
+
`useAsyncData` states for data: **the run belongs to what asks, not to the
|
|
76
|
+
render.**
|
|
77
|
+
|
|
78
|
+
1. **The address asks** → `routeAction`, above. A section of a page that has an
|
|
79
|
+
address (`/places/:id/plan`) is a page.
|
|
80
|
+
2. **A state asks** → `actionRunEffect`. A plan drawn once a mode is entered, a
|
|
81
|
+
tab chosen, a popup opened: the signal that says so starts the import, declared
|
|
82
|
+
once at module scope — one action for every instance, started by the state
|
|
83
|
+
change rather than by the render that follows it, and holding its answer across
|
|
84
|
+
mounts:
|
|
93
85
|
|
|
94
86
|
```jsx
|
|
95
87
|
const PLAN_CODE = actionRunEffect(
|
|
@@ -105,7 +97,9 @@ what asks, not to the render.**
|
|
|
105
97
|
return (
|
|
106
98
|
<PlanFrame>
|
|
107
99
|
{loading ? <Text>…</Text> : null}
|
|
108
|
-
{error ?
|
|
100
|
+
{error ? (
|
|
101
|
+
<Button action={() => window.location.reload()}>Recharger</Button>
|
|
102
|
+
) : null}
|
|
109
103
|
{Plan ? <Plan /> : null}
|
|
110
104
|
</PlanFrame>
|
|
111
105
|
);
|
|
@@ -114,8 +108,7 @@ what asks, not to the render.**
|
|
|
114
108
|
|
|
115
109
|
3. **Nothing above the component asks** → `useAsyncData` with a function, the
|
|
116
110
|
fallback: the component's own existence is the only thing that asks, so the
|
|
117
|
-
request is its own,
|
|
118
|
-
for the life of the instance.
|
|
111
|
+
request is its own, for the life of the instance.
|
|
119
112
|
|
|
120
113
|
```jsx
|
|
121
114
|
const [Plan, loading, error] = useAsyncData(
|
|
@@ -124,118 +117,108 @@ what asks, not to the render.**
|
|
|
124
117
|
);
|
|
125
118
|
```
|
|
126
119
|
|
|
127
|
-
It starts one render late, and per instance: a component fetched this way
|
|
128
|
-
|
|
120
|
+
It starts one render late, and per instance: a component fetched this way in
|
|
121
|
+
three places asks three times (the browser fetches the module once), and
|
|
129
122
|
coming back to it draws its wait again for the microtask the import takes to
|
|
130
123
|
answer from the module map. Reach for it when there is genuinely no state to
|
|
131
124
|
read the request off — not to save declaring one.
|
|
132
125
|
|
|
133
|
-
Delegating instead (the hook without `loading: true`, under a `<Loading>`
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
happens before the module's components exist, so nothing inside it is parked
|
|
140
|
-
by the code arriving.
|
|
126
|
+
Delegating instead (the hook without `loading: true`, under a `<Loading>` around
|
|
127
|
+
the frame) follows the rule for data: **the `<Loading>` goes where the fallback
|
|
128
|
+
should land.** A boundary around the whole router takes the router away while a
|
|
129
|
+
page loads — top bar, tabs and all. Nothing has to be kept out of a subtree loaded
|
|
130
|
+
this way: the suspension happens before the module's components exist, so nothing
|
|
131
|
+
inside it is parked by the code arriving.
|
|
141
132
|
|
|
142
133
|
## Ahead of the render: intent
|
|
143
134
|
|
|
144
135
|
**A link preloads where it leads when the pointer or the focus reaches it.**
|
|
145
|
-
`<Link>` and `<Button>` with an `href` or a `route` do it by default
|
|
146
|
-
`prefetch={false}` opts one out
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
`routeAction(route, action, undefined, { prefetch: false })` keeps a
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
A prefetch that fails is not reported: nothing on screen asked for it. The
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
136
|
+
`<Link>` and `<Button>` with an `href` or a `route` do it by default
|
|
137
|
+
(`prefetch={false}` opts one out): every route of `setupRoutes()` the url matches,
|
|
138
|
+
a section and the page inside it alike, preruns **its route actions declared
|
|
139
|
+
without a params function** — a page's code, a read without params. One whose
|
|
140
|
+
params come from the address waits for the arrival (a link's prefetch has no
|
|
141
|
+
address, the id may still be chosen), and so does one given a params function
|
|
142
|
+
that asks for nothing (`() => true`).
|
|
143
|
+
`routeAction(route, action, undefined, { prefetch: false })` keeps a param-less
|
|
144
|
+
read that is not worth a hover out of it.
|
|
145
|
+
|
|
146
|
+
A prefetch that fails is not reported: nothing on screen asked for it. The failure
|
|
147
|
+
stays on the action, and a failed action is run again by whatever asks next — the
|
|
148
|
+
arrival, which shows its own failure, and every later hover alike.
|
|
149
|
+
|
|
150
|
+
From code: `route.preload()` for a route about to be navigated to,
|
|
151
|
+
`preloadUrl(url)` for an address. A press that computes its destination has
|
|
152
|
+
nothing to preload — the navigation itself asks.
|
|
162
153
|
|
|
163
154
|
## What a transition photographs
|
|
164
155
|
|
|
165
156
|
A route element still waiting is what the container rendered — its pending
|
|
166
|
-
screen, or the fallback of the boundary it suspended into — so a route
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
gets photographed when it is not.
|
|
157
|
+
screen, or the fallback of the boundary it suspended into — so a route transition
|
|
158
|
+
or a `RouteTravel` takes its picture of **that**, code and data alike. navi does
|
|
159
|
+
not hold a navigation for a chunk: the honest answer to "a chunk is a hundred
|
|
160
|
+
milliseconds" is the prefetch above — the code is there before the press — and a
|
|
161
|
+
skeleton in the page's own shape is what gets photographed when it is not.
|
|
172
162
|
|
|
173
163
|
## `import.meta.css` on the far side of a split
|
|
174
164
|
|
|
175
165
|
A module's stylesheet ships **inside its chunk** and is adopted when the
|
|
176
|
-
assignment runs (see [css_architecture.md](./css_architecture.md))
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
is the same
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
in the build fixes. A rule that depends on it is a rule to make more
|
|
191
|
-
specific.
|
|
166
|
+
assignment runs (see [css_architecture.md](./css_architecture.md)) — for the
|
|
167
|
+
app's own modules: code from `node_modules` goes to a shared vendor chunk, its
|
|
168
|
+
stylesheet with it. Two consequences:
|
|
169
|
+
|
|
170
|
+
- **The fallback cannot use a class the lazy module defines.** It is rendered by
|
|
171
|
+
the eager side, before the chunk exists. The frame of what it stands in for —
|
|
172
|
+
background, border, aspect ratio — belongs to the eager module, the lazy one
|
|
173
|
+
owning only the inside; duplicating the frame into the eager side is the same
|
|
174
|
+
answer.
|
|
175
|
+
- **Between unlayered sheets, the last adopted wins at equal specificity**, and a
|
|
176
|
+
sheet is adopted when its module first assigns it — for navi's components, on
|
|
177
|
+
their first render. Which of the entry and a chunk, or of two chunks, comes last
|
|
178
|
+
is therefore nothing the build fixes: a rule that depends on it is a rule to
|
|
179
|
+
make more specific.
|
|
192
180
|
|
|
193
181
|
## When the code does not come
|
|
194
182
|
|
|
195
|
-
Someone has the app open, a deploy moves the chunks, and the next `import()`
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
183
|
+
Someone has the app open, a deploy moves the chunks, and the next `import()` is a
|
|
184
|
+
404; or the network is gone. The browser says both the same way — a `TypeError`
|
|
185
|
+
with no status — and neither is a bug of the page: there is nothing in the app's
|
|
186
|
+
code to point at.
|
|
199
187
|
|
|
200
188
|
**Asking again never fetches.** The document remembers a module whose fetch
|
|
201
189
|
failed as failed: a second `import()` of the same address is refused without a
|
|
202
|
-
request, for the life of the document. So a retry
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
in words the reader understands.
|
|
206
|
-
reloading itself is a loop waiting to happen.
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
`error: true` — reaches it like a failed read does; a boundary that shows only
|
|
213
|
-
what came from the network and rethrows the rest as a bug recognises an import
|
|
214
|
-
by its action rather than by the browser's message. `error.action` is the
|
|
190
|
+
request, for the life of the document. So a retry has nothing to do —
|
|
191
|
+
`error.action.rerun()` fails again at once, on the same failure — and the one way
|
|
192
|
+
out is a fresh document: **`window.location.reload()`**, which the screen offers
|
|
193
|
+
in words the reader understands. navi does not reload on its own: a document
|
|
194
|
+
reloading itself is a loop waiting to happen.
|
|
195
|
+
|
|
196
|
+
The page or the component reading its code says it where it stands, as above. A
|
|
197
|
+
failure left to a boundary reaches it like a failed read does; a boundary that
|
|
198
|
+
shows only what came from the network and rethrows the rest as a bug recognises
|
|
199
|
+
an import by its action, not by the browser's message. `error.action` is the
|
|
215
200
|
instance that ran, not the route action itself (a route action hands out the
|
|
216
201
|
instance for its params), and every instance bound from one action carries its
|
|
217
202
|
`callback`: `error.action.callback === GAME_PAGE_CODE.callback`.
|
|
218
203
|
|
|
219
204
|
## Preact's `lazy()` is not this
|
|
220
205
|
|
|
221
|
-
`preact/compat`
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
suspension it cannot attribute to an action. Read the import through an
|
|
226
|
-
action.
|
|
206
|
+
`preact/compat`'s `lazy()` suspends without telling `<Loading>` what it waits
|
|
207
|
+
for, so the boundary has no action to follow — on a fresh one, it draws nothing
|
|
208
|
+
while the module loads — and a failure is not an action's, so no `rerun` brings
|
|
209
|
+
the page back. Read the import through an action.
|
|
227
210
|
|
|
228
211
|
## The build
|
|
229
212
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
213
|
+
`import()` becomes a chunk of its own under `js/`, versioned with the rest —
|
|
214
|
+
unless the module is also imported statically, which keeps it where the static
|
|
215
|
+
import puts it; what it imports from `node_modules` goes to the shared vendor
|
|
216
|
+
chunk. A target without dynamic import gets the fallback.
|
|
233
217
|
|
|
234
218
|
## Reference
|
|
235
219
|
|
|
236
|
-
- `src/nav/demos/code_splitting/code_splitting_demo.html` — two pages whose
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
played by hand.
|
|
220
|
+
- `src/nav/demos/code_splitting/code_splitting_demo.html` — two pages whose code
|
|
221
|
+
is a route action, one prefetched on hover and one opted out, and a component
|
|
222
|
+
inside a page that stays, drawing its own wait and failure; every import is
|
|
223
|
+
drawn on the backend's frontier so a wait or a failure can be played by hand.
|
|
241
224
|
- `src/nav/route_action.js`, `src/nav/use_preload_on_intent.js`
|