@jsenv/navi 0.29.98 → 0.29.101
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/dist/jsenv_navi.js +741 -553
- package/dist/jsenv_navi.js.map +7 -7
- package/docs/navigation.md +127 -0
- package/package.json +1 -1
package/docs/navigation.md
CHANGED
|
@@ -139,6 +139,133 @@ export const ADMIN_SETTINGS_ROUTE = route(`/admin/settings/:tab=${tabSignal}`);
|
|
|
139
139
|
So the rule is the one you would want: name a section and it becomes a place;
|
|
140
140
|
leave it unnamed and it stays a setting carried along.
|
|
141
141
|
|
|
142
|
+
### Which values a param accepts
|
|
143
|
+
|
|
144
|
+
A param says which segments it accepts, and a segment it declines is not a
|
|
145
|
+
half-match to be sorted out later — the route simply does not match:
|
|
146
|
+
|
|
147
|
+
```js
|
|
148
|
+
export const GAME_ROUTE = route(`/:gameId=${gameIdSignal}`, {
|
|
149
|
+
params: { gameId: /^W-[A-Z0-9]{8}$/i },
|
|
150
|
+
});
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
A constraint is a regexp, the list of accepted values, or a `(value) => boolean`
|
|
154
|
+
— the list is compared as strings, so it can be the very `oneOf` given to the
|
|
155
|
+
signal bound to that param:
|
|
156
|
+
|
|
157
|
+
```js
|
|
158
|
+
const SECTIONS = ["candidate", "to_come", "done"];
|
|
159
|
+
const sectionSignal = stateSignal("to_come", {
|
|
160
|
+
id: "section",
|
|
161
|
+
oneOf: SECTIONS,
|
|
162
|
+
});
|
|
163
|
+
export const GAMES_SECTION_ROUTE = route(`/games/:section=${sectionSignal}`, {
|
|
164
|
+
params: { section: SECTIONS },
|
|
165
|
+
});
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
This is what makes a param usable at the root, where it would otherwise swallow
|
|
169
|
+
every single-segment address: `/cgu` and `/me` stay other routes' urls,
|
|
170
|
+
`<Route fallback>` is reachable for `/whatever`, and no signal is written for a
|
|
171
|
+
url this route has nothing to do with.
|
|
172
|
+
|
|
173
|
+
A constrained param is also **required** — no segment is not one of the values
|
|
174
|
+
it accepts — so `/:gameId` does not match `/`. The address with no segment is a
|
|
175
|
+
route of its own, which is the shape you want anyway.
|
|
176
|
+
|
|
177
|
+
#### Constrain the shape, never the existence
|
|
178
|
+
|
|
179
|
+
A constraint answers one question: **is this segment addressed to this route?**
|
|
180
|
+
It is decided on the url alone, before anything is written, so it can only be
|
|
181
|
+
about shape — that a segment looks like a game code, not that the game exists.
|
|
182
|
+
|
|
183
|
+
Whether the value is any good is a different question, asked later and answered
|
|
184
|
+
by different things: the signal's own validation (`oneOf`, `autoFix`) and the
|
|
185
|
+
route action's data. That question belongs to a route that **did** match, with a
|
|
186
|
+
page free to repair itself, show a not-found screen, offer a way out:
|
|
187
|
+
|
|
188
|
+
```js
|
|
189
|
+
// ✅ /W-ZZZZZZZZ matches, the action 404s, the page says so
|
|
190
|
+
// ❌ constraining gameId to the codes that exist — matching cannot ask a server,
|
|
191
|
+
// and "no route matched" is a worse answer than "this game is gone"
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
So the signal never takes part in matching. It knows what to make of a value;
|
|
195
|
+
the route decides whether the url is its own.
|
|
196
|
+
|
|
197
|
+
#### Why order stops being load-bearing
|
|
198
|
+
|
|
199
|
+
When several routes match one url and bind the **same signal** on a param of the
|
|
200
|
+
same name, they all write it, in declaration order — the last one wins:
|
|
201
|
+
|
|
202
|
+
```js
|
|
203
|
+
route(`/games/:gameId=${gameIdSignal}`); // declared first
|
|
204
|
+
route(`/:gameId=${gameIdSignal}/:state`); // declared later
|
|
205
|
+
// on /games/W-ABC234PQ the second one matches too and writes "games"
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Constraining `gameId` removes that second match entirely, which is the fix.
|
|
209
|
+
Where a param genuinely cannot be constrained, the routes must not share a
|
|
210
|
+
signal.
|
|
211
|
+
|
|
212
|
+
### An address that only sends elsewhere
|
|
213
|
+
|
|
214
|
+
Some addresses are not pages: the root of an app whose home screen is « my
|
|
215
|
+
games », the old address of a section that moved, the share link of a game
|
|
216
|
+
carrying a segment only WhatsApp cares about. They exist to be resolved, and a
|
|
217
|
+
route says so itself:
|
|
218
|
+
|
|
219
|
+
```js
|
|
220
|
+
export const HOME_ROUTE = route("/", { redirectRoute: MY_GAMES_ROUTE });
|
|
221
|
+
export const GAME_SHARED_ROUTE = route("/:gameId/:shareState", {
|
|
222
|
+
redirectRoute: GAME_ROUTE,
|
|
223
|
+
});
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
The params found in the url carry over to the ones the target route declares
|
|
227
|
+
under the same name — `gameId` above needs no help — and what it cannot place
|
|
228
|
+
is left behind, `shareState` included. `redirectRouteParams` says the rest:
|
|
229
|
+
|
|
230
|
+
```js
|
|
231
|
+
// renaming, when the two routes do not call it the same thing
|
|
232
|
+
route("/partie/:id", {
|
|
233
|
+
redirectRoute: GAME_ROUTE,
|
|
234
|
+
redirectRouteParams: ({ id }) => ({ gameId: id }),
|
|
235
|
+
});
|
|
236
|
+
// dropping one, keeping the others
|
|
237
|
+
route("/:gameId/invite", {
|
|
238
|
+
redirectRoute: MY_GAMES_ROUTE,
|
|
239
|
+
redirectRouteParams: { gameId: undefined },
|
|
240
|
+
});
|
|
241
|
+
// carrying nothing over
|
|
242
|
+
route("/tri", { redirectRoute: MY_GAMES_ROUTE, redirectRouteParams: null });
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
#### Why it is not a page rendering `null`
|
|
246
|
+
|
|
247
|
+
The redirection is resolved at the door of the navigation, before the url is
|
|
248
|
+
written anywhere. Nothing about that address ever happens: no history entry, no
|
|
249
|
+
route matching, no route action loading data for a screen nobody will see, no
|
|
250
|
+
element mounted, nothing painted — and going back lands on the page before it
|
|
251
|
+
rather than replaying the redirection forever.
|
|
252
|
+
|
|
253
|
+
A page doing it in an effect gets none of that. It has to be routed to first,
|
|
254
|
+
which means the address exists, its action runs, and the app is on a screen
|
|
255
|
+
nobody should see for one paint — one a route transition can even animate _to_.
|
|
256
|
+
Anything reached by rendering is already too late, so a redirection is declared
|
|
257
|
+
with the address and never appears in the `<Route>` tree at all.
|
|
258
|
+
|
|
259
|
+
It fires on the route's own address only. `/` catches everything below it when
|
|
260
|
+
it renders a container, and would carry `/cgu` away with it if redirecting
|
|
261
|
+
followed the same reading — so redirecting asks the stricter question: is this
|
|
262
|
+
url exactly that route's address?
|
|
263
|
+
|
|
264
|
+
Where several redirecting routes answer for one url, the more specific wins —
|
|
265
|
+
`/:gameId/invite` over `/:gameId/:shareState`, the same reading the rest of the
|
|
266
|
+
router uses. Chains collapse into one navigation, and a cycle throws naming the
|
|
267
|
+
addresses it goes through.
|
|
268
|
+
|
|
142
269
|
### Search params
|
|
143
270
|
|
|
144
271
|
A param that qualifies a page rather than naming it — a zoom level, a sort, a
|