@jsenv/navi 0.29.418 → 0.29.419
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/README.md +1 -1
- package/dist/dev/jsenv_navi.js +192 -43
- package/dist/dev/jsenv_navi.js.map +10 -10
- package/dist/jsenv_navi.js +190 -41
- package/dist/jsenv_navi.js.map +10 -10
- package/docs/AI_INSTRUCTIONS.md +177 -261
- package/docs/actions.md +164 -251
- package/docs/autofocus.md +66 -83
- package/docs/control_object.md +117 -214
- package/docs/control_value.md +121 -185
- package/docs/create_and_edit.md +159 -254
- package/docs/css_architecture.md +196 -260
- package/docs/data_states.md +41 -44
- package/docs/dialog_shape.md +91 -134
- package/docs/drag_interactions.md +115 -174
- package/docs/drag_to_travel.md +10 -9
- package/docs/dynamic_import.md +119 -136
- package/docs/error_handling.md +75 -127
- package/docs/field_validation.md +49 -89
- package/docs/form_changed.md +61 -78
- package/docs/group.md +141 -0
- package/docs/i18n.md +31 -76
- package/docs/interactions.md +6 -4
- package/docs/list_action.md +36 -51
- package/docs/list_refresh.md +73 -115
- package/docs/mobile_touch.md +56 -62
- package/docs/navigation.md +377 -723
- package/docs/network_policy.md +61 -75
- package/docs/pan_zoom.md +62 -103
- package/docs/popup_backdrop.md +128 -168
- package/docs/popup_lift.md +179 -268
- package/docs/popup_open.md +380 -651
- package/docs/resource.md +228 -408
- package/docs/route_transitions.md +333 -482
- package/docs/safe_area.md +102 -89
- package/docs/scroll.md +175 -335
- package/docs/state_binding.md +2 -1
- package/docs/testid.md +18 -23
- package/docs/typography.md +111 -141
- package/docs/view_transitions.md +106 -104
- package/docs/z_index.md +61 -109
- package/package.json +2 -2
- package/docs/badge_list.md +0 -83
- package/docs/control_group.md +0 -168
- package/docs/mobile_layout_pitfalls.md +0 -145
package/docs/resource.md
CHANGED
|
@@ -29,9 +29,8 @@ Each callback returns the data to upsert into the store:
|
|
|
29
29
|
|
|
30
30
|
Actions are read in components through the action system (`useAsyncData`,
|
|
31
31
|
`<Button action>`, …) — see [actions.md](./actions.md). An action's `data` is
|
|
32
|
-
`undefined` until its callback has answered once, plural verbs included
|
|
33
|
-
|
|
34
|
-
looks like ([data_states.md](./data_states.md#data-and-loading-are-independent)).
|
|
32
|
+
`undefined` until its callback has answered once, plural verbs included
|
|
33
|
+
([data_states.md](./data_states.md#data-and-loading-are-independent)).
|
|
35
34
|
|
|
36
35
|
- [`store.upsert()` is not how data enters the store](#storeupsert-is-not-how-data-enters-the-store)
|
|
37
36
|
- [`persist`: the last answer, drawn again after a reload](#persist-the-last-answer-drawn-again-after-a-reload)
|
|
@@ -39,11 +38,6 @@ looks like ([data_states.md](./data_states.md#data-and-loading-are-independent))
|
|
|
39
38
|
- [Searching the same collection](#searching-the-same-collection)
|
|
40
39
|
- [Relations: pick one of the four methods](#relations-pick-one-of-the-four-methods)
|
|
41
40
|
- [Callback return contracts](#callback-return-contracts)
|
|
42
|
-
- [`.one(propertyName, childResource, { GET, PUT, DELETE })`](#onepropertyname-childresource--get-put-delete-)
|
|
43
|
-
- [`.many(propertyName, childResource, restCallbacks)`](#manypropertyname-childresource-restcallbacks)
|
|
44
|
-
- [`.scopedOne(propertyName, { idKey, GET, POST, PUT, PATCH, DELETE })`](#scopedonepropertyname--idkey-get-post-put-patch-delete-)
|
|
45
|
-
- [`.scopedMany(propertyName, { idKey, GET, GET_MANY, POST, PUT, PATCH, DELETE, … })`](#scopedmanypropertyname--idkey-get-get_many-post-put-patch-delete--)
|
|
46
|
-
- [A relation declared on a scoped child](#a-relation-declared-on-a-scoped-child)
|
|
47
41
|
- [When the backend answers a sub-route with the whole parent](#when-the-backend-answers-a-sub-route-with-the-whole-parent)
|
|
48
42
|
- [Relations and autorerun](#relations-and-autorerun)
|
|
49
43
|
- [`withParams()`: a scope with reruns of its own](#withparams-a-scope-with-reruns-of-its-own)
|
|
@@ -54,10 +48,10 @@ looks like ([data_states.md](./data_states.md#data-and-loading-are-independent))
|
|
|
54
48
|
## `store.upsert()` is not how data enters the store
|
|
55
49
|
|
|
56
50
|
navi writes the store. Declare the resource and its relations, return the shape
|
|
57
|
-
each callback owes
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
51
|
+
each callback owes, and the write happens. A hand-written `store.upsert()` in
|
|
52
|
+
the path of a normal request of the resource is the sign that something is
|
|
53
|
+
missing: a relation that is not declared, or a callback that does not return
|
|
54
|
+
what it should.
|
|
61
55
|
|
|
62
56
|
`store` is exposed for what is **not** a request of the resource, where writing
|
|
63
57
|
it yourself is the point:
|
|
@@ -65,28 +59,23 @@ it yourself is the point:
|
|
|
65
59
|
- seeding it with data that came from elsewhere — state rendered by the server,
|
|
66
60
|
a websocket message (the resource's own last answer is not "elsewhere": that
|
|
67
61
|
is [`persist`](#persist-the-last-answer-drawn-again-after-a-reload), below);
|
|
68
|
-
- absorbing the parent
|
|
69
|
-
callback
|
|
70
|
-
|
|
62
|
+
- absorbing the parent fields a **scoped** relation's route answered with: a
|
|
63
|
+
`.scopedOne()`/`.scopedMany()` callback writes the relation and nothing else
|
|
64
|
+
(see [When the backend answers a sub-route with the whole
|
|
71
65
|
parent](#when-the-backend-answers-a-sub-route-with-the-whole-parent)).
|
|
72
66
|
|
|
73
|
-
A list that loads its rows a slice at a time is **not** one of those cases —
|
|
74
|
-
`GET_RANGE`,
|
|
67
|
+
A list that loads its rows a slice at a time is **not** one of those cases —
|
|
68
|
+
that is `GET_RANGE`, below.
|
|
75
69
|
|
|
76
70
|
## `persist`: the last answer, drawn again after a reload
|
|
77
71
|
|
|
78
|
-
Some `GET`s are the app
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
[data_states.md](./data_states.md#data-and-loading-are-independent) reached on
|
|
86
|
-
a reload: the last known row drawn, the request out behind it, the fresh answer
|
|
87
|
-
replacing it. `persist` names the resource whose answer is worth keeping, and
|
|
88
|
-
the signal that keeps it — a `stateSignal` that persists, the primitive navi
|
|
89
|
-
has for a state that survives a reload:
|
|
72
|
+
Some `GET`s are the app — `GET /me` answers who is signed in, what the header
|
|
73
|
+
and the tab bar draw — and on a reload, or an app reopened from the home screen,
|
|
74
|
+
waiting for them is pure loss: the previous answer was on screen a moment ago
|
|
75
|
+
and is almost always still true. `persist` puts that reload on the second line
|
|
76
|
+
of the table in [data_states.md](./data_states.md#data-and-loading-are-independent)
|
|
77
|
+
— the last known row drawn, the request out behind it — and names the signal
|
|
78
|
+
that keeps the copy, a `stateSignal` that persists:
|
|
90
79
|
|
|
91
80
|
```js
|
|
92
81
|
const mePersistedSignal = stateSignal(undefined, {
|
|
@@ -105,70 +94,50 @@ ME.one("user", USER);
|
|
|
105
94
|
ME.many("my_games", GAME);
|
|
106
95
|
```
|
|
107
96
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
- **the first run of the `GET` draws the kept row.** It enters the store and
|
|
116
|
-
becomes the action's provisional value: `data` is set while `loading` is
|
|
117
|
-
`true`, the row is on screen and the request is out behind it. Nothing else
|
|
118
|
-
moves — `runningState` is `running`, the answer replaces the row, an error
|
|
119
|
-
lands in `errorSignal` beside it;
|
|
120
|
-
- **the copy follows the store.** It is written with its relations inline — the
|
|
121
|
-
shape the `GET` callback returns, so it re-enters through the same setters a
|
|
122
|
-
real answer goes through — and rewritten whenever the row or a row it reaches
|
|
123
|
-
through a relation changes: a `PUT` on the item, a list upserting one of its
|
|
124
|
-
children, a game joining `my_games`. A rewrite that changes nothing is
|
|
125
|
-
skipped, so the signal moves only when the copy does;
|
|
126
|
-
- **under a network policy answering reads from the store, the kept row answers
|
|
127
|
-
the `GET`** ([network_policy.md](./network_policy.md)): a reload with no
|
|
128
|
-
network reopens on the last answer rather than on nothing.
|
|
97
|
+
The copy follows the store — a `PUT` on the item, a game joining `my_games`
|
|
98
|
+
rewrite it — and the first run of the `GET` draws it as its provisional value:
|
|
99
|
+
`data` set while `loading` is `true`, the answer replacing it. Under a network
|
|
100
|
+
policy answering reads from the store, the kept row answers the `GET`
|
|
101
|
+
([network_policy.md](./network_policy.md)): a reload with no network reopens on
|
|
102
|
+
it. A plain `signal()` keeps the copy for the page's lifetime, which is what a
|
|
103
|
+
test wants.
|
|
129
104
|
|
|
130
105
|
What stays the app's, because only the app knows:
|
|
131
106
|
|
|
132
|
-
- **the key.** A
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
new id starts clean.
|
|
107
|
+
- **the key.** A deploy that changes the shape of the answer must not draw the
|
|
108
|
+
old one: put in the signal's `id` whatever makes a copy unusable — the
|
|
109
|
+
deployed version — and a new id starts clean.
|
|
136
110
|
- **`when`: is the copy about this session?** A tab viewing as someone else, an
|
|
137
|
-
account being switched — the row
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
- **the moment, not the state.** Sign-out is
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
what it shows until the app resets the action, as it does today.
|
|
111
|
+
account being switched — the row is somebody's, and must not be kept for the
|
|
112
|
+
next person. `when` is read whenever the copy is about to be read or written:
|
|
113
|
+
`false` reads nothing, writes nothing, and empties the signal. It is a
|
|
114
|
+
function so it can read a signal, and it has to: a plain variable is only
|
|
115
|
+
looked at again when the store changes.
|
|
116
|
+
- **the moment, not the state.** Sign-out is something that happens, not a
|
|
117
|
+
condition that holds: writing `undefined` into the signal forgets the copy at
|
|
118
|
+
once, and nothing is written until a `GET` lands again. The store rows stay —
|
|
119
|
+
the screen keeps what it shows until the app resets the action.
|
|
147
120
|
- **what the `GET` feeds beside its return value.** Counts, badges, a feature
|
|
148
|
-
flag set from the response are the app's signals, not the row
|
|
149
|
-
kept, and the callback does not run on a kept row.
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
the emptiness test of data_states.md — and the refresh is shown as usual.
|
|
121
|
+
flag set from the response are the app's signals, not the row: they are not
|
|
122
|
+
kept, and the callback does not run on a kept row.
|
|
123
|
+
- **the veil.** A loading screen waiting on the `GET` being `running` stays over
|
|
124
|
+
the drawn row for the whole request. It waits on `data` being set — the
|
|
125
|
+
emptiness test of data_states.md — and the refresh is shown as usual.
|
|
154
126
|
|
|
155
127
|
What the copy is not:
|
|
156
128
|
|
|
157
|
-
- **an answer.** The `GET` goes out every time;
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
`GET` is asked for every id would keep every id. It is meant for a singleton
|
|
162
|
-
or a handful of rows.
|
|
129
|
+
- **an answer.** The `GET` goes out every time; a screen must not read `data` as
|
|
130
|
+
"the server confirmed" — that is `loading`, and the two are independent.
|
|
131
|
+
- **a store on disk.** One row per params key, `GET` only: meant for a singleton
|
|
132
|
+
or a handful of rows, not a resource whose `GET` is asked for every id.
|
|
163
133
|
- **seeded at declaration.** The row enters the store at the first run of the
|
|
164
|
-
`GET`, once the relations are declared, so
|
|
165
|
-
|
|
166
|
-
that first run is empty.
|
|
134
|
+
`GET`, once the relations are declared, so its relation values are normalized
|
|
135
|
+
as a real answer's are. `ME.store` read before that first run is empty.
|
|
167
136
|
|
|
168
137
|
## `GET_RANGE`: feeding a list that loads as it scrolls
|
|
169
138
|
|
|
170
139
|
A `<List.Items>` asks for the rows it is about to draw and keeps what it gets.
|
|
171
|
-
`GET_RANGE` is the resource's answer to that question
|
|
140
|
+
`GET_RANGE` is the resource's answer to that question, one slice at a time:
|
|
172
141
|
|
|
173
142
|
```js
|
|
174
143
|
const GAME = resource("game", {
|
|
@@ -188,18 +157,19 @@ const GAME = resource("game", {
|
|
|
188
157
|
```
|
|
189
158
|
|
|
190
159
|
The callback receives the bound params merged with the range the list asks for
|
|
191
|
-
(`start`, `end`, `limit`, `before`, `after`, `around
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
160
|
+
(`start`, `end`, `limit`, `before`, `after`, `around`), and `{ signal }`,
|
|
161
|
+
aborted when the list stops wanting those rows. It returns a range the way a
|
|
162
|
+
`Content-Range` does: **`{ items, start, count }`** — these rows, at this place,
|
|
163
|
+
out of that many. `start` may be omitted when the list asked for one at or after
|
|
164
|
+
`0` (a negative `start` counts back from the end, and the source must then say
|
|
165
|
+
where the slice landed); `count` defaults to `start + items.length`. The items
|
|
166
|
+
are upserted on their way in, so the list draws store items, never copies of the
|
|
167
|
+
JSON.
|
|
168
|
+
|
|
169
|
+
But the list keeps the object it was handed, and an update replaces the item
|
|
170
|
+
object (the store holds values, which is what makes a change detectable). So a
|
|
171
|
+
row that must follow its **own fields** through a write reads them from the
|
|
172
|
+
store:
|
|
203
173
|
|
|
204
174
|
```jsx
|
|
205
175
|
const GameCard = ({ id }) => {
|
|
@@ -208,41 +178,27 @@ const GameCard = ({ id }) => {
|
|
|
208
178
|
};
|
|
209
179
|
```
|
|
210
180
|
|
|
211
|
-
|
|
212
|
-
makes a change detectable), so the one the list is holding is the one it was
|
|
213
|
-
given. Relations are not concerned: they are keyed by owner, and a row reading
|
|
181
|
+
Relations are not concerned: they are keyed by owner, and a row reading
|
|
214
182
|
`game.candidates` reads the shared collection whatever object carries it.
|
|
215
183
|
|
|
216
|
-
`GET_RANGE` is a **reader, not an action
|
|
217
|
-
place in the rerun graph
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
it rather than starting over
|
|
228
|
-
([list_refresh.md](./list_refresh.md#leaving-the-screen-and-coming-back)).
|
|
229
|
-
|
|
230
|
-
Membership is what a re-read answers: a verb listed in `rerunOn.GET_RANGE`
|
|
231
|
-
(`["POST", "DELETE"]` by default) tells the run the collection moved, and it
|
|
232
|
-
asks again for the window it draws, rows staying on screen meanwhile. A write
|
|
233
|
-
that moves an item in or out through another verb — a `PATCH` that archives —
|
|
234
|
-
is added there, or `reader.invalidate()` is called; a `key` on the run is not the
|
|
235
|
-
way ([list_refresh.md](./list_refresh.md#a-paginated-list-stays-on-screen-too)).
|
|
236
|
-
|
|
237
|
-
It reads a collection, so it lives on the resource itself (or on a
|
|
238
|
-
`withParams()` of it), not on a relation.
|
|
184
|
+
`GET_RANGE` is a **reader, not an action**: it keeps no response and takes no
|
|
185
|
+
place in the rerun graph — the list already holds the slices it received, and a
|
|
186
|
+
`POST` invalidating "the collection" would otherwise send every slice ever
|
|
187
|
+
loaded back to the network at once. What it keeps is the collection's
|
|
188
|
+
composition, so a list coming back finds it drawn
|
|
189
|
+
([list_refresh.md](./list_refresh.md#leaving-the-screen-and-coming-back)), and a
|
|
190
|
+
verb listed in `rerunOn.GET_RANGE` makes it ask again for the window it draws,
|
|
191
|
+
rows staying on screen
|
|
192
|
+
([list_refresh.md](./list_refresh.md#a-paginated-list-stays-on-screen-too)). It
|
|
193
|
+
reads a collection, so it lives on the resource (or on a `withParams()` of it),
|
|
194
|
+
not on a relation.
|
|
239
195
|
|
|
240
196
|
## Searching the same collection
|
|
241
197
|
|
|
242
|
-
A screen showing a collection asks a second question about it
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
198
|
+
A screen showing a collection soon asks a second question about it: the list
|
|
199
|
+
draws a page, a search box goes looking in the whole base for what the page does
|
|
200
|
+
not hold. Same entity, same ids — so it is the same `GET_MANY`, with one more
|
|
201
|
+
param:
|
|
246
202
|
|
|
247
203
|
```js
|
|
248
204
|
const USER = resource("user", {
|
|
@@ -251,65 +207,53 @@ const USER = resource("user", {
|
|
|
251
207
|
});
|
|
252
208
|
|
|
253
209
|
const pageAction = USER.GET_MANY.bindParams({ scope: "seatable" });
|
|
254
|
-
const searchAction = USER.GET_MANY.bindParams(
|
|
255
|
-
scope: "seatable",
|
|
256
|
-
|
|
257
|
-
|
|
210
|
+
const searchAction = USER.GET_MANY.bindParams(
|
|
211
|
+
computed(() => ({ scope: "seatable", search: searchSignal.value })),
|
|
212
|
+
{ debounce: 300 },
|
|
213
|
+
);
|
|
258
214
|
```
|
|
259
215
|
|
|
260
|
-
Not a resource of its own, and not a `createAction` sitting beside the resource
|
|
261
|
-
|
|
262
|
-
object from the same user everywhere else on screen
|
|
216
|
+
Not a resource of its own, and not a `createAction` sitting beside the resource:
|
|
217
|
+
those answer with **detached copies** — the user a search found is a different
|
|
218
|
+
object from the same user everywhere else on screen, it does not follow an
|
|
263
219
|
update, and a mutation writes to the one it is not. The resource's own
|
|
264
220
|
`GET_MANY` upserts what comes back like any other read, so one object per user
|
|
265
221
|
answers both questions.
|
|
266
222
|
|
|
267
223
|
**Two `GET_MANY` on one resource do not compete.** Each bound instance keeps its
|
|
268
|
-
own array of ids (none until it has answered) and resolves it against the
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
wholesale"](#when-the-backend-answers-a-sub-route-with-the-whole-parent), below,
|
|
273
|
-
is about the rows the store keeps for one owner — not about what some other
|
|
274
|
-
action reading the same resource is showing.
|
|
275
|
-
|
|
276
|
-
It is `bindParams`, not `withParams`: a `withParams()` carves an autorerun scope
|
|
277
|
-
(a POST at the root does not reach into it), and a word someone typed is not a
|
|
278
|
-
scope — the search must refresh after a write like the list does
|
|
224
|
+
own array of ids (none until it has answered) and resolves it against the shared
|
|
225
|
+
store, so answering the search leaves the page's list as it was — clear the
|
|
226
|
+
search box and every row it had is still there. It is `bindParams`, not
|
|
227
|
+
`withParams`: a word someone typed is not a scope
|
|
279
228
|
([`withParams()`](#withparams-a-scope-with-reruns-of-its-own)).
|
|
280
229
|
|
|
281
|
-
The lifecycle a search wants comes with being an action
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
([actions.md](./actions.md#reading-an-action)).
|
|
230
|
+
The lifecycle a search wants comes with being an action, read with
|
|
231
|
+
`useAsyncData(searchAction, { run: true, loading: true })`
|
|
232
|
+
([actions.md](./actions.md#params-bindparams-and-calling-the-action)): the typing
|
|
233
|
+
is debounced by the binding, `loading` is up while a word is in flight, and a
|
|
234
|
+
word already asked is answered by the instance that asked it, with no second
|
|
235
|
+
request.
|
|
288
236
|
|
|
289
237
|
What stays the app's is what to KEEP of a search once the word changes — the
|
|
290
238
|
rows someone picked out of it and expects to still see under the next word.
|
|
291
239
|
Keep their **ids**, not the objects: an object is a snapshot, and it stops
|
|
292
240
|
following its row the moment anything writes to it. `RESOURCE.useAllByIds(ids)`
|
|
293
|
-
reads those rows as they are now —
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
store are written, and re-renders when its row is — `useArray` to the whole
|
|
300
|
-
store. An entry of `useAllByIds` given by a unique key rather than by id can
|
|
301
|
-
only be found by reading the whole store, and that read follows it too.
|
|
241
|
+
reads those rows as they are now — the ids the store no longer holds drop out —
|
|
242
|
+
`RESOURCE.useById(id)` one row, `RESOURCE.useArray()` the whole store. None is a
|
|
243
|
+
hook (a `.map()` over them is fine), and each subscribes the render that calls
|
|
244
|
+
it to what it returns: a card reading its own row stays still while other rows
|
|
245
|
+
of the store are written. An entry of `useAllByIds` given by a unique key rather
|
|
246
|
+
than by id is found by reading the whole store, and that read follows it too.
|
|
302
247
|
|
|
303
248
|
A row counts as written only when something in it changed:
|
|
304
249
|
|
|
305
|
-
- **An answer**
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
and the game row stays the one it was.
|
|
250
|
+
- **An answer** is compared in depth to the row held: a revalidation returning
|
|
251
|
+
the same rows keeps every one of them — though parsed JSON makes every nested
|
|
252
|
+
object a new one — and their readers stay still.
|
|
253
|
+
- **A relation** changes its row when the row points at other children; the
|
|
254
|
+
children themselves are followed through the relation — `game.place.label`
|
|
255
|
+
read in a render re-renders it when the place is written, and the game row
|
|
256
|
+
stays the one it was.
|
|
313
257
|
- **A value written with `store.upsert()`** is compared by reference: a new
|
|
314
258
|
object is a change, whatever it holds.
|
|
315
259
|
|
|
@@ -330,14 +274,11 @@ PATCH: ({ id, op, ...rest }) => {
|
|
|
330
274
|
|
|
331
275
|
One verb dispatching on a string gives up everything the store does for you:
|
|
332
276
|
per-operation action state (loading/error per button), per-relation autorerun,
|
|
333
|
-
and a child collection that other components can read.
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
spelled as one, and the one the gesture is actually about (game × group) is the
|
|
339
|
-
one that ends up with no rows, no action state, and nothing for its row on
|
|
340
|
-
screen to read.
|
|
277
|
+
and a child collection that other components can read. The same smell wears a
|
|
278
|
+
second coat: a verb on one relation carrying an id from a **different** relation
|
|
279
|
+
— a `group_id` sent to the game × user shares, which the server expands into
|
|
280
|
+
people — is two relations spelled as one, and the one the gesture is about
|
|
281
|
+
(game × group) ends up with no rows and nothing for its row on screen to read.
|
|
341
282
|
|
|
342
283
|
| Situation | Use |
|
|
343
284
|
| ------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
|
@@ -350,173 +291,53 @@ screen to read.
|
|
|
350
291
|
Singular vs plural is about the relation, not the verb: `.one`/`.scopedOne` for a
|
|
351
292
|
single sub-object, `.many`/`.scopedMany` for a collection.
|
|
352
293
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
never happened. The fix is on the shape — give that relation its own
|
|
362
|
-
relationship method and a callback returning what was written, even when the
|
|
363
|
-
backend must grow a field for it — never a flag next to the button.
|
|
294
|
+
**A write whose effect the response does not carry is a modelling bug, not a
|
|
295
|
+
case for the client to remember.** The row on screen claiming "this happened"
|
|
296
|
+
reads the store, which holds only what responses carried; whatever the screen
|
|
297
|
+
memorizes instead lives on the client and dies with the session — reload, and
|
|
298
|
+
the row confidently claims the write never happened. The fix is on the shape —
|
|
299
|
+
give that relation its own relationship method and a callback returning what was
|
|
300
|
+
written, even when the backend must grow a field for it — never a flag next to
|
|
301
|
+
the button.
|
|
364
302
|
|
|
365
303
|
## Callback return contracts
|
|
366
304
|
|
|
367
|
-
These are not guessable — each relationship method has its own shape
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
`DELETE`
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
`
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
```js
|
|
396
|
-
const USER_FRIENDS = USER.many("friends", USER, {
|
|
397
|
-
GET_MANY: async ({ id }) => {
|
|
398
|
-
const friends = await fetchJson(`/users/${id}/friends`);
|
|
399
|
-
return { id, friends }; // { id, friends: [{ id: 2 }, { id: 3 }] }
|
|
400
|
-
},
|
|
401
|
-
POST: async ({ id, friendId }) =>
|
|
402
|
-
fetchJson(`/users/${id}/friends`, { method: "POST", body: { friendId } }),
|
|
403
|
-
DELETE: async ({ id, friendId }) => {
|
|
404
|
-
await fetchJson(`/users/${id}/friends/${friendId}`, { method: "DELETE" });
|
|
405
|
-
return [id, friendId]; // [parentId, childId]
|
|
406
|
-
},
|
|
407
|
-
DELETE_MANY: async ({ id, friendIds }) => {
|
|
408
|
-
await fetchJson(`/users/${id}/friends`, {
|
|
409
|
-
method: "DELETE",
|
|
410
|
-
body: { friendIds },
|
|
411
|
-
});
|
|
412
|
-
return [id, friendIds]; // [parentId, [childId, …]]
|
|
413
|
-
},
|
|
414
|
-
});
|
|
415
|
-
```
|
|
416
|
-
|
|
417
|
-
The singular POST/PUT/PATCH callbacks return the **child** object. It is upserted
|
|
418
|
-
into the child store, but it does not join the parent's array on its own: the
|
|
419
|
-
array is the backend's, and only a GET_MANY refresh rewrites it.
|
|
420
|
-
|
|
421
|
-
### `.scopedOne(propertyName, { idKey, GET, POST, PUT, PATCH, DELETE })`
|
|
422
|
-
|
|
423
|
-
Every callback returns `[ownerId, props | null]`:
|
|
424
|
-
|
|
425
|
-
```js
|
|
426
|
-
const USER_PROFILE = USER.scopedOne("profile", {
|
|
427
|
-
GET: async ({ id }) => [id, await fetchJson(`/users/${id}/profile`)],
|
|
428
|
-
PATCH: async ({ id, ...props }) => [
|
|
429
|
-
id,
|
|
430
|
-
await fetchJson(`/users/${id}/profile`, { method: "PATCH", body: props }),
|
|
431
|
-
],
|
|
432
|
-
DELETE: async ({ id }) => {
|
|
433
|
-
await fetchJson(`/users/${id}/profile`, { method: "DELETE" });
|
|
434
|
-
return [id, null];
|
|
435
|
-
},
|
|
436
|
-
});
|
|
437
|
-
```
|
|
438
|
-
|
|
439
|
-
The property is `null` until a callback provides data. `ownerId` may be
|
|
440
|
-
`{ [uniqueKey]: value }` when the owner is known by an alternate key rather than
|
|
441
|
-
its id.
|
|
442
|
-
|
|
443
|
-
### `.scopedMany(propertyName, { idKey, GET, GET_MANY, POST, PUT, PATCH, DELETE, … })`
|
|
444
|
-
|
|
445
|
-
Every callback returns `[ownerId, ...rest]`:
|
|
446
|
-
|
|
447
|
-
```js
|
|
448
|
-
const GAME_CANDIDATES = GAME.scopedMany("candidates", {
|
|
449
|
-
idKey: "user_id",
|
|
450
|
-
GET_MANY: async ({ id }) => [id, await fetchJson(`/games/${id}/candidates`)],
|
|
451
|
-
POST: async ({ id, ...body }) => [
|
|
452
|
-
id,
|
|
453
|
-
await fetchJson(`/games/${id}/candidates`, { method: "POST", body }),
|
|
454
|
-
],
|
|
455
|
-
PUT: async ({ id, oldUserId, ...props }) => [id, oldUserId, props], // id rename
|
|
456
|
-
DELETE: async ({ id, user_id }) => {
|
|
457
|
-
await fetchJson(`/games/${id}/candidates/${user_id}`, { method: "DELETE" });
|
|
458
|
-
return [id, user_id];
|
|
459
|
-
},
|
|
460
|
-
});
|
|
461
|
-
```
|
|
462
|
-
|
|
463
|
-
| Callback | Returns |
|
|
464
|
-
| ------------------ | ------------------------------------------------------ |
|
|
465
|
-
| GET / POST / PATCH | `[ownerId, props]` |
|
|
466
|
-
| PUT (id rename) | `[ownerId, oldId, props]` |
|
|
467
|
-
| DELETE | `[ownerId, childId]` |
|
|
468
|
-
| any `*_MANY` | `[ownerId, itemArray]` — replaces the whole collection |
|
|
469
|
-
| DELETE_MANY | `[ownerId, [childId, …]]` |
|
|
470
|
-
|
|
471
|
-
`idKey` names the child's own key inside its owner (`user_id` above); it defaults
|
|
472
|
-
to `"id"`.
|
|
473
|
-
|
|
474
|
-
### A relation declared on a scoped child
|
|
475
|
-
|
|
476
|
-
A scoped child takes relations of its own — a column pointing at a shared data
|
|
477
|
-
type:
|
|
478
|
-
|
|
479
|
-
```js
|
|
480
|
-
const TABLE_COLUMNS = TABLE.scopedMany("columns", {
|
|
481
|
-
idKey: "name",
|
|
482
|
-
GET_MANY: async ({ id }) => [id, await fetchJson(`/tables/${id}/columns`)],
|
|
483
|
-
// [id, [{ name: "email", dataType: { id: 2, name: "varchar" } }, …]]
|
|
484
|
-
PATCH: async ({ id, name, ...props }) => [
|
|
485
|
-
id,
|
|
486
|
-
await fetchJson(`/tables/${id}/columns/${name}`, {
|
|
487
|
-
method: "PATCH",
|
|
488
|
-
body: props,
|
|
489
|
-
}),
|
|
490
|
-
],
|
|
491
|
-
});
|
|
492
|
-
TABLE_COLUMNS.one("dataType", DATA_TYPE);
|
|
493
|
-
```
|
|
494
|
-
|
|
495
|
-
Every column gets a reactive `dataType` read from the `DATA_TYPE` store, fed by
|
|
496
|
-
what the scoped child's callbacks embed. The relation itself takes none of the
|
|
497
|
-
callbacks whose result names the column — `.one()`'s `GET`/`PUT`/`DELETE`,
|
|
498
|
-
`.many()`'s `GET_MANY`/`DELETE`/`DELETE_MANY`, and every callback of a
|
|
499
|
-
`.scopedOne()`/`.scopedMany()`, whose `ownerId` would be the column: they name
|
|
500
|
-
it by its `name` alone, and a column exists only inside its table — two tables
|
|
501
|
-
can each have an `email`. Declaring one throws. A column's data type is read and
|
|
502
|
-
changed through `TABLE_COLUMNS`, whose results say which table
|
|
503
|
-
(`[id, { name, dataType }]`); the fields of a data type itself, through
|
|
504
|
-
`DATA_TYPE`.
|
|
505
|
-
|
|
506
|
-
A `.scopedOne()`/`.scopedMany()` chained on a scoped child works the same way,
|
|
507
|
-
and keeps one value per column: `TABLE_COLUMNS.scopedMany("constraints")` gives
|
|
508
|
-
`users.email` and `admins.email` a collection each, fed by the `constraints`
|
|
509
|
-
embedded in each table's columns.
|
|
305
|
+
These are not guessable — each relationship method has its own shape, written
|
|
306
|
+
out with an example in its JSDoc:
|
|
307
|
+
|
|
308
|
+
- **`.one(propertyName, CHILD, { GET, PUT, DELETE })`** — `GET`/`PUT` return
|
|
309
|
+
the **parent** with the child nested inside (`{ id, session: { id: 10 } }`,
|
|
310
|
+
`session: null` for none); `DELETE` the parent id, or `{ id }`. A parent
|
|
311
|
+
`GET`/`POST` that embeds the child inline works too.
|
|
312
|
+
- **`.many(propertyName, CHILD, callbacks)`** — `GET_MANY` returns the
|
|
313
|
+
**parent** with the array nested inside; `GET`/`POST`/`PUT`/`PATCH` the
|
|
314
|
+
**child**, upserted into the child store but not joining the parent's array,
|
|
315
|
+
which only a `GET_MANY` rewrites; `DELETE` `[parentId, childId]`,
|
|
316
|
+
`DELETE_MANY` `[parentId, [childId, …]]`.
|
|
317
|
+
- **`.scopedOne(propertyName, callbacks)`** — every callback returns
|
|
318
|
+
`[ownerId, props | null]`; the property is `null` until one does.
|
|
319
|
+
- **`.scopedMany(propertyName, { idKey, … })`** — `[ownerId, props]`; `PUT` (an
|
|
320
|
+
id rename) `[ownerId, oldId, props]`; `DELETE` `[ownerId, childId]`; any
|
|
321
|
+
`*_MANY` `[ownerId, itemArray]`, which replaces the whole collection;
|
|
322
|
+
`DELETE_MANY` `[ownerId, [childId, …]]`. `idKey` names the child's own key
|
|
323
|
+
inside its owner (`"id"` by default).
|
|
324
|
+
|
|
325
|
+
`ownerId` may be `{ [uniqueKey]: value }` when the owner is known by an
|
|
326
|
+
alternate key. A scoped child takes relations of its own —
|
|
327
|
+
`TABLE_COLUMNS.one("dataType", DATA_TYPE)`, fed by what the columns' callbacks
|
|
328
|
+
embed — but none of the callbacks whose result names the child by itself
|
|
329
|
+
(`.one()`'s `GET`/`PUT`/`DELETE`, `.many()`'s `GET_MANY`/`DELETE`/`DELETE_MANY`,
|
|
330
|
+
every scoped callback): a column exists only inside its table, and two tables
|
|
331
|
+
can each have an `email`. Declaring one throws.
|
|
510
332
|
|
|
511
333
|
## When the backend answers a sub-route with the whole parent
|
|
512
334
|
|
|
513
335
|
This is the common REST shape, and it is the reason `op` dispatch feels
|
|
514
336
|
attractive: a full-parent response absorbs into a parent `PATCH` with no
|
|
515
337
|
thinking. Model the relation anyway and absorb the response in the callback —
|
|
516
|
-
`
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
[Searching the same collection](#searching-the-same-collection)).
|
|
338
|
+
**any `*_MANY` callback replaces the collection wholesale**: the rows the store
|
|
339
|
+
keeps for that owner, not what some other action reading the same resource is
|
|
340
|
+
showing (see [Searching the same collection](#searching-the-same-collection)).
|
|
520
341
|
|
|
521
342
|
```js
|
|
522
343
|
const GAME_CANDIDATES = GAME.scopedMany("candidates", {
|
|
@@ -537,22 +358,22 @@ const GAME_CANDIDATES = GAME.scopedMany("candidates", {
|
|
|
537
358
|
});
|
|
538
359
|
```
|
|
539
360
|
|
|
540
|
-
For `.many()`, the equivalent is its GET_MANY callback, which already takes
|
|
541
|
-
parent object with the array nested inside — return the response untouched.
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
and ignore the full parent the backend sent back.
|
|
361
|
+
For `.many()`, the equivalent is its `GET_MANY` callback, which already takes
|
|
362
|
+
the parent object with the array nested inside — return the response untouched.
|
|
363
|
+
Deletion is the exception: `DELETE`/`DELETE_MANY` drop by id rather than
|
|
364
|
+
replace, so return `[ownerId, deletedId]` from the id you already have in the
|
|
365
|
+
params, and ignore the full parent the backend sent back.
|
|
546
366
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
367
|
+
The parent's own fields in such a response (a `status`, a `score`) are applied
|
|
368
|
+
only where the callback returns the parent: `.one()`'s `GET`/`PUT` and
|
|
369
|
+
`.many()`'s `GET_MANY` upsert it whole, fields included. A scoped relation's
|
|
370
|
+
callback writes the relation alone — if the parent's own fields change too,
|
|
371
|
+
either let the parent `GET` rerun (below) or `store.upsert()` the parent
|
|
372
|
+
explicitly.
|
|
551
373
|
|
|
552
374
|
## Relations and autorerun
|
|
553
375
|
|
|
554
|
-
Relationship mutations do **not** invalidate their parent by default. The
|
|
555
|
-
rules, verified by `src/state/rest/tests/resource_graph_parent_rerun.test.js`:
|
|
376
|
+
Relationship mutations do **not** invalidate their parent by default. The rules:
|
|
556
377
|
|
|
557
378
|
- `.scopedMany` child **POST** reruns the owner's singular `GET`, whether it
|
|
558
379
|
goes through the child or through a `withParams()` scope of it — but only when
|
|
@@ -563,21 +384,21 @@ rules, verified by `src/state/rest/tests/resource_graph_parent_rerun.test.js`:
|
|
|
563
384
|
already carries the updated child.
|
|
564
385
|
- `.scopedOne` mutations rerun nothing, ever — the result is the new value.
|
|
565
386
|
- `.one` / `.many` children live in an independent store; mutating them never
|
|
566
|
-
reruns the parent. Declare it
|
|
567
|
-
|
|
387
|
+
reruns the parent. Declare it with
|
|
388
|
+
[`dependencies`](#dependencies-rerun-after-another-resource-writes) if you
|
|
389
|
+
need it.
|
|
568
390
|
- Within a relationship resource, the usual defaults still apply: its own
|
|
569
391
|
`GET_MANY` reruns after its own `POST`. A `.one`/`.many` `GET` is reset (not
|
|
570
392
|
rerun) by its own `DELETE`, as a root `GET` is; a scoped `GET` is not reset —
|
|
571
|
-
its data
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
by key).
|
|
393
|
+
its data resolves to nothing once the child is dropped. Every relationship
|
|
394
|
+
method accepts `rerunOn`/`dependencies`; what a relation leaves out is its
|
|
395
|
+
parent resource's (`rerunOn` key by key).
|
|
575
396
|
|
|
576
397
|
Splitting a sub-resource out of a parent `PATCH` therefore changes the refresh
|
|
577
398
|
graph: a parent field the parent's own response kept fresh is, once the relation
|
|
578
399
|
is a resource of its own, refreshed only by these rules. When a parent field
|
|
579
|
-
|
|
580
|
-
|
|
400
|
+
depends on a child mutation, say so with `dependencies` rather than relying on a
|
|
401
|
+
rerun that will not happen.
|
|
581
402
|
|
|
582
403
|
## `withParams()`: a scope with reruns of its own
|
|
583
404
|
|
|
@@ -589,8 +410,8 @@ resource read under several fixed questions at once — the admins and the guest
|
|
|
589
410
|
of one `USER`, each list on its own screen. A guest created should not send the
|
|
590
411
|
admin list back to the network.
|
|
591
412
|
|
|
592
|
-
`withParams()`
|
|
593
|
-
|
|
413
|
+
`withParams()` binds params into every action of the resource and gives the
|
|
414
|
+
result a rerun scope of its own:
|
|
594
415
|
|
|
595
416
|
```js
|
|
596
417
|
const ADMIN = USER.withParams({ role: "admin" });
|
|
@@ -603,29 +424,29 @@ await ADMIN.POST({ name: "Bob" }); // reruns ADMIN.GET_MANY, and nothing else
|
|
|
603
424
|
|
|
604
425
|
The isolation is complete, in both directions: a write on `ADMIN` reaches no
|
|
605
426
|
read of `GUEST` and no read of `USER` itself, and a write on `USER` reaches no
|
|
606
|
-
read of either scope
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
`
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
`
|
|
620
|
-
`rerunOn` and `dependencies` for that scope;
|
|
621
|
-
resource's — a `rerunOn` key by key, so
|
|
622
|
-
the resource's `GET_MANY` and
|
|
623
|
-
about something.
|
|
624
|
-
|
|
625
|
-
It is not the tool for a param the user types
|
|
427
|
+
read of either scope. What the scopes still share is the **store**: an item
|
|
428
|
+
updated through one is the same object in the others, so the fields of a row
|
|
429
|
+
change everywhere without a request — only the membership of each list is a
|
|
430
|
+
question its own scope answers.
|
|
431
|
+
|
|
432
|
+
**A scope is the object `withParams()` returned, not its params.** Each call
|
|
433
|
+
builds one of its own: two `USER.withParams({ role: "admin" })` written in two
|
|
434
|
+
files do not reach each other, and neither do
|
|
435
|
+
`USER.withParams({ role: "admin" }).withParams({ gender: "male" })` and
|
|
436
|
+
`USER.withParams({ role: "admin", gender: "male" })`, though they bind the same
|
|
437
|
+
params. Declare each scope once, and import it.
|
|
438
|
+
|
|
439
|
+
It scopes a relation as well, keeping the relation's return contract
|
|
440
|
+
(`TABLE_COLUMNS.withParams({ withTypes: true }).GET_MANY` replaces that table's
|
|
441
|
+
columns). Its second argument takes `rerunOn` and `dependencies` for that scope;
|
|
442
|
+
what it leaves out is the resource's — a `rerunOn` key by key, so
|
|
443
|
+
`{ rerunOn: { GET: ["PATCH"] } }` keeps the resource's `GET_MANY` and
|
|
444
|
+
`GET_RANGE`. Empty params throw: a scope has to be about something.
|
|
445
|
+
|
|
446
|
+
It is not the tool for a param the user types: a search word bound with
|
|
626
447
|
`withParams()` would keep its results from refreshing after a write on the
|
|
627
|
-
resource
|
|
628
|
-
|
|
448
|
+
resource, where a `bindParams` on the resource's own `GET_MANY` stays in the
|
|
449
|
+
rerun graph ([Searching the same collection](#searching-the-same-collection)).
|
|
629
450
|
`withParams()` is for a scope the code fixes once — a role, a status, a tenant.
|
|
630
451
|
|
|
631
452
|
### `dependencies`: rerun after another resource writes
|
|
@@ -642,12 +463,13 @@ const ROLE_WITH_OWNERS = ROLE.withParams(
|
|
|
642
463
|
```
|
|
643
464
|
|
|
644
465
|
Any write on a listed resource (`POST`, `PUT`, `PATCH`, `DELETE`) reruns the
|
|
645
|
-
completed `GET` and `GET_MANY` of the resource that declared it
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
466
|
+
completed `GET` and `GET_MANY` of the resource that declared it, and invalidates
|
|
467
|
+
its `GET_RANGE` readers when the verb is in its `rerunOn.GET_RANGE`. A
|
|
468
|
+
dependency is a facade: `DATABASE` and `DATABASE.withParams(…)` are two of them,
|
|
469
|
+
and only writes on the one listed count. Nothing goes the other way, and nothing
|
|
470
|
+
is inferred — a relation that really is a sub-route of the parent is modelled
|
|
471
|
+
with the relationship methods ([above](#relations-and-autorerun)), never
|
|
472
|
+
declared as a dependency.
|
|
651
473
|
|
|
652
474
|
## A function calling the verb, or the instance
|
|
653
475
|
|
|
@@ -665,30 +487,32 @@ Both forms are real, and what separates them is narrower than it looks:
|
|
|
665
487
|
</Button>
|
|
666
488
|
```
|
|
667
489
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
the same params holds one instance, so a run
|
|
677
|
-
another. Reach for it when something else
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
currently writing it.
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
490
|
+
Either way the button is busy while the write runs and shows the error if it
|
|
491
|
+
fails; either way what runs inside is the same resource run, so the store is
|
|
492
|
+
updated, the actions this mutation invalidates rerun the same, and a failure
|
|
493
|
+
fails the action around it
|
|
494
|
+
([error_handling.md](./error_handling.md#what-a-failing-action-does)). But a
|
|
495
|
+
function is wrapped into an action of its own, one with no verb, and the
|
|
496
|
+
instance carries two things that wrapper does not:
|
|
497
|
+
|
|
498
|
+
- **identity.** Everyone binding the same params holds one instance, so a run
|
|
499
|
+
started in one place is visible from another. Reach for it when something else
|
|
500
|
+
on screen has to see this very run — a user row in a side explorer going
|
|
501
|
+
loading while the main page renames that user, two affordances for the same
|
|
502
|
+
delete that must both go busy at once. Nothing else in the stack answers that
|
|
503
|
+
question: the store holds the data, not who is currently writing it.
|
|
504
|
+
- **the verb** (`meta.verb`). Under a [network policy](./network_policy.md), a
|
|
505
|
+
control holding a write verb's instance — or sitting in a `<Form>` holding one
|
|
506
|
+
— is read-only before the press and says why. The function form is not seen:
|
|
507
|
+
the press is accepted, and the write fails after it with a
|
|
508
|
+
`NetworkPolicyError`.
|
|
509
|
+
|
|
510
|
+
Where neither pays — one affordance, in one place, that nobody else watches, and
|
|
511
|
+
no network policy to hold the write — a `bindParams({ id })` written per row
|
|
512
|
+
builds an identity nothing reads, and a plain function says the gesture more
|
|
513
|
+
directly. Either way, what to READ afterwards is the store, or the instance that
|
|
514
|
+
ran — never the verb, whose signals hold only a run made on the verb itself, with
|
|
515
|
+
no params ([actions.md](./actions.md#the-answer-is-kept-on-the-instance-that-ran)).
|
|
692
516
|
|
|
693
517
|
What genuinely loses what a resource gives is neither of those two, but a
|
|
694
518
|
callback that goes around the verb:
|
|
@@ -702,10 +526,6 @@ callback that goes around the verb:
|
|
|
702
526
|
</Button>
|
|
703
527
|
```
|
|
704
528
|
|
|
705
|
-
Away from a component, where the run is a gesture and not something to render,
|
|
706
|
-
an action is callable: `GAME.DELETE({ id })` is
|
|
707
|
-
`GAME.DELETE.bindParams({ id }).rerun()`.
|
|
708
|
-
|
|
709
529
|
## See also
|
|
710
530
|
|
|
711
531
|
- [list_refresh.md](./list_refresh.md) — what re-runs after a write, and what
|