@jsenv/navi 0.29.43 → 0.29.44
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 +218 -46
- package/dist/jsenv_navi.js.map +21 -13
- package/docs/AI_INSTRUCTIONS.md +9 -2
- package/docs/control_group.md +5 -0
- package/docs/control_object.md +133 -0
- package/docs/control_value.md +19 -0
- package/docs/form_changed.md +2 -0
- package/docs/list_refresh.md +61 -12
- package/package.json +1 -1
package/docs/AI_INSTRUCTIONS.md
CHANGED
|
@@ -69,8 +69,10 @@ consistency across the app, not from any single call site.
|
|
|
69
69
|
`docs/resource_with_params.md`, `docs/resource_dependencies.md`.
|
|
70
70
|
- `docs/list_refresh.md` — what a write sends back to the network and what stays
|
|
71
71
|
on screen meanwhile: stale data returned by `useAsyncData({ loading: true })`,
|
|
72
|
-
what updates from a response without any request, `rerunOn` and its defaults
|
|
73
|
-
|
|
72
|
+
what updates from a response without any request, `rerunOn` and its defaults,
|
|
73
|
+
and how a paginated `<List.Items>` re-reads its slices without disappearing.
|
|
74
|
+
Read it before adding verbs to `rerunOn`, hiding a list on `loading`, or
|
|
75
|
+
remounting a list with a `key` to refresh it.
|
|
74
76
|
- `docs/css_architecture.md` — how Navi's CSS layering works, and the
|
|
75
77
|
supported ways to override component styles (props > CSS variables > direct
|
|
76
78
|
rule overrides, in that preference order).
|
|
@@ -90,6 +92,11 @@ consistency across the app, not from any single call site.
|
|
|
90
92
|
`signal` carrying something is an answer). Read it before a screen that
|
|
91
93
|
modifies an existing resource (`pristineKey`), and before reaching for
|
|
92
94
|
`canSendWhileUnchanged` because a submit "does nothing".
|
|
95
|
+
- `docs/control_object.md` — a value made of several controls: `<ControlGroup>`
|
|
96
|
+
(the shape) vs `<Form>` (the shape plus a send), what a group's `name` does
|
|
97
|
+
and what a nameless one merges, and what a `<Picker type="object">` needs in
|
|
98
|
+
its popup (one group, not two controls). Read it before making one value out
|
|
99
|
+
of several controls, and before putting anything in a picker popup.
|
|
93
100
|
- `docs/control_group.md` — `<Group>`: several controls reading as one framed
|
|
94
101
|
object (one border per seam, radius on the outer corners only). Read it
|
|
95
102
|
before placing bordered controls against each other, and before writing
|
package/docs/control_group.md
CHANGED
|
@@ -130,6 +130,11 @@ The controls shipped by navi all follow this: `Button`, `Input`, `Select`,
|
|
|
130
130
|
- Controls separated by space, each with its own frame — that is a `Box` with
|
|
131
131
|
`spacing`, they were never one object.
|
|
132
132
|
- A label and its control — that is `Field`.
|
|
133
|
+
- Several controls making ONE value between them (an address out of three
|
|
134
|
+
fields, a day and two wheels out of one moment) — that is `ControlGroup`,
|
|
135
|
+
which is about the value and draws nothing. Same word, other subject: see
|
|
136
|
+
[control_object.md](./control_object.md). Both at once is fine — a `Group`
|
|
137
|
+
around the members of a `ControlGroup`.
|
|
133
138
|
- Radio buttons or checkboxes sharing a name and a validation — that is
|
|
134
139
|
`RadioGroup` / `CheckboxGroup`, which is about the value, not the frame. They
|
|
135
140
|
can be put inside a `Group` if you also want them to share a frame.
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# A value made of several controls
|
|
2
|
+
|
|
3
|
+
One question, answered by more than one control: a day and two wheels that make
|
|
4
|
+
"mardi 19h", three fields that make an address, two wheels that add up to a
|
|
5
|
+
number of minutes. Navi has one mechanism for that — a group aggregates what its
|
|
6
|
+
children hold into a single value, and hands it back the same way — and three
|
|
7
|
+
places it shows up. This file says which one to reach for, and what a picker
|
|
8
|
+
whose value is an object needs in its popup.
|
|
9
|
+
|
|
10
|
+
- [`<ControlGroup>`: the shape](#controlgroup-the-shape)
|
|
11
|
+
- [`<Form>`: the shape, plus a send](#form-the-shape-plus-a-send)
|
|
12
|
+
- [Naming, and what a nameless group does](#naming-and-what-a-nameless-group-does)
|
|
13
|
+
- [A picker whose value is an object](#a-picker-whose-value-is-an-object)
|
|
14
|
+
- [`Group` is not `ControlGroup`](#group-is-not-controlgroup)
|
|
15
|
+
|
|
16
|
+
## `<ControlGroup>`: the shape
|
|
17
|
+
|
|
18
|
+
A group with no opinion beyond the shape of its value: it aggregates its named
|
|
19
|
+
children into an object, distributes an object back down to them, and carries
|
|
20
|
+
its own `action`/`uiAction`/`command` if you want one.
|
|
21
|
+
|
|
22
|
+
```jsx
|
|
23
|
+
<ControlGroup name="address">
|
|
24
|
+
<Input name="street" />
|
|
25
|
+
<Input name="city" />
|
|
26
|
+
<Input name="zip" />
|
|
27
|
+
</ControlGroup>
|
|
28
|
+
// worth { street, city, zip }
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
It is also the brick composite controls are built from:
|
|
32
|
+
`aggregateChildStates` / `distributeChildUIState` replace "one key per child"
|
|
33
|
+
with a value of your own — two wheels that are one number of minutes, three
|
|
34
|
+
fields that are one date. A group with them takes and hands back a single
|
|
35
|
+
value, so it can be driven by one `value`/`signal` like any other control. It is
|
|
36
|
+
what `SpinTime` is made of, and the same mechanism `InputDuration`, `SpinGroup`
|
|
37
|
+
and `WheelGroup` use for their own members.
|
|
38
|
+
|
|
39
|
+
## `<Form>`: the shape, plus a send
|
|
40
|
+
|
|
41
|
+
A `<Form>` aggregates exactly the same way. What it adds is everything about
|
|
42
|
+
**sending**: the reference it measures against, the refusal to act when nothing
|
|
43
|
+
changed (see [form_changed.md](./form_changed.md)), the submit button and its
|
|
44
|
+
`readOnlyWhileFormUnchanged`, what follows a successful send (`command`), and a
|
|
45
|
+
`<form>` element with the browser's own submit/reset.
|
|
46
|
+
|
|
47
|
+
So the choice is not about the value, it is about whether this cluster is a
|
|
48
|
+
**question with a send**:
|
|
49
|
+
|
|
50
|
+
- a shape inside a bigger whole → `ControlGroup`;
|
|
51
|
+
- something the user sends → `Form`.
|
|
52
|
+
|
|
53
|
+
A `<Form>` inside a `<Form>` is legal — the inner one becomes a group without
|
|
54
|
+
the `<form>` element — but it carries all of the above with it: its own
|
|
55
|
+
reference, its own "nothing changed", its own submit story. Grouping three
|
|
56
|
+
fields into a sub-object should cost none of that.
|
|
57
|
+
|
|
58
|
+
## Naming, and what a nameless group does
|
|
59
|
+
|
|
60
|
+
A group's `name` is the key its value lands under, in the group above it. This
|
|
61
|
+
is true of a `ControlGroup` and of a nested `Form` alike.
|
|
62
|
+
|
|
63
|
+
Left nameless, a group is a **grouping**: it exists to hold its children
|
|
64
|
+
together (shared navigation, a visual cluster) without claiming a key, and what
|
|
65
|
+
it holds is merged into the object around it as if its children had been written
|
|
66
|
+
there.
|
|
67
|
+
|
|
68
|
+
```jsx
|
|
69
|
+
<ControlGroup name="when">
|
|
70
|
+
<DaySpin name="day" />
|
|
71
|
+
<WheelGroup>
|
|
72
|
+
<Wheel name="hours" />
|
|
73
|
+
<Wheel name="minutes" />
|
|
74
|
+
</WheelGroup>
|
|
75
|
+
</ControlGroup>
|
|
76
|
+
// worth { day, hours, minutes } — the WheelGroup adds no key of its own
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
A nameless **leaf** is a different story: it is a control whose value has
|
|
80
|
+
nowhere to go, and it is warned about. When that is deliberate — a control that
|
|
81
|
+
only opens something — say so with `allowNameless` (see
|
|
82
|
+
[form_changed.md](./form_changed.md#a-control-that-is-not-a-field)).
|
|
83
|
+
|
|
84
|
+
## A picker whose value is an object
|
|
85
|
+
|
|
86
|
+
`type="object"` is the picker whose value is what the group in its popup
|
|
87
|
+
aggregates (next to `type="array"`, whose value is a selection):
|
|
88
|
+
|
|
89
|
+
```jsx
|
|
90
|
+
<Picker name="when" type="object" value={{ day, hours, minutes }}>
|
|
91
|
+
<ControlGroup>
|
|
92
|
+
<DaySpin name="day" signal={daySignal} />
|
|
93
|
+
<WheelGroup>
|
|
94
|
+
<Wheel name="hours" signal={hoursSignal} />
|
|
95
|
+
<Wheel name="minutes" signal={minutesSignal} />
|
|
96
|
+
</WheelGroup>
|
|
97
|
+
</ControlGroup>
|
|
98
|
+
</Picker>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Two things to get right:
|
|
102
|
+
|
|
103
|
+
- **One control in the popup, and it must be the group.** A picker syncs with a
|
|
104
|
+
single control: the first one receives the picker's whole value and is the
|
|
105
|
+
only one read back. Two controls side by side in a popup is the shape to
|
|
106
|
+
avoid — the second is neither filled nor collected, and navi says so in dev.
|
|
107
|
+
Wrap them in one `ControlGroup` (or one `Form`, when the popup has a send of
|
|
108
|
+
its own).
|
|
109
|
+
- **The value travels by name.** What the picker was given goes down into the
|
|
110
|
+
group, which hands each named child its own key; a nameless grouping inside
|
|
111
|
+
receives the whole object and picks out what it names. Give a picker of this
|
|
112
|
+
kind a scalar type and there is no group to distribute anything — the whole
|
|
113
|
+
object lands on one control, which is how `"[object Object]"` ends up in a
|
|
114
|
+
url.
|
|
115
|
+
|
|
116
|
+
## `Group` is not `ControlGroup`
|
|
117
|
+
|
|
118
|
+
Two names, two subjects, no relation:
|
|
119
|
+
|
|
120
|
+
- `<Group>` is about the **frame** — several controls reading as one object to
|
|
121
|
+
the eye, one border per seam. See [control_group.md](./control_group.md).
|
|
122
|
+
- `<ControlGroup>` is about the **value** — several controls reading as one
|
|
123
|
+
object to the action.
|
|
124
|
+
|
|
125
|
+
They compose: a `Group` around controls that also happen to be a
|
|
126
|
+
`ControlGroup`'s children is fine, and either can exist without the other.
|
|
127
|
+
|
|
128
|
+
## See also
|
|
129
|
+
|
|
130
|
+
- [form_changed.md](./form_changed.md) — what a form sends, and what it measures
|
|
131
|
+
against
|
|
132
|
+
- [control_value.md](./control_value.md) — who holds a single control's value
|
|
133
|
+
- [control_group.md](./control_group.md) — the visual `<Group>`
|
package/docs/control_value.md
CHANGED
|
@@ -52,6 +52,23 @@ Both halves are worth knowing about, because each replaces a habit:
|
|
|
52
52
|
- the follow replaces the `key` or the `value`/`uiAction` pair used to push an
|
|
53
53
|
outside change into a control.
|
|
54
54
|
|
|
55
|
+
The follow goes all the way up: a bound control that lives inside a group — two
|
|
56
|
+
wheels in a `WheelGroup`, a field in a `ControlGroup` — makes that group
|
|
57
|
+
re-aggregate when its signal is written, and the form above sees the new value.
|
|
58
|
+
A shortcut that pushes the controls from the outside is an answer like any
|
|
59
|
+
other: the wheels roll, and the submit lights up.
|
|
60
|
+
|
|
61
|
+
```jsx
|
|
62
|
+
<Button
|
|
63
|
+
onClick={() => {
|
|
64
|
+
hoursSignal.value = 2;
|
|
65
|
+
minutesSignal.value = 0;
|
|
66
|
+
}}
|
|
67
|
+
>
|
|
68
|
+
2h
|
|
69
|
+
</Button>
|
|
70
|
+
```
|
|
71
|
+
|
|
55
72
|
## `signal` + `defaultValue`: the answer and where it starts
|
|
56
73
|
|
|
57
74
|
They are not competing, they answer two different questions:
|
|
@@ -126,6 +143,8 @@ directions; it just has nothing extra to say.
|
|
|
126
143
|
|
|
127
144
|
- [form_changed.md](./form_changed.md) — what a form makes of each of these:
|
|
128
145
|
which fields it counts as already answered, and when it sends nothing
|
|
146
|
+
- [control_object.md](./control_object.md) — several controls reading as one
|
|
147
|
+
object: which group aggregates them, and how the value travels down by name
|
|
129
148
|
- [control_group.md](./control_group.md) — several controls reading as one
|
|
130
149
|
framed object
|
|
131
150
|
- [actions.md](./actions.md#action-or-uiaction) — `action` or `uiAction`: which
|
package/docs/form_changed.md
CHANGED
|
@@ -124,4 +124,6 @@ It is then neither collected nor complained about.
|
|
|
124
124
|
|
|
125
125
|
- [control_value.md](./control_value.md) — who holds a control's value:
|
|
126
126
|
nothing, a bound `signal`, or you
|
|
127
|
+
- [control_object.md](./control_object.md) — one value made of several
|
|
128
|
+
controls: `ControlGroup`, `Form`, and a picker whose value is an object
|
|
127
129
|
- [actions.md](./actions.md) — what an action does around the send itself
|
package/docs/list_refresh.md
CHANGED
|
@@ -67,9 +67,56 @@ way back is a re-read.
|
|
|
67
67
|
`DELETE` is symmetric: returning the id drops the item from the store, and every
|
|
68
68
|
list containing it drops it too.
|
|
69
69
|
|
|
70
|
+
## A paginated list stays on screen too
|
|
71
|
+
|
|
72
|
+
A `<List.Items>` reading through `GET_RANGE` holds the slices it received —
|
|
73
|
+
places in a collection, not a list of ids — so nothing the store does can fix
|
|
74
|
+
them: a row that changed tab, or one that was deleted, moves every row after it
|
|
75
|
+
one rank up, and only the collection knows who fills the last place.
|
|
76
|
+
|
|
77
|
+
It is told, and it re-reads by itself:
|
|
78
|
+
|
|
79
|
+
```jsx
|
|
80
|
+
<List.Items
|
|
81
|
+
count={count}
|
|
82
|
+
itemsAction={NOTIFICATION.GET_RANGE.bindParams({ scope })}
|
|
83
|
+
renderItem={(item, index, { refreshing }) => …}
|
|
84
|
+
/>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The reader keeps no value, so there is nothing to rerun; what it has is a
|
|
88
|
+
signal, bumped by the verbs `rerunOn.GET_RANGE` lists (`["POST", "DELETE"]` by
|
|
89
|
+
default — `DELETE` is in there precisely because the store cannot fix places).
|
|
90
|
+
A run hearing it asks again **for the window it is drawing**, and keeps drawing
|
|
91
|
+
it meanwhile:
|
|
92
|
+
|
|
93
|
+
| Moment | what the run draws | state |
|
|
94
|
+
| ------------------------------- | -------------------- | ------------ |
|
|
95
|
+
| nothing received yet | skeletons | loading |
|
|
96
|
+
| re-reading after a first answer | the rows from before | `refreshing` |
|
|
97
|
+
| answer received | the new rows | — |
|
|
98
|
+
|
|
99
|
+
The rows, the scroll position and the row being read all stay; the slices
|
|
100
|
+
outside the window are forgotten only once the answer is in, and asked for
|
|
101
|
+
again if the user goes back to them. A re-read that fails leaves the rows from
|
|
102
|
+
before on screen. While it is in flight, the list carries `navi-refreshing` and
|
|
103
|
+
`renderItem` gets `{ refreshing }` — read it as "what you see is from before",
|
|
104
|
+
never as "there is nothing to see".
|
|
105
|
+
|
|
106
|
+
An app that knows a row is on its way out (it is the one deleting it) says so
|
|
107
|
+
itself: it is the one rendering the row, so it draws it loading, muted, or not
|
|
108
|
+
at all. The run is not told about rows, only about the collection.
|
|
109
|
+
|
|
110
|
+
```jsx
|
|
111
|
+
// ✗ remounting the run to refresh it: every row on screen becomes a skeleton
|
|
112
|
+
// again, and the list reopens where it opens, not where it was being read
|
|
113
|
+
<List.Items key={`${scope}:${moved}`} … />
|
|
114
|
+
```
|
|
115
|
+
|
|
70
116
|
## `rerunOn`, verb by verb
|
|
71
117
|
|
|
72
|
-
`rerunOn` says which verbs invalidate this resource's `GET` / `GET_MANY
|
|
118
|
+
`rerunOn` says which verbs invalidate this resource's `GET` / `GET_MANY` /
|
|
119
|
+
`GET_RANGE`:
|
|
73
120
|
|
|
74
121
|
```js
|
|
75
122
|
const GAME_RADAR = resource("game_radar", {
|
|
@@ -78,12 +125,13 @@ const GAME_RADAR = resource("game_radar", {
|
|
|
78
125
|
});
|
|
79
126
|
```
|
|
80
127
|
|
|
81
|
-
Defaults are `{ GET: false, GET_MANY: ["POST"] }`:
|
|
128
|
+
Defaults are `{ GET: false, GET_MANY: ["POST"], GET_RANGE: ["POST", "DELETE"] }`:
|
|
82
129
|
|
|
83
|
-
| Default
|
|
84
|
-
|
|
|
85
|
-
| `GET: false`
|
|
86
|
-
| `GET_MANY: ["POST"]`
|
|
130
|
+
| Default | Why |
|
|
131
|
+
| ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
132
|
+
| `GET: false` | `PUT`/`PATCH` already update the UI through the store; `DELETE` resets the `GET` rather than re-running it, so a deleted item shows nothing instead of a spinner then a 404. Give the deleted case its own UI (an "item not found" panel, a redirect) instead of `GET: ["DELETE"]`. |
|
|
133
|
+
| `GET_MANY: ["POST"]` | Whether a new item belongs in this list depends on filters, pagination, sort — the backend knows, the client does not. `DELETE` is excluded because the store already removes the item from every list. |
|
|
134
|
+
| `GET_RANGE: ["POST", "DELETE"]` | A slice is a range of places: a row leaving the collection shifts every place after it, which the store cannot do. Add the verb that moves an item in or out of the collection — a `PATCH` that archives, one that changes an item's tab. |
|
|
87
135
|
|
|
88
136
|
Adding `PUT`/`PATCH` to `GET_MANY` is the usual over-correction: it costs a
|
|
89
137
|
request and a `loading` pass to obtain something the response already contained.
|
|
@@ -92,12 +140,13 @@ updated item — not client-side refreshing.
|
|
|
92
140
|
|
|
93
141
|
## Decision table
|
|
94
142
|
|
|
95
|
-
| What changed
|
|
96
|
-
|
|
|
97
|
-
| a field of one item
|
|
98
|
-
| membership of the list
|
|
99
|
-
| the ORDER of the list
|
|
100
|
-
| nothing came back (`204`)
|
|
143
|
+
| What changed | Re-read the list? |
|
|
144
|
+
| --------------------------- | ---------------------------------------------------- |
|
|
145
|
+
| a field of one item | no — the write's response is enough |
|
|
146
|
+
| membership of the list | yes (`POST`) — the backend decides who belongs |
|
|
147
|
+
| the ORDER of the list | yes — the store stores, it does not sort (see below) |
|
|
148
|
+
| nothing came back (`204`) | yes — there is nothing to put in the store |
|
|
149
|
+
| a place in a paginated list | yes — a `GET_RANGE` reads places, and places shift |
|
|
101
150
|
|
|
102
151
|
## The store stores, it does not sort
|
|
103
152
|
|