@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.
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/navi",
3
- "version": "0.29.98",
3
+ "version": "0.29.101",
4
4
  "type": "module",
5
5
  "description": "Library of components including navigation to create frontend applications",
6
6
  "repository": {