@jsenv/navi 0.29.488 → 0.29.490
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/dev/jsenv_navi.js +5083 -1291
- package/dist/dev/jsenv_navi.js.map +118 -83
- package/dist/dev/jsenv_navi_side_effects.js +1 -0
- package/dist/dev/jsenv_navi_side_effects.js.map +2 -2
- package/dist/jsenv_navi.js +4985 -1289
- package/dist/jsenv_navi.js.map +118 -83
- package/dist/jsenv_navi_side_effects.js +1 -0
- package/dist/jsenv_navi_side_effects.js.map +2 -2
- package/docs/AI_INSTRUCTIONS.md +7 -5
- package/docs/actions.md +17 -0
- package/docs/control_value.md +50 -3
- package/docs/create_and_edit.md +29 -16
- package/docs/css_architecture.md +29 -1
- package/docs/drag_interactions.md +90 -11
- package/docs/error_handling.md +9 -2
- package/docs/form_changed.md +20 -5
- package/docs/group.md +5 -1
- package/docs/i18n.md +28 -13
- package/docs/interactions.md +38 -6
- package/docs/list_refresh.md +4 -1
- package/docs/navigation.md +28 -0
- package/docs/network_policy.md +14 -3
- package/docs/popup_lift.md +15 -3
- package/docs/popup_open.md +117 -13
- package/docs/resource.md +121 -20
- package/docs/safe_area.md +14 -3
- package/docs/scroll.md +5 -1
- package/docs/testid.md +7 -4
- package/package.json +3 -3
package/docs/resource.md
CHANGED
|
@@ -66,6 +66,25 @@ it yourself is the point:
|
|
|
66
66
|
(see [When the backend answers a sub-route with the whole
|
|
67
67
|
parent](#when-the-backend-answers-a-sub-route-with-the-whole-parent)).
|
|
68
68
|
|
|
69
|
+
A pushed message is an answer — JSON, every object in it new — and enters
|
|
70
|
+
through `store.upsertAnswer(row)`, which compares it in depth to the row held:
|
|
71
|
+
the readers of a row that did not move stay still. `store.upsert()` compares
|
|
72
|
+
each field by reference, so a pushed row equal to the one held re-renders its
|
|
73
|
+
readers as soon as it carries a nested object. The store is not a list: a row
|
|
74
|
+
it did not hold joins no `GET_MANY` and no `GET_RANGE` composition. Read again
|
|
75
|
+
what it belongs to, as a `POST` of the resource would:
|
|
76
|
+
|
|
77
|
+
```js
|
|
78
|
+
socket.addEventListener("message", (message) => {
|
|
79
|
+
const run = JSON.parse(message.data);
|
|
80
|
+
const known = RUN.store.select(run.id);
|
|
81
|
+
RUN.store.upsertAnswer(run);
|
|
82
|
+
if (!known) {
|
|
83
|
+
RUN.GET_RANGE.invalidate(); // or RUN.GET_MANY.rerun()
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
```
|
|
87
|
+
|
|
69
88
|
A list that loads its rows a slice at a time is **not** one of those cases —
|
|
70
89
|
that is `GET_RANGE`, below.
|
|
71
90
|
|
|
@@ -129,6 +148,14 @@ What stays the app's, because only the app knows:
|
|
|
129
148
|
the drawn row for the whole request. It waits on `data` being set — the
|
|
130
149
|
emptiness test of data_states.md — and the refresh is shown as usual.
|
|
131
150
|
|
|
151
|
+
**An identity change is a fresh document.** Sign-out, switching to another
|
|
152
|
+
account, taking an invited account, leaving a "view as": write `undefined` into
|
|
153
|
+
the kept signals (`persist`, `keepPageOnScreen`), then
|
|
154
|
+
`window.location.replace(…)`. navi's `reload()` reruns the route actions only;
|
|
155
|
+
the stores, the component reads and the lists' compositions stay, and they are
|
|
156
|
+
the previous person's. A list of things to reset one by one is a list nobody
|
|
157
|
+
can tell is complete.
|
|
158
|
+
|
|
132
159
|
What the copy is not:
|
|
133
160
|
|
|
134
161
|
- **an answer.** The `GET` goes out every time; a screen must not read `data` as
|
|
@@ -170,11 +197,17 @@ keepPageOnScreen({
|
|
|
170
197
|
screens: `GAME.GET` for any game opened, `USER.GET_MANY` for a tab and for
|
|
171
198
|
every search. Kept per resource, that is every game ever opened. The page's
|
|
172
199
|
reads are what navi already runs for it: the route actions asking something for
|
|
173
|
-
the page (`activeRouteActionsSignal
|
|
174
|
-
without the page's code, what the app reruns when it comes back to the
|
|
175
|
-
foreground), and the compositions the `<List.Items>`
|
|
200
|
+
the page (`activeRouteActionsSignal`), and the compositions the `<List.Items>`
|
|
176
201
|
on screen read. A navigation replaces the slot with the next page's reads.
|
|
177
202
|
|
|
203
|
+
The same reads are what the app reads again when it comes back to the
|
|
204
|
+
foreground: `rerunActions(activeRouteReadsSignal.peek())`. That set is the
|
|
205
|
+
route actions without the page's code, plus the `GET_RANGE` readers of the
|
|
206
|
+
lists on screen; a reader handed to `rerunActions` revalidates — its lists ask
|
|
207
|
+
again for the window they draw, their rows kept, and every composition stays
|
|
208
|
+
(`reader.revalidate()`, where `invalidate()` would send the lists off screen
|
|
209
|
+
back to skeletons).
|
|
210
|
+
|
|
178
211
|
What is kept, for the next document's first run of the same read with the same
|
|
179
212
|
params:
|
|
180
213
|
|
|
@@ -481,6 +514,37 @@ for its verb. A dependent names it like a callback —
|
|
|
481
514
|
`{ resource: ORDER, on: ["cancel"] }` reruns after a cancel, not after an
|
|
482
515
|
archive — and a resource listed bare counts it with every other write.
|
|
483
516
|
|
|
517
|
+
A scoped relation takes `writes` as well. A sub-collection with several
|
|
518
|
+
writes — seat a player, move one, take one out — is a `.scopedMany()` naming
|
|
519
|
+
each of them, never `op`s dispatched inside the owner's `PATCH`. A
|
|
520
|
+
`.scopedMany()` write may also take a plural verb, for a route answering the
|
|
521
|
+
whole owner:
|
|
522
|
+
|
|
523
|
+
```js
|
|
524
|
+
const GAME_SEATS = GAME.scopedMany("participants", {
|
|
525
|
+
idKey: "slot",
|
|
526
|
+
writes: {
|
|
527
|
+
add: {
|
|
528
|
+
verb: "POST_MANY",
|
|
529
|
+
callback: async ({ id, ...body }) => {
|
|
530
|
+
const game = await post(`/games/${id}/participants`, body);
|
|
531
|
+
return [game.id, game.participants];
|
|
532
|
+
},
|
|
533
|
+
},
|
|
534
|
+
move: { verb: "PATCH_MANY", callback: moveParticipant },
|
|
535
|
+
remove: { verb: "DELETE", callback: removeParticipant },
|
|
536
|
+
},
|
|
537
|
+
});
|
|
538
|
+
const ALERT = resource("alert", {
|
|
539
|
+
dependencies: [{ resource: GAME_SEATS, on: ["add", "remove"] }],
|
|
540
|
+
GET,
|
|
541
|
+
});
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
Each answers in the relation's shape for its verb — `[ownerId, items]` for a
|
|
545
|
+
plural one, which replaces the collection and reruns nothing on the owner
|
|
546
|
+
([Relations and autorerun](#relations-and-autorerun)).
|
|
547
|
+
|
|
484
548
|
## Callback return contracts
|
|
485
549
|
|
|
486
550
|
These are not guessable — each relationship method has its own shape, written
|
|
@@ -492,9 +556,15 @@ out with an example in its JSDoc:
|
|
|
492
556
|
`GET`/`POST` that embeds the child inline works too.
|
|
493
557
|
- **`.many(propertyName, CHILD, callbacks)`** — `GET_MANY` returns the
|
|
494
558
|
**parent** with the array nested inside; `GET`/`POST`/`PUT`/`PATCH` the
|
|
495
|
-
**child**, upserted into the child store but not joining the parent's array
|
|
496
|
-
|
|
497
|
-
|
|
559
|
+
**child**, upserted into the child store but not joining the parent's array;
|
|
560
|
+
`POST` `[parentId, child]` upserts the child **and** appends it to the
|
|
561
|
+
parent's array (putting someone in a group); `POST_MANY`/`PUT_MANY`/`PATCH_MANY`
|
|
562
|
+
an array of children, upserted only, or `[parentId, [child, …]]`, which also
|
|
563
|
+
replaces the parent's array; `DELETE` `[parentId, childId]`, `DELETE_MANY`
|
|
564
|
+
`[parentId, [childId, …]]`. A `POST` never renames, so its array is always the
|
|
565
|
+
join; a child is never an array, so a plural answer is never ambiguous. A
|
|
566
|
+
parent the store does not hold yet is left alone: its array comes with its
|
|
567
|
+
next read.
|
|
498
568
|
- **`.scopedOne(propertyName, callbacks)`** — every callback returns
|
|
499
569
|
`[ownerId, props | null]`; the property is `null` until one does.
|
|
500
570
|
- **`.scopedMany(propertyName, { idKey, … })`** — `[ownerId, props]`; `PUT` (an
|
|
@@ -540,10 +610,12 @@ const GAME_CANDIDATES = GAME.scopedMany("candidates", {
|
|
|
540
610
|
```
|
|
541
611
|
|
|
542
612
|
For `.many()`, the equivalent is its `GET_MANY` callback, which already takes
|
|
543
|
-
the parent object with the array nested inside — return the response untouched
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
613
|
+
the parent object with the array nested inside — return the response untouched
|
|
614
|
+
— and, for a write, a plural callback returning `[parentId, children]` (a
|
|
615
|
+
favourite toggled, answered with the whole list of favourites). Deletion is the
|
|
616
|
+
exception: `DELETE`/`DELETE_MANY` drop by id rather than replace, so return
|
|
617
|
+
`[ownerId, deletedId]` from the id you already have in the params, and ignore
|
|
618
|
+
the full parent the backend sent back.
|
|
547
619
|
|
|
548
620
|
The parent's own fields in such a response (a `status`, a `score`) are applied
|
|
549
621
|
only where the callback returns the parent: `.one()`'s `GET`/`PUT` and
|
|
@@ -557,17 +629,21 @@ explicitly.
|
|
|
557
629
|
Relationship mutations do **not** invalidate their parent by default. The rules:
|
|
558
630
|
|
|
559
631
|
- `.scopedMany` child **POST** reruns the owner's singular `GET`, whether it
|
|
560
|
-
goes through the child
|
|
561
|
-
the last GET response actually embedded that
|
|
562
|
-
is never rerun by a child POST (a list of
|
|
563
|
-
them gained a child).
|
|
564
|
-
- `.scopedMany` child **PUT / PATCH / DELETE
|
|
565
|
-
|
|
632
|
+
goes through the child, a `withParams()` scope of it or a write named with
|
|
633
|
+
that verb — but only when the last GET response actually embedded that
|
|
634
|
+
property. GET_MANY on the parent is never rerun by a child POST (a list of
|
|
635
|
+
parents is not stale because one of them gained a child).
|
|
636
|
+
- `.scopedMany` child **PUT / PATCH / DELETE**, and every **`*_MANY`** write
|
|
637
|
+
(`POST_MANY` included), rerun nothing on the owner: the callback result
|
|
638
|
+
already carries the updated child, or the whole collection. Its own reads
|
|
639
|
+
still follow its `rerunOn` (`POST` covers `POST_MANY` there).
|
|
566
640
|
- `.scopedOne` mutations rerun nothing, ever — the result is the new value.
|
|
567
641
|
- `.one` / `.many` children live in an independent store; mutating them never
|
|
568
|
-
reruns the parent.
|
|
569
|
-
[
|
|
570
|
-
|
|
642
|
+
reruns the parent. A `.many()` write changes the parent's array only through
|
|
643
|
+
the answers naming it (`[parentId, …]`, see
|
|
644
|
+
[Callback return contracts](#callback-return-contracts)); otherwise declare
|
|
645
|
+
it with [`dependencies`](#dependencies-rerun-after-another-resource-writes)
|
|
646
|
+
if you need it.
|
|
571
647
|
- Within a relationship resource, the usual defaults still apply: its own
|
|
572
648
|
`GET_MANY` reruns after its own `POST`. A `.one`/`.many` `GET` is reset (not
|
|
573
649
|
rerun) by its own `DELETE`, as a root `GET` is; a scoped `GET` is not reset —
|
|
@@ -645,7 +721,7 @@ const ROLE_WITH_OWNERS = ROLE.withParams(
|
|
|
645
721
|
|
|
646
722
|
Any write on a listed resource (`POST`, `PUT`, `PATCH`, `DELETE`) reruns the
|
|
647
723
|
completed `GET` and `GET_MANY` of the resource that declared it, and invalidates
|
|
648
|
-
its `GET_RANGE` readers
|
|
724
|
+
its `GET_RANGE` readers: its own `rerunOn` is about its own writes. A
|
|
649
725
|
dependency is a facade: `DATABASE` and `DATABASE.withParams(…)` are two of them,
|
|
650
726
|
and only writes on the one listed count. Nothing goes the other way, and nothing
|
|
651
727
|
is inferred — a relation that really is a sub-route of the parent is modelled
|
|
@@ -674,6 +750,31 @@ write, or that the resource does not define, throws at declaration — a
|
|
|
674
750
|
dependency that never fires leaves a count on its old value with nothing to say
|
|
675
751
|
so.
|
|
676
752
|
|
|
753
|
+
A resource can only list one declared before it: a `const` further down the
|
|
754
|
+
file is not there yet. When two resources each change what the other shows —
|
|
755
|
+
the members of a group are users, and starring a user writes into a group — or
|
|
756
|
+
when a file's order would decide it, give `dependencies` as a function:
|
|
757
|
+
|
|
758
|
+
```js
|
|
759
|
+
const USER = resource("user", {
|
|
760
|
+
dependencies: () => [
|
|
761
|
+
{ resource: PLAYER_GROUP, on: ["addMember", "removeMember"] },
|
|
762
|
+
],
|
|
763
|
+
GET,
|
|
764
|
+
});
|
|
765
|
+
const PLAYER_GROUP = resource("player_group", {
|
|
766
|
+
dependencies: [USER],
|
|
767
|
+
GET,
|
|
768
|
+
writes: { addMember, removeMember },
|
|
769
|
+
});
|
|
770
|
+
```
|
|
771
|
+
|
|
772
|
+
It is called once, before the first request of any resource leaves — every
|
|
773
|
+
module has been evaluated by then — and what it returns is checked as a list
|
|
774
|
+
written in place is; what fails throws into that first run. A cycle is fine: a
|
|
775
|
+
rerun is one hop, and it reruns reads only. `createAction`'s `dependencies` take
|
|
776
|
+
the same function form.
|
|
777
|
+
|
|
677
778
|
## A function calling the verb, or the instance
|
|
678
779
|
|
|
679
780
|
Both forms are real, and what separates them is narrower than it looks:
|
package/docs/safe_area.md
CHANGED
|
@@ -125,13 +125,16 @@ Beware of making that container a scroller by accident — see
|
|
|
125
125
|
|
|
126
126
|
An app whose document scrolls still has screens that must fit the screen: a
|
|
127
127
|
head that stays put over a part that scrolls, a wizard whose step fills what is
|
|
128
|
-
left. The height of the free band is the visual viewport less both insets
|
|
128
|
+
left. The height of the free band is the visual viewport less both insets, less
|
|
129
|
+
the strips that stick under a bar in the flow:
|
|
129
130
|
|
|
130
131
|
```css
|
|
131
132
|
.wizard {
|
|
132
133
|
height: calc(
|
|
133
134
|
var(--navi-vvh) - var(--navi-safe-area-inset-top) -
|
|
134
|
-
var(--navi-safe-area-inset-bottom)
|
|
135
|
+
var(--navi-safe-area-inset-bottom) -
|
|
136
|
+
var(--navi-transition-cover-top, 0px) -
|
|
137
|
+
var(--navi-transition-cover-bottom, 0px)
|
|
135
138
|
);
|
|
136
139
|
}
|
|
137
140
|
```
|
|
@@ -143,6 +146,13 @@ the app's own bands and an overlaying keyboard (see
|
|
|
143
146
|
slot is how a `FixedBar` writes what it takes, and reading it for layout misses
|
|
144
147
|
the notch on an edge with no bar, the band of a capped app, and the keyboard.
|
|
145
148
|
|
|
149
|
+
The covers are the strips of [the next section](#a-strip-under-a-bar): an
|
|
150
|
+
offline line above the page pushes it down by its height, and a page as tall as
|
|
151
|
+
the band would end that much under the bottom bar. `useTransitionCover`
|
|
152
|
+
publishes the tallest strip on each edge, in the frame the strip changes, and
|
|
153
|
+
removes it when the strip goes; the `0px` is for an app that has none, where the
|
|
154
|
+
variable does not exist.
|
|
155
|
+
|
|
146
156
|
### A strip under a bar
|
|
147
157
|
|
|
148
158
|
A strip that stays under the top bar — a status banner, an "offline" line — is
|
|
@@ -162,7 +172,8 @@ useTransitionCover(bannerRef);
|
|
|
162
172
|
```
|
|
163
173
|
|
|
164
174
|
`useTransitionCover` publishes the strip's height, so the pictures of a route
|
|
165
|
-
movement are cut under it rather than sliding over it
|
|
175
|
+
movement are cut under it rather than sliding over it, and a page fitting the
|
|
176
|
+
free band leaves room for it ([above](#a-page-that-fits-the-free-band)). An anchor's
|
|
166
177
|
`scroll-margin` adds only what `:root`'s `scroll-padding` does not hold already
|
|
167
178
|
— the strip's own height, a few pixels of air — never the bar or the band.
|
|
168
179
|
|
package/docs/scroll.md
CHANGED
|
@@ -128,7 +128,11 @@ followed — a place read from an answer that refreshes (yesterday's "today", th
|
|
|
128
128
|
today's) — and it is compared by value, so it can be computed at every render. A list with an `id` also **comes back where it was** when its screen is
|
|
129
129
|
left and come back to, or the page reloaded, the way the page does (its `id`
|
|
130
130
|
must name one list of the app, see [below](#a-list-that-comes-back-one-id-one-list));
|
|
131
|
-
**`scrollResetOnNavigation`** opts out.
|
|
131
|
+
**`scrollResetOnNavigation`** opts out. A push to its screen is an arrival and
|
|
132
|
+
forgets it, even when the address names the item one was reading: a link that is
|
|
133
|
+
a return by meaning says `asReturn`, and the list comes back where that address
|
|
134
|
+
was left (see
|
|
135
|
+
[navigation.md](./navigation.md#where-a-navigation-lands-the-scroll)). What it draws there before its source
|
|
132
136
|
answers is another matter
|
|
133
137
|
([What a run knows when it opens](#what-a-run-knows-when-it-opens)). A list scrolling the document that
|
|
134
138
|
opens on an item places the document itself: the offset the page kept for its url
|
package/docs/testid.md
CHANGED
|
@@ -154,10 +154,13 @@ error message, a section, a row — put the testid on the surrounding component
|
|
|
154
154
|
built on `Box` forwards it to its own element.
|
|
155
155
|
|
|
156
156
|
A selectable `List.Item` is a control too, and the rule holds there: its host is
|
|
157
|
-
the real checkbox or radio the row selects with, an input navi draws invisible
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
157
|
+
the real checkbox or radio the row selects with, an input navi draws invisible,
|
|
158
|
+
as big as the row, under the row's content — content that lets the pointer
|
|
159
|
+
through to it (`selectableArea="all"`, the default). A `data-testid` on the item
|
|
160
|
+
names that input: it is what a press on the row reaches and what `toBeChecked()`
|
|
161
|
+
reads. A test that presses the row, or waits for it, targets the item's testid.
|
|
162
|
+
A testid on the content is for reading what the row says: Playwright refuses a
|
|
163
|
+
click on it, since the input is what the pointer meets there.
|
|
161
164
|
|
|
162
165
|
What navi builds itself is named through props. A `data-testid` on a `<Picker>`
|
|
163
166
|
names the trigger; `popupTestId` names the popup it builds (the callout, under
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsenv/navi",
|
|
3
|
-
"version": "0.29.
|
|
3
|
+
"version": "0.29.490",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Library of components including navigation to create frontend applications",
|
|
6
6
|
"repository": {
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
"preact": "^11.0.0-rc.2"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@jsenv/dom": "0.17.
|
|
38
|
-
"@jsenv/humanize": "1.8.
|
|
37
|
+
"@jsenv/dom": "0.17.98",
|
|
38
|
+
"@jsenv/humanize": "1.8.11",
|
|
39
39
|
"@jsenv/validity": "0.4.7"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|