@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.
@@ -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
- - # Actions System - Declarative Resource Management for Frontend Applications
4
- -
5
- - This module provides a comprehensive system for managing asynchronous resources (API calls, data fetching)
6
- - in a declarative, signal-based architecture. It's designed for complex frontend applications that need
7
- - fine-grained control over loading states, caching, and resource lifecycle management.
8
- -
9
- - ## Core Concepts
10
- -
11
- - ### 🔧 **Action Templates**
12
- - Factory functions that define how to load resources. Templates are pure and reusable.
13
- - ```js
14
-
15
- ```
16
-
17
- - const getUserTemplate = createActionTemplate(async ({ userId }) => {
18
- - const response = await fetch(`/api/users/${userId}`);
19
- - return response.json();
20
- - });
21
- - ```
22
-
23
- ```
24
-
25
- -
26
- - ### 🎯 **Action Instances**
27
- - Stateful objects created from templates with specific parameters. Each unique parameter set
28
- - gets its own cached instance (automatic memoization).
29
- - ```js
30
-
31
- ```
32
-
33
- - const userAction = getUserTemplate.instantiate({ userId: 123 });
34
- - const status = useActionStatus(userAction); // { pending, data, error, ... }
35
- - ```
36
-
37
- ```
38
-
39
- -
40
- - ### 🔄 **Action Proxies**
41
- - Dynamic actions that react to signal changes, automatically reloading when parameters change.
42
- - ```js
43
-
44
- ```
45
-
46
- - const userProxy = createActionProxy(getUserTemplate, {
47
- - userId: userIdSignal, // Signal - reactive
48
- - includeProfile: true // Static - not reactive
49
- - });
50
- - // Automatically reloads when userIdSignal changes
51
- - ```
52
-
53
- ```
54
-
55
- -
56
- - ## Loading States & Lifecycle
57
- -
58
- - ### 📊 **State Management**
59
- - Each action has a well-defined state machine:
60
- - - `IDLE` → `LOADING` → `LOADED` (success)
61
- - - `IDLE` `LOADING` `FAILED` (error)
62
- - - `IDLE` `LOADING` `ABORTED` (cancelled)
63
- -
64
- - ### ⚡ **Load Types**
65
- - - **`.load()`** - Load with user intent (sets `loadRequested: true`)
66
- - - **`.preload()`** - Background loading (sets `loadRequested: false`)
67
- - - **`.reload()`** - Force reload even if already loaded
68
- - - **`.unload()`** - Cancel loading and reset state
69
- -
70
- - ### 🛡️ **Preload Protection**
71
- - Preloaded actions are protected from garbage collection for 5 minutes to ensure
72
- - they remain available for components that may load later (e.g., via dynamic imports).
73
- -
74
- - ## Key Features
75
- -
76
- - ### 🧠 **Intelligent Memoization**
77
- - - Actions with identical parameters share the same instance
78
- - - Uses deep equality comparison with `compareTwoJsValues`
79
- - - Supports `SYMBOL_IDENTITY` for fast recognition of "conceptually same" objects
80
- - - Memory-efficient with automatic garbage collection
81
- -
82
- - ### 🔗 **Parameter Binding & Composition**
83
- - ```js
84
-
85
- ```
86
-
87
- - const baseAction = getUserTemplate.instantiate({ userId: 123 });
88
- - const enrichedAction = baseAction.bindParams({ includeProfile: true });
89
- - // Result: { userId: 123, includeProfile: true }
90
- -
91
- - // Supports objects, primitives, and signals
92
- - const dynamicAction = baseAction.bindParams(filtersSignal);
93
- - ```
94
-
95
- ```
96
-
97
- -
98
- - ### 🎮 **Concurrent Loading Control**
99
- - - Prevents duplicate requests for same resource
100
- - - Smart request deduplication and racing condition handling
101
- - - Coordinated loading/unloading of multiple actions via `updateActions()`
102
- -
103
- - ### 🔧 **Side Effects & Cleanup**
104
- - ```js
105
-
106
- ```
107
-
108
- - const actionTemplate = createActionTemplate(callback, {
109
- - sideEffect: (params, loadParams) => {
110
- - // Setup logic (analytics, subscriptions, etc.)
111
- - return () => {
112
- - // Cleanup logic - called on unload/abort
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