@jsenv/navi 0.29.43 → 0.29.45
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 +1021 -761
- package/dist/jsenv_navi.js.map +40 -27
- package/dist/jsenv_navi_side_effects.js +86 -1
- package/dist/jsenv_navi_side_effects.js.map +3 -3
- package/docs/AI_INSTRUCTIONS.md +12 -4
- package/docs/control_group.md +5 -0
- package/docs/control_object.md +133 -0
- package/docs/control_value.md +19 -0
- package/docs/css_architecture.md +66 -0
- package/docs/form_changed.md +52 -0
- package/docs/list_refresh.md +61 -12
- package/package.json +1 -1
|
@@ -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/css_architecture.md
CHANGED
|
@@ -120,6 +120,71 @@ Note that scoping to an ancestor is not enough: `.my-sidebar { --link-color-pres
|
|
|
120
120
|
|
|
121
121
|
When a component default deserves to be themed globally, promote it: declare a `--navi-<component>-<thing>` in [navi_css_vars.js](../src/navi_css_vars.js) and make the component default read `var(--navi-…)`.
|
|
122
122
|
|
|
123
|
+
#### An app narrower than the screen
|
|
124
|
+
|
|
125
|
+
An app that never spans the whole window — a phone-shaped column centered in a
|
|
126
|
+
wide one, bands on the sides — has one problem with popups: a dialog lives in
|
|
127
|
+
the browser's top layer, so it is calibrated on the _viewport_, and would paint
|
|
128
|
+
1500px of modal over a 600px app. The top bar and the bottom nav have the same
|
|
129
|
+
problem and solve it by repeating the app width by hand; popups must not need
|
|
130
|
+
that, because the app would then have to know which components exist.
|
|
131
|
+
|
|
132
|
+
So the app states its own screen once, and never names a component:
|
|
133
|
+
|
|
134
|
+
```css
|
|
135
|
+
:root {
|
|
136
|
+
--navi-app-max-width: 600px;
|
|
137
|
+
/* --navi-app-max-height too, for an app that also caps its height */
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
In pixels: popup placement reads this value back from CSS to compute its own
|
|
142
|
+
margins, and a custom property computes to a token stream rather than to a
|
|
143
|
+
length, so `40rem` would arrive there as the string `"40rem"`. A non-px value
|
|
144
|
+
still caps the popup's size (that part is pure CSS) but leaves the margins
|
|
145
|
+
viewport-sized, and says so in the console.
|
|
146
|
+
|
|
147
|
+
Every popup follows: `Dialog`, `Popover`, and everything built on them
|
|
148
|
+
(`Picker`, `Select`…). It is a ceiling and nothing more — on a screen narrower
|
|
149
|
+
than the app it never binds, and each popup still subtracts its own
|
|
150
|
+
`marginWithContainer` from it, so the gap with the edges is kept either way.
|
|
151
|
+
That gap is itself a share of the app's screen, not of the window (`"3appw"`,
|
|
152
|
+
navi's own unit alongside `vvw`/`vvh`) — otherwise a 3% margin measured on a
|
|
153
|
+
1500px window would eat 90px out of a 600px app.
|
|
154
|
+
|
|
155
|
+
Do **not** try to get this by setting `--dialog-max-width` on `.navi_dialog`
|
|
156
|
+
from the app. Two reasons:
|
|
157
|
+
|
|
158
|
+
- it is a `--component-*` token, declared on the element (see the table above),
|
|
159
|
+
so components that write it themselves outrank an app rule of lower
|
|
160
|
+
specificity — `.navi_picker[aria-haspopup="dialog"] .navi_dialog` does exactly
|
|
161
|
+
that, and the app's cap silently disappears for every picker;
|
|
162
|
+
- it is the knob a single popup uses to ask for a specific size, not a ceiling.
|
|
163
|
+
`--navi-app-max-width` feeds `--dialog-maxmax-width`, the hard ceiling _under_
|
|
164
|
+
that knob, so a popup that genuinely needs its own `maxWidth` can still say so
|
|
165
|
+
without any of them escaping the app's screen.
|
|
166
|
+
|
|
167
|
+
##### Current limitations
|
|
168
|
+
|
|
169
|
+
`--navi-app-max-width` caps how big a popup may get; it does not move where one
|
|
170
|
+
is placed. Placement is still computed against the real viewport
|
|
171
|
+
(`pickPositionRelativeTo`, in `@jsenv/dom`). That is invisible for anything
|
|
172
|
+
centered on its cross axis — `center`, `bottom`, `top`, which is what a dialog
|
|
173
|
+
does nearly always — but shows for anything anchored to an edge: a
|
|
174
|
+
`positionArea` like `bottom-start`, a `SidePanel`, a fixed bar. Those sit
|
|
175
|
+
against the window's edge rather than the app column's, so they stay on the real
|
|
176
|
+
viewport for now (`side_panel.jsx` restates `--dialog-maxmax-width` as the full
|
|
177
|
+
viewport on purpose).
|
|
178
|
+
|
|
179
|
+
Making them follow the app column too means narrowing the container rect
|
|
180
|
+
placement is computed against, inside `pickPositionRelativeTo` — worth doing the
|
|
181
|
+
day a side panel or a fixed bar has to live inside a simulated screen.
|
|
182
|
+
|
|
183
|
+
Note that an app can already get all of it, placement included, by rendering
|
|
184
|
+
itself in an iframe of the target width: the viewport then genuinely _is_ the
|
|
185
|
+
app's screen and no token is needed at all. `--navi-app-max-width` is the answer
|
|
186
|
+
for an app that does not want to pay that price.
|
|
187
|
+
|
|
123
188
|
### 3. Direct rule override (avoid unless necessary)
|
|
124
189
|
|
|
125
190
|
Overriding the actual CSS rules (not the variables) is intentionally hard — that is by design. If you find yourself needing to do this, it usually means a CSS variable should be exposed for that property. Open an issue or add the variable yourself and contribute it back.
|
|
@@ -133,4 +198,5 @@ Overriding the actual CSS rules (not the variables) is intentionally hard — th
|
|
|
133
198
|
| One component instance | Component prop or `style` attribute |
|
|
134
199
|
| All instances of a component | `--component-*` in unlayered app CSS, on a selector matching the component |
|
|
135
200
|
| A global design token | `--navi-*` on `:root` |
|
|
201
|
+
| How wide popups may ever get | `--navi-app-max-width` on `:root` |
|
|
136
202
|
| A structural layout rule | Expose a new CSS variable (contribute) |
|
package/docs/form_changed.md
CHANGED
|
@@ -7,6 +7,7 @@ as an answer the form already holds, and what to do on a screen whose fields are
|
|
|
7
7
|
filled a request later.
|
|
8
8
|
|
|
9
9
|
- [Sending nothing is the default](#sending-nothing-is-the-default)
|
|
10
|
+
- [What follows a send](#what-follows-a-send)
|
|
10
11
|
- [What the form is measured against](#what-the-form-is-measured-against)
|
|
11
12
|
- [What counts as already held](#what-counts-as-already-held)
|
|
12
13
|
- [A screen filled after it opened: `pristineKey`](#a-screen-filled-after-it-opened-pristinekey)
|
|
@@ -31,6 +32,50 @@ duplicates are fine.
|
|
|
31
32
|
<Form action={notify} canSendWhileUnchanged>
|
|
32
33
|
```
|
|
33
34
|
|
|
35
|
+
## What follows a send
|
|
36
|
+
|
|
37
|
+
The form has answered its question; `command` says what the screen does about
|
|
38
|
+
it — dismiss the popup (`--navi-close`), move on the slide map
|
|
39
|
+
(`--navi-left`…), go to a page (`--navi-nav-to:/games/42`), stay put
|
|
40
|
+
(`--navi-void`). Left out, the surface the form sits in decides: a popup closes,
|
|
41
|
+
a slide goes on, a form on a page does nothing.
|
|
42
|
+
|
|
43
|
+
It runs **whether or not there was anything to send** — that is the other half
|
|
44
|
+
of the rule above: the person is done either way, and a submit that ran no
|
|
45
|
+
action still closes the popup, still moves on, still navigates. Which is why
|
|
46
|
+
this is a prop, decided before the send: the form has to know where it goes even
|
|
47
|
+
when nothing happened.
|
|
48
|
+
|
|
49
|
+
Nothing runs when the send fails, or when a constraint refuses it. The form then
|
|
50
|
+
stays in front of the person, showing what it is waiting for.
|
|
51
|
+
|
|
52
|
+
### When only the response knows where to go
|
|
53
|
+
|
|
54
|
+
A creation lands on the page the server just made, and its id comes back with
|
|
55
|
+
the response — too late for a prop. Do it in the action, which is where the
|
|
56
|
+
answer is:
|
|
57
|
+
|
|
58
|
+
```jsx
|
|
59
|
+
<Form
|
|
60
|
+
action={async (value) => {
|
|
61
|
+
const game = await createGame(value);
|
|
62
|
+
navTo(`/games/${game.id}`);
|
|
63
|
+
}}
|
|
64
|
+
>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Nothing to declare: a creation always has something to send, so there is no
|
|
68
|
+
"the press did nothing" case for `command` to cover.
|
|
69
|
+
|
|
70
|
+
If you would rather it go through the command machinery all the same (to reuse
|
|
71
|
+
whatever a command does on that surface), the form carries what follows the send
|
|
72
|
+
as `data-after-send`, read once the send has succeeded — so an action can write
|
|
73
|
+
it while it runs:
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
formRef.current.setAttribute("data-after-send", `--navi-nav-to:/games/${id}`);
|
|
77
|
+
```
|
|
78
|
+
|
|
34
79
|
## What the form is measured against
|
|
35
80
|
|
|
36
81
|
One value, called the baseline here: **what the form held the last time it had
|
|
@@ -90,6 +135,11 @@ Its submit is live, and pressing it sends back the resource untouched.
|
|
|
90
135
|
Change it **once**, when the screen is ready. Taken again after someone started
|
|
91
136
|
typing, it would call what they wrote the reference.
|
|
92
137
|
|
|
138
|
+
No need to delay it by a tick: the reference is taken when the fields have
|
|
139
|
+
settled, and again at the end of that same tick — so a row that arrives in a
|
|
140
|
+
render of its own (a value computed from signals, a memoized row) is part of it
|
|
141
|
+
without the screen having to know which of its fields settle late.
|
|
142
|
+
|
|
93
143
|
Do not use a `key` on the `<Form>` for this: it remounts every control and every
|
|
94
144
|
popup inside it, and anything half-typed goes with them.
|
|
95
145
|
|
|
@@ -124,4 +174,6 @@ It is then neither collected nor complained about.
|
|
|
124
174
|
|
|
125
175
|
- [control_value.md](./control_value.md) — who holds a control's value:
|
|
126
176
|
nothing, a bound `signal`, or you
|
|
177
|
+
- [control_object.md](./control_object.md) — one value made of several
|
|
178
|
+
controls: `ControlGroup`, `Form`, and a picker whose value is an object
|
|
127
179
|
- [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
|
|