@jsenv/navi 0.29.20 → 0.29.22
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 +2 -0
- package/dist/jsenv_navi.js +2640 -182
- package/dist/jsenv_navi.js.map +279 -85
- package/docs/AI_INSTRUCTIONS.md +22 -0
- package/docs/actions.md +117 -250
- package/docs/drag_to_travel.md +371 -0
- package/docs/resource.md +88 -0
- package/docs/scroll.md +1 -1
- package/package.json +2 -2
package/docs/AI_INSTRUCTIONS.md
CHANGED
|
@@ -49,6 +49,9 @@ consistency across the app, not from any single call site.
|
|
|
49
49
|
values navi's own popups/bars/tables use. Read it before writing a `z-index`.
|
|
50
50
|
- `docs/MOBILE_LAYOUT_PITFALLS.md` — mobile-specific layout gotchas (viewport
|
|
51
51
|
units, virtual keyboard, safe areas).
|
|
52
|
+
- `src/nav/route_ui.md` — routes as UI: layout/section patterns, and
|
|
53
|
+
`RouteTravel` (swiping between pages that are URLs — the tabs of a page,
|
|
54
|
+
driven by thumb, wheel, or a link).
|
|
52
55
|
- Source code on GitHub: https://github.com/jsenv/core/tree/main/packages/frontend/navi/src
|
|
53
56
|
— worth checking if the JSDoc on an export genuinely doesn't answer your
|
|
54
57
|
question.
|
|
@@ -73,6 +76,25 @@ consistency across the app, not from any single call site.
|
|
|
73
76
|
- **Field components** (`Input`, `Select`, `Checkbox`, etc.) take an `action`
|
|
74
77
|
prop to respond to interaction — this is the standard wiring, not
|
|
75
78
|
`onChange` + manual state.
|
|
79
|
+
- **View transitions**: navi components animate their own changes
|
|
80
|
+
(`itemTransition` on `List`, `RouteTravel` for routes) and never decide for
|
|
81
|
+
the whole document. Two things are the application's call, not navi's:
|
|
82
|
+
- a `view-transition-name` must be unique per document (a duplicate aborts
|
|
83
|
+
the transition) — scope any name your app adds;
|
|
84
|
+
- list/grid transitions rely on nested groups
|
|
85
|
+
(`view-transition-group: contain`, Chrome/Edge 140+). On browsers without
|
|
86
|
+
it nothing is named, so an unconditional `startViewTransition` falls back
|
|
87
|
+
to a full-page cross-fade. If that fade is unwanted in your app, the app —
|
|
88
|
+
not a component — writes:
|
|
89
|
+
`@supports not (view-transition-group: contain) { :root { view-transition-name: none } }`.
|
|
90
|
+
Only the application knows whether a page-wide fade is a decent default or
|
|
91
|
+
a glitch there.
|
|
92
|
+
- a bonus that costs nothing: any element given its own
|
|
93
|
+
`view-transition-name` (a tab underline, a header) is animated by the
|
|
94
|
+
browser from where it was to where it is during any transition — `Nav`
|
|
95
|
+
does this for its current-tab indicator automatically
|
|
96
|
+
(`currentIndicator`), which is why the bar follows a `RouteTravel` swipe
|
|
97
|
+
with no wiring.
|
|
76
98
|
|
|
77
99
|
If unsure which export solves a problem, check `README.md` first — the
|
|
78
100
|
`src/` tree on GitHub is there too if a specific export's own JSDoc doesn't
|
package/docs/actions.md
CHANGED
|
@@ -1,250 +1,117 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
-
|
|
114
|
-
|
|
115
|
-
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
-
|
|
121
|
-
- ## Usage Patterns
|
|
122
|
-
-
|
|
123
|
-
- ### 🏗️ **Basic Resource Loading**
|
|
124
|
-
- ```js
|
|
125
|
-
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
- const getUserAction = createActionTemplate(async ({ userId }) => {
|
|
129
|
-
- return await api.getUser(userId);
|
|
130
|
-
- });
|
|
131
|
-
-
|
|
132
|
-
- // In component
|
|
133
|
-
- const userAction = getUserAction.instantiate({ userId: 123 });
|
|
134
|
-
- const { pending, data, error } = useActionStatus(userAction);
|
|
135
|
-
-
|
|
136
|
-
- useEffect(() => {
|
|
137
|
-
- userAction.load();
|
|
138
|
-
- }, []);
|
|
139
|
-
- ```
|
|
140
|
-
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
-
|
|
144
|
-
- ### 🔄 **Reactive Data Loading**
|
|
145
|
-
- ```js
|
|
146
|
-
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
- const searchProxy = createActionProxy(searchTemplate, {
|
|
150
|
-
- query: searchSignal,
|
|
151
|
-
- filters: filtersSignal
|
|
152
|
-
- });
|
|
153
|
-
- // Automatically reloads when signals change
|
|
154
|
-
- ```
|
|
155
|
-
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
-
|
|
159
|
-
- ### 📋 **Master-Detail Pattern**
|
|
160
|
-
- ```js
|
|
161
|
-
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
- const usersAction = getUsersTemplate.instantiate();
|
|
165
|
-
- const selectedUser = signal(null);
|
|
166
|
-
-
|
|
167
|
-
- const userDetailsProxy = createActionProxy(getUserTemplate, {
|
|
168
|
-
- userId: computed(() => selectedUser.value?.id)
|
|
169
|
-
- });
|
|
170
|
-
- ```
|
|
171
|
-
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
-
|
|
175
|
-
- ### 🏃 **Progressive Loading**
|
|
176
|
-
- ```js
|
|
177
|
-
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
- // Preload on hover, load on click
|
|
181
|
-
- <button
|
|
182
|
-
- onMouseEnter={() => action.preload()}
|
|
183
|
-
- onClick={() => action.load()}
|
|
184
|
-
- >
|
|
185
|
-
- Load User
|
|
186
|
-
- </button>
|
|
187
|
-
- ```
|
|
188
|
-
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
-
|
|
192
|
-
- ## Advanced Features
|
|
193
|
-
-
|
|
194
|
-
- ### 🎭 **Custom Data Transformation**
|
|
195
|
-
- ```js
|
|
196
|
-
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
- const actionTemplate = createActionTemplate(fetchUser, {
|
|
200
|
-
- computedDataSignal: computed(() => {
|
|
201
|
-
- const rawData = dataSignal.value;
|
|
202
|
-
- return rawData ? transformUser(rawData) : null;
|
|
203
|
-
- })
|
|
204
|
-
- });
|
|
205
|
-
- ```
|
|
206
|
-
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
-
|
|
210
|
-
- ### 🎨 **Async Rendering Support**
|
|
211
|
-
- ```js
|
|
212
|
-
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
- const actionTemplate = createActionTemplate(fetchData, {
|
|
216
|
-
- renderLoadedAsync: async () => {
|
|
217
|
-
- const { UserComponent } = await import('./UserComponent.js');
|
|
218
|
-
- return (user) => <UserComponent user={user} />;
|
|
219
|
-
- }
|
|
220
|
-
- });
|
|
221
|
-
- ```
|
|
222
|
-
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
-
|
|
226
|
-
- ### 🛠️ **Debugging & Observability**
|
|
227
|
-
- Built-in debug mode with detailed logging of state transitions, loading coordination,
|
|
228
|
-
- and memory management. Enable with `debug = true`.
|
|
229
|
-
-
|
|
230
|
-
- ## Integration Points
|
|
231
|
-
-
|
|
232
|
-
- - **Signals**: Built on @preact/signals for reactive state management
|
|
233
|
-
- - **Navigation**: Integrates with navigation systems for route-based loading
|
|
234
|
-
- - **Components**: Use `useActionStatus()` hook for component integration
|
|
235
|
-
- - **Memory Management**: Automatic cleanup with WeakMap-based private properties
|
|
236
|
-
-
|
|
237
|
-
- ## Performance Characteristics
|
|
238
|
-
-
|
|
239
|
-
- - **Memory Efficient**: Weak references prevent memory leaks
|
|
240
|
-
- - **Request Deduplication**: Identical requests are automatically merged
|
|
241
|
-
- - **Minimal Re-renders**: Signal-based updates only trigger when data actually changes
|
|
242
|
-
- - **Lazy Loading**: Actions only created when needed, with intelligent memoization
|
|
243
|
-
-
|
|
244
|
-
- This system is particularly well-suited for:
|
|
245
|
-
- - SPAs with complex data fetching requirements
|
|
246
|
-
- - Applications needing fine-grained loading state control
|
|
247
|
-
- - Systems requiring request coordination and deduplication
|
|
248
|
-
- - Progressive loading and preloading scenarios
|
|
249
|
-
- - Master-detail interfaces with dynamic parameter binding
|
|
250
|
-
\*/
|
|
1
|
+
# Actions
|
|
2
|
+
|
|
3
|
+
An action is an async callback plus the state of its last run, held in signals:
|
|
4
|
+
running or not, the error it failed with, the data it produced. Components read
|
|
5
|
+
that state instead of keeping their own.
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
import { createAction } from "@jsenv/navi";
|
|
9
|
+
|
|
10
|
+
const getUser = createAction(async ({ id }, { signal }) => {
|
|
11
|
+
const response = await fetch(`/users/${id}`, { signal });
|
|
12
|
+
return response.json();
|
|
13
|
+
});
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The callback receives `(params, { reason, event, signal, isPrerun })`. `signal`
|
|
17
|
+
is aborted when the run is called off — pass it to `fetch`.
|
|
18
|
+
|
|
19
|
+
`resource()` creates one action per REST callback rather than having you write
|
|
20
|
+
them by hand — see [resource.md](./resource.md).
|
|
21
|
+
|
|
22
|
+
## Params: `bindParams`, and calling the action
|
|
23
|
+
|
|
24
|
+
`createAction` gives one action for the callback; the params make instances of
|
|
25
|
+
it, each with its own state:
|
|
26
|
+
|
|
27
|
+
```js
|
|
28
|
+
const getUser123 = getUser.bindParams({ id: 123 });
|
|
29
|
+
await getUser123.run();
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Two `bindParams` with equal params give **the same instance** (deep equality),
|
|
33
|
+
which is what makes state shared between two components asking for the same
|
|
34
|
+
thing, and what deduplicates their requests.
|
|
35
|
+
|
|
36
|
+
An action is callable, and calling it is the short way to bind and run in one
|
|
37
|
+
go:
|
|
38
|
+
|
|
39
|
+
```js
|
|
40
|
+
getUser({ id: 123 }); // getUser.bindParams({ id: 123 }).rerun()
|
|
41
|
+
getUser(); // getUser.rerun()
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Use it wherever a run is a **gesture** — a click handler, an event, a step in a
|
|
45
|
+
flow — where the params are known at that moment and the run is the point:
|
|
46
|
+
|
|
47
|
+
```js
|
|
48
|
+
const deleteGame = (game) => GAME.DELETE({ id: game.id });
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Use `bindParams` when what you need is the **instance**, not the run: to hand it
|
|
52
|
+
to a component that will run it and read its state
|
|
53
|
+
(`<Button action={GAME.DELETE.bindParams({ id })}>`), or to keep a handle on it.
|
|
54
|
+
Note that calling the action `rerun()`s it — the run happens even if that
|
|
55
|
+
instance already holds data, which is what you want from a gesture and not what
|
|
56
|
+
you want from a component asking for data.
|
|
57
|
+
|
|
58
|
+
Params may be signals, and then the action follows them:
|
|
59
|
+
|
|
60
|
+
```js
|
|
61
|
+
const userAction = getUser.bindParams({ id: userIdSignal });
|
|
62
|
+
// a new params value reruns it
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Running: `run`, `rerun`, `prerun`, `reset`
|
|
66
|
+
|
|
67
|
+
| Method | Does |
|
|
68
|
+
| ---------- | ------------------------------------------------------------------------ |
|
|
69
|
+
| `run()` | Asks for the data. An action already running or completed has it: no-op. |
|
|
70
|
+
| `rerun()` | Runs again whatever state it is in — a refresh, an explicit "check now". |
|
|
71
|
+
| `prerun()` | Same as `run()`, in the background: nothing asked for it on screen yet. |
|
|
72
|
+
| `reset()` | Aborts what is running and puts the action back to idle, data and all. |
|
|
73
|
+
| `abort()` | Calls off the run in flight, keeping the data it had. |
|
|
74
|
+
|
|
75
|
+
## Reading an action
|
|
76
|
+
|
|
77
|
+
```jsx
|
|
78
|
+
const [user] = useAsyncData(userAction);
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`useAsyncData` suspends until the data is there and throws on failure, leaving
|
|
82
|
+
both to the nearest `<Loading>` and `<ErrorBoundary>`; pass `{ loading: true }`
|
|
83
|
+
or `{ error: true }` to handle either inside the component (stale data stays
|
|
84
|
+
available while a rerun is in flight). `useActionStatus(action)` gives the whole
|
|
85
|
+
state at once — `{ idle, loading, completed, aborted, error, data, params }` —
|
|
86
|
+
for a component that needs to look at it rather than render it.
|
|
87
|
+
|
|
88
|
+
Controls take the action itself and wire the rest: `<Button action>` runs it on
|
|
89
|
+
click, shows its loading state, and puts its error where the user can see it.
|
|
90
|
+
That is the reason to pass an action instance rather than an arrow calling the
|
|
91
|
+
callback:
|
|
92
|
+
|
|
93
|
+
```jsx
|
|
94
|
+
// ✓ loading, error and disabled states come from the action
|
|
95
|
+
<Button action={GAME_CANDIDATES.POST.bindParams({ id: game.id, user_id })}>
|
|
96
|
+
Accept
|
|
97
|
+
</Button>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Reruns
|
|
101
|
+
|
|
102
|
+
Actions do not stay stale on their own: a resource's `POST` reruns the
|
|
103
|
+
`GET_MANY` that lists it, a `DELETE` resets the `GET` that loaded the item, and
|
|
104
|
+
`dependencies`/`rerunOn` declare the rest. What re-runs after a write, and what
|
|
105
|
+
stays on screen while it does, is in [list_refresh.md](./list_refresh.md) and
|
|
106
|
+
[resource_dependencies.md](./resource_dependencies.md).
|
|
107
|
+
|
|
108
|
+
`rerunActions(actions)` / `updateActions(actions)` drive several at once (route
|
|
109
|
+
changes do exactly that).
|
|
110
|
+
|
|
111
|
+
## See also
|
|
112
|
+
|
|
113
|
+
- [resource.md](./resource.md) — actions created from REST callbacks, and the
|
|
114
|
+
store behind them
|
|
115
|
+
- [resource_with_params.md](./resource_with_params.md) — `withParams()` and
|
|
116
|
+
isolated rerun scopes
|
|
117
|
+
- [list_refresh.md](./list_refresh.md) — what a write refreshes
|