@jsenv/navi 0.0.1

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.
Files changed (123) hide show
  1. package/index.js +51 -0
  2. package/package.json +38 -0
  3. package/src/action_private_properties.js +11 -0
  4. package/src/action_proxy_test.html +353 -0
  5. package/src/action_run_states.js +5 -0
  6. package/src/actions.js +1377 -0
  7. package/src/browser_integration/browser_integration.js +191 -0
  8. package/src/browser_integration/document_back_and_forward.js +17 -0
  9. package/src/browser_integration/document_loading_signal.js +100 -0
  10. package/src/browser_integration/document_state_signal.js +9 -0
  11. package/src/browser_integration/document_url_signal.js +9 -0
  12. package/src/browser_integration/use_is_visited.js +19 -0
  13. package/src/browser_integration/via_history.js +199 -0
  14. package/src/browser_integration/via_navigation.js +168 -0
  15. package/src/components/action_execution/form_context.js +8 -0
  16. package/src/components/action_execution/render_actionable_component.jsx +27 -0
  17. package/src/components/action_execution/use_action.js +330 -0
  18. package/src/components/action_execution/use_execute_action.js +161 -0
  19. package/src/components/action_renderer.jsx +136 -0
  20. package/src/components/collect_form_element_values.js +79 -0
  21. package/src/components/demos/0_button_demo.html +155 -0
  22. package/src/components/demos/1_checkbox_demo.html +257 -0
  23. package/src/components/demos/2_input_textual_demo.html +354 -0
  24. package/src/components/demos/3_radio_demo.html +222 -0
  25. package/src/components/demos/4_select_demo.html +104 -0
  26. package/src/components/demos/5_list_scrollable_demo.html +153 -0
  27. package/src/components/demos/action/0_button_demo.html +204 -0
  28. package/src/components/demos/action/10_shortcuts_demo.html +189 -0
  29. package/src/components/demos/action/11_nested_shortcuts_demo.html +401 -0
  30. package/src/components/demos/action/1_input_text_demo.html +461 -0
  31. package/src/components/demos/action/2_form_multiple.html +303 -0
  32. package/src/components/demos/action/3_details_demo.html +172 -0
  33. package/src/components/demos/action/4_input_checkbox_demo.html +611 -0
  34. package/src/components/demos/action/6_checkbox_list_demo.html +109 -0
  35. package/src/components/demos/action/7_radio_list_demo.html +217 -0
  36. package/src/components/demos/action/8_editable_text_demo.html +442 -0
  37. package/src/components/demos/action/9_link_demo.html +172 -0
  38. package/src/components/demos/demo.md +0 -0
  39. package/src/components/demos/route/basic/basic.html +14 -0
  40. package/src/components/demos/route/basic/basic_route_demo.jsx +224 -0
  41. package/src/components/demos/route/multi/multi.html +14 -0
  42. package/src/components/demos/route/multi/multi_route_demo.jsx +277 -0
  43. package/src/components/details/details.jsx +248 -0
  44. package/src/components/details/summary_marker.jsx +141 -0
  45. package/src/components/editable_text/editable_text.jsx +96 -0
  46. package/src/components/error_boundary_context.js +9 -0
  47. package/src/components/form.jsx +144 -0
  48. package/src/components/input/button.jsx +333 -0
  49. package/src/components/input/checkbox_list.jsx +294 -0
  50. package/src/components/input/field.jsx +61 -0
  51. package/src/components/input/field_css.js +118 -0
  52. package/src/components/input/input.jsx +15 -0
  53. package/src/components/input/input_checkbox.jsx +370 -0
  54. package/src/components/input/input_radio.jsx +299 -0
  55. package/src/components/input/input_textual.jsx +338 -0
  56. package/src/components/input/radio_list.jsx +283 -0
  57. package/src/components/input/select.jsx +273 -0
  58. package/src/components/input/use_form_event.js +20 -0
  59. package/src/components/input/use_on_change.js +12 -0
  60. package/src/components/link/link.jsx +291 -0
  61. package/src/components/loader/loader_background.jsx +324 -0
  62. package/src/components/loader/loading_spinner.jsx +68 -0
  63. package/src/components/loader/network_speed.js +83 -0
  64. package/src/components/loader/rectangle_loading.jsx +225 -0
  65. package/src/components/route.jsx +15 -0
  66. package/src/components/selection/selection.js +5 -0
  67. package/src/components/selection/selection_context.jsx +262 -0
  68. package/src/components/shortcut/os.js +9 -0
  69. package/src/components/shortcut/shortcut_context.jsx +390 -0
  70. package/src/components/use_action_events.js +37 -0
  71. package/src/components/use_auto_focus.js +43 -0
  72. package/src/components/use_debounce_true.js +31 -0
  73. package/src/components/use_focus_group.js +19 -0
  74. package/src/components/use_initial_value.js +104 -0
  75. package/src/components/use_is_visited.js +19 -0
  76. package/src/components/use_ref_array.js +38 -0
  77. package/src/components/use_signal_sync.js +50 -0
  78. package/src/components/use_state_array.js +40 -0
  79. package/src/docs/actions.md +228 -0
  80. package/src/docs/demos/resource/action_status.jsx +42 -0
  81. package/src/docs/demos/resource/demo.md +1 -0
  82. package/src/docs/demos/resource/resource_demo_0.html +84 -0
  83. package/src/docs/demos/resource/resource_demo_10_post_gc.html +364 -0
  84. package/src/docs/demos/resource/resource_demo_11_describe_many.html +362 -0
  85. package/src/docs/demos/resource/resource_demo_2.html +173 -0
  86. package/src/docs/demos/resource/resource_demo_3_filtered_users.html +415 -0
  87. package/src/docs/demos/resource/resource_demo_4_details.html +284 -0
  88. package/src/docs/demos/resource/resource_demo_5_renderer_lazy.html +115 -0
  89. package/src/docs/demos/resource/resource_demo_6_gc.html +217 -0
  90. package/src/docs/demos/resource/resource_demo_7_child_gc.html +240 -0
  91. package/src/docs/demos/resource/resource_demo_8_proxy_gc.html +319 -0
  92. package/src/docs/demos/resource/resource_demo_9_describe_one.html +472 -0
  93. package/src/docs/demos/resource/tata.jsx +3 -0
  94. package/src/docs/demos/resource/toto.jsx +3 -0
  95. package/src/docs/demos/user_nav/user_nav.html +12 -0
  96. package/src/docs/demos/user_nav/user_nav.jsx +330 -0
  97. package/src/docs/resource_dependencies.md +103 -0
  98. package/src/docs/resource_with_params.md +80 -0
  99. package/src/notes.md +13 -0
  100. package/src/route/route.js +518 -0
  101. package/src/route/route.test.html +228 -0
  102. package/src/store/array_signal_store.js +537 -0
  103. package/src/store/local_storage_signal.js +17 -0
  104. package/src/store/resource_graph.js +1303 -0
  105. package/src/store/tests/resource_graph_autoreload_demo.html +12 -0
  106. package/src/store/tests/resource_graph_autoreload_demo.jsx +964 -0
  107. package/src/store/tests/resource_graph_dependencies.test.js +95 -0
  108. package/src/store/value_in_local_storage.js +187 -0
  109. package/src/symbol_object_signal.js +1 -0
  110. package/src/use_action_data.js +10 -0
  111. package/src/use_action_status.js +47 -0
  112. package/src/utils/add_many_event_listeners.js +15 -0
  113. package/src/utils/array_add_remove.js +61 -0
  114. package/src/utils/array_signal.js +15 -0
  115. package/src/utils/compare_two_js_values.js +172 -0
  116. package/src/utils/execute_with_cleanup.js +21 -0
  117. package/src/utils/get_caller_info.js +85 -0
  118. package/src/utils/iterable_weak_set.js +62 -0
  119. package/src/utils/js_value_weak_map.js +162 -0
  120. package/src/utils/js_value_weak_map_demo.html +690 -0
  121. package/src/utils/merge_two_js_values.js +53 -0
  122. package/src/utils/stringify_for_display.js +150 -0
  123. package/src/utils/weak_effect.js +48 -0
@@ -0,0 +1,50 @@
1
+ import { useSignal } from "@preact/signals";
2
+ import { useRef } from "preact/hooks";
3
+
4
+ /**
5
+ * Creates a signal that stays synchronized with an external value,
6
+ * only updating the signal when the value actually changes.
7
+ *
8
+ * This hook solves a common reactive UI pattern where:
9
+ * 1. A signal controls a UI element (like an input field)
10
+ * 2. The UI element can be modified by user interaction
11
+ * 3. When the external "source of truth" changes, it should take precedence
12
+ *
13
+ * @param {any} value - The external value to sync with (the "source of truth")
14
+ * @param {any} [initialValue] - Optional initial value for the signal (defaults to value)
15
+ * @returns {Signal} A signal that tracks the external value but allows temporary local changes
16
+ *
17
+ * @example
18
+ * const FileNameEditor = ({ file }) => {
19
+ * // Signal stays in sync with file.name, but allows user editing
20
+ * const nameSignal = useSignalSync(file.name);
21
+ *
22
+ * return (
23
+ * <EditableText
24
+ * valueSignal={nameSignal} // User can edit this
25
+ * action={renameFileAction} // Saves changes
26
+ * />
27
+ * );
28
+ * };
29
+ *
30
+ * // Scenario:
31
+ * // 1. file.name = "doc.txt", nameSignal.value = "doc.txt"
32
+ * // 2. User types "report" -> nameSignal.value = "report.txt"
33
+ * // 3. External update: file.name = "shared-doc.txt"
34
+ * // 4. Next render: nameSignal.value = "shared-doc.txt" (model wins!)
35
+ *
36
+ */
37
+
38
+ export const useSignalSync = (value, initialValue = value) => {
39
+ const signal = useSignal(initialValue);
40
+ const previousValueRef = useRef(value);
41
+
42
+ // Only update signal when external value actually changes
43
+ // This preserves user input between external changes
44
+ if (previousValueRef.current !== value) {
45
+ previousValueRef.current = value;
46
+ signal.value = value; // Model takes precedence
47
+ }
48
+
49
+ return signal;
50
+ };
@@ -0,0 +1,40 @@
1
+ import { useCallback, useState } from "preact/hooks";
2
+ import { addIntoArray, removeFromArray } from "../utils/array_add_remove.js";
3
+ import {
4
+ resolveInitialValue,
5
+ useExternalValueSync,
6
+ } from "./use_initial_value.js";
7
+
8
+ export const useStateArray = (
9
+ externalValue = [],
10
+ fallbackValue,
11
+ defaultValue = [],
12
+ ) => {
13
+ const initialValue = resolveInitialValue(
14
+ externalValue,
15
+ fallbackValue,
16
+ defaultValue,
17
+ );
18
+ const [array, setArray] = useState(initialValue);
19
+
20
+ // Sync external value changes
21
+ useExternalValueSync(externalValue, defaultValue, setArray, "state_array");
22
+
23
+ const add = useCallback((valueToAdd) => {
24
+ setArray((array) => {
25
+ return addIntoArray(array, valueToAdd);
26
+ });
27
+ }, []);
28
+
29
+ const remove = useCallback((valueToRemove) => {
30
+ setArray((array) => {
31
+ return removeFromArray(array, valueToRemove);
32
+ });
33
+ }, []);
34
+
35
+ const reset = useCallback(() => {
36
+ setArray(initialValue);
37
+ }, [initialValue]);
38
+
39
+ return [array, add, remove, reset];
40
+ };
@@ -0,0 +1,228 @@
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
+ - const getUserTemplate = createActionTemplate(async ({ userId }) => {
17
+ - const response = await fetch(`/api/users/${userId}`);
18
+ - return response.json();
19
+ - });
20
+ - ```
21
+
22
+ ```
23
+ -
24
+ - ### 🎯 **Action Instances**
25
+ - Stateful objects created from templates with specific parameters. Each unique parameter set
26
+ - gets its own cached instance (automatic memoization).
27
+ - ```js
28
+
29
+ ```
30
+ - const userAction = getUserTemplate.instantiate({ userId: 123 });
31
+ - const status = useActionStatus(userAction); // { pending, data, error, ... }
32
+ - ```
33
+
34
+ ```
35
+ -
36
+ - ### 🔄 **Action Proxies**
37
+ - Dynamic actions that react to signal changes, automatically reloading when parameters change.
38
+ - ```js
39
+
40
+ ```
41
+ - const userProxy = createActionProxy(getUserTemplate, {
42
+ - userId: userIdSignal, // Signal - reactive
43
+ - includeProfile: true // Static - not reactive
44
+ - });
45
+ - // Automatically reloads when userIdSignal changes
46
+ - ```
47
+
48
+ ```
49
+ -
50
+ - ## Loading States & Lifecycle
51
+ -
52
+ - ### 📊 **State Management**
53
+ - Each action has a well-defined state machine:
54
+ - - `IDLE` → `LOADING` → `LOADED` (success)
55
+ - - `IDLE` → `LOADING` → `FAILED` (error)
56
+ - - `IDLE` → `LOADING` → `ABORTED` (cancelled)
57
+ -
58
+ - ### ⚡ **Load Types**
59
+ - - **`.load()`** - Load with user intent (sets `loadRequested: true`)
60
+ - - **`.preload()`** - Background loading (sets `loadRequested: false`)
61
+ - - **`.reload()`** - Force reload even if already loaded
62
+ - - **`.unload()`** - Cancel loading and reset state
63
+ -
64
+ - ### 🛡️ **Preload Protection**
65
+ - Preloaded actions are protected from garbage collection for 5 minutes to ensure
66
+ - they remain available for components that may load later (e.g., via dynamic imports).
67
+ -
68
+ - ## Key Features
69
+ -
70
+ - ### 🧠 **Intelligent Memoization**
71
+ - - Actions with identical parameters share the same instance
72
+ - - Uses deep equality comparison with `compareTwoJsValues`
73
+ - - Supports `SYMBOL_IDENTITY` for fast recognition of "conceptually same" objects
74
+ - - Memory-efficient with automatic garbage collection
75
+ -
76
+ - ### 🔗 **Parameter Binding & Composition**
77
+ - ```js
78
+
79
+ ```
80
+ - const baseAction = getUserTemplate.instantiate({ userId: 123 });
81
+ - const enrichedAction = baseAction.bindParams({ includeProfile: true });
82
+ - // Result: { userId: 123, includeProfile: true }
83
+ -
84
+ - // Supports objects, primitives, and signals
85
+ - const dynamicAction = baseAction.bindParams(filtersSignal);
86
+ - ```
87
+
88
+ ```
89
+ -
90
+ - ### 🎮 **Concurrent Loading Control**
91
+ - - Prevents duplicate requests for same resource
92
+ - - Smart request deduplication and racing condition handling
93
+ - - Coordinated loading/unloading of multiple actions via `updateActions()`
94
+ -
95
+ - ### 🔧 **Side Effects & Cleanup**
96
+ - ```js
97
+
98
+ ```
99
+ - const actionTemplate = createActionTemplate(callback, {
100
+ - sideEffect: (params, loadParams) => {
101
+ - // Setup logic (analytics, subscriptions, etc.)
102
+ - return () => {
103
+ - // Cleanup logic - called on unload/abort
104
+ - };
105
+ - }
106
+ - });
107
+ - ```
108
+
109
+ ```
110
+ -
111
+ - ## Usage Patterns
112
+ -
113
+ - ### 🏗️ **Basic Resource Loading**
114
+ - ```js
115
+
116
+ ```
117
+ - const getUserAction = createActionTemplate(async ({ userId }) => {
118
+ - return await api.getUser(userId);
119
+ - });
120
+ -
121
+ - // In component
122
+ - const userAction = getUserAction.instantiate({ userId: 123 });
123
+ - const { pending, data, error } = useActionStatus(userAction);
124
+ -
125
+ - useEffect(() => {
126
+ - userAction.load();
127
+ - }, []);
128
+ - ```
129
+
130
+ ```
131
+ -
132
+ - ### 🔄 **Reactive Data Loading**
133
+ - ```js
134
+
135
+ ```
136
+ - const searchProxy = createActionProxy(searchTemplate, {
137
+ - query: searchSignal,
138
+ - filters: filtersSignal
139
+ - });
140
+ - // Automatically reloads when signals change
141
+ - ```
142
+
143
+ ```
144
+ -
145
+ - ### 📋 **Master-Detail Pattern**
146
+ - ```js
147
+
148
+ ```
149
+ - const usersAction = getUsersTemplate.instantiate();
150
+ - const selectedUser = signal(null);
151
+ -
152
+ - const userDetailsProxy = createActionProxy(getUserTemplate, {
153
+ - userId: computed(() => selectedUser.value?.id)
154
+ - });
155
+ - ```
156
+
157
+ ```
158
+ -
159
+ - ### 🏃 **Progressive Loading**
160
+ - ```js
161
+
162
+ ```
163
+ - // Preload on hover, load on click
164
+ - <button
165
+ - onMouseEnter={() => action.preload()}
166
+ - onClick={() => action.load()}
167
+ - >
168
+ - Load User
169
+ - </button>
170
+ - ```
171
+
172
+ ```
173
+ -
174
+ - ## Advanced Features
175
+ -
176
+ - ### 🎭 **Custom Data Transformation**
177
+ - ```js
178
+
179
+ ```
180
+ - const actionTemplate = createActionTemplate(fetchUser, {
181
+ - computedDataSignal: computed(() => {
182
+ - const rawData = dataSignal.value;
183
+ - return rawData ? transformUser(rawData) : null;
184
+ - })
185
+ - });
186
+ - ```
187
+
188
+ ```
189
+ -
190
+ - ### 🎨 **Async Rendering Support**
191
+ - ```js
192
+
193
+ ```
194
+ - const actionTemplate = createActionTemplate(fetchData, {
195
+ - renderLoadedAsync: async () => {
196
+ - const { UserComponent } = await import('./UserComponent.js');
197
+ - return (user) => <UserComponent user={user} />;
198
+ - }
199
+ - });
200
+ - ```
201
+
202
+ ```
203
+ -
204
+ - ### 🛠️ **Debugging & Observability**
205
+ - Built-in debug mode with detailed logging of state transitions, loading coordination,
206
+ - and memory management. Enable with `debug = true`.
207
+ -
208
+ - ## Integration Points
209
+ -
210
+ - - **Signals**: Built on @preact/signals for reactive state management
211
+ - - **Navigation**: Integrates with navigation systems for route-based loading
212
+ - - **Components**: Use `useActionStatus()` hook for component integration
213
+ - - **Memory Management**: Automatic cleanup with WeakMap-based private properties
214
+ -
215
+ - ## Performance Characteristics
216
+ -
217
+ - - **Memory Efficient**: Weak references prevent memory leaks
218
+ - - **Request Deduplication**: Identical requests are automatically merged
219
+ - - **Minimal Re-renders**: Signal-based updates only trigger when data actually changes
220
+ - - **Lazy Loading**: Actions only created when needed, with intelligent memoization
221
+ -
222
+ - This system is particularly well-suited for:
223
+ - - SPAs with complex data fetching requirements
224
+ - - Applications needing fine-grained loading state control
225
+ - - Systems requiring request coordination and deduplication
226
+ - - Progressive loading and preloading scenarios
227
+ - - Master-detail interfaces with dynamic parameter binding
228
+ \*/
@@ -0,0 +1,42 @@
1
+ import { useActionStatus } from "@jsenv/navi";
2
+ import { stringifyForDisplay } from "../../../utils/stringify_for_display.js";
3
+
4
+ export const ActionStatus = ({ action, name = action.name }) => {
5
+ const { idle, preloaded, pending, params, error, aborted, data } =
6
+ useActionStatus(action);
7
+
8
+ return (
9
+ <fieldset>
10
+ <legend>{name}</legend>
11
+
12
+ <div style="display: flex; flex-direction: column; gap: 5px;">
13
+ <div>
14
+ <span>
15
+ <span>loading state:</span>{" "}
16
+ <strong>
17
+ {idle
18
+ ? "idle"
19
+ : aborted
20
+ ? "aborted"
21
+ : error
22
+ ? "error"
23
+ : pending
24
+ ? "pending"
25
+ : preloaded
26
+ ? "preloaded"
27
+ : "loaded"}
28
+ </strong>
29
+ </span>
30
+ </div>
31
+ <div style="display: flex; gap: 5px;">
32
+ <span>params: </span>
33
+ <pre style="margin: 0">{stringifyForDisplay(params)}</pre>
34
+ </div>
35
+ <div style="display: flex; gap: 5px;">
36
+ <span>data: </span>
37
+ <pre style="margin: 0">{stringifyForDisplay(data)}</pre>
38
+ </div>
39
+ </div>
40
+ </fieldset>
41
+ );
42
+ };
@@ -0,0 +1 @@
1
+ ## Prochain truc a faire:
@@ -0,0 +1,84 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" href="data:," />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>Action demo 0</title>
8
+ </head>
9
+ <body>
10
+ <div id="root" style="position: relative; width: 200px"></div>
11
+
12
+ <script type="module" jsenv-type="module/jsx">
13
+ /**
14
+ * Ici on teste juste que d'ouvrir le details
15
+ * charge l'action et que la donée est affichée
16
+ * Si on reload la page, le details est open et l'action est lancée
17
+ */
18
+
19
+ import { render } from "preact";
20
+ import { signal, effect } from "@preact/signals";
21
+ import { useActionStatus, resource } from "@jsenv/navi";
22
+
23
+ const USER = resource("user", {
24
+ idKey: "name",
25
+ GET_MANY: () => {
26
+ return [
27
+ {
28
+ name: "Alice",
29
+ },
30
+ {
31
+ name: "bob",
32
+ },
33
+ {
34
+ name: "Charlie",
35
+ },
36
+ ];
37
+ },
38
+ });
39
+ const openedSignal = signal(
40
+ localStorage.getItem("user_opened") === "true",
41
+ );
42
+ effect(() => {
43
+ const opened = openedSignal.value;
44
+ if (opened) {
45
+ localStorage.setItem("user_opened", "true");
46
+ } else {
47
+ localStorage.removeItem("user_opened");
48
+ }
49
+ });
50
+ if (openedSignal.peek()) {
51
+ USER.GET_MANY.preload();
52
+ }
53
+
54
+ // eslint-disable-next-line no-unused-vars
55
+ const App = () => {
56
+ const { data } = useActionStatus(USER.GET_MANY);
57
+
58
+ return (
59
+ <div>
60
+ <details
61
+ open={openedSignal.value}
62
+ onToggle={(e) => {
63
+ if (e.target.open) {
64
+ openedSignal.value = true;
65
+ USER.GET_MANY.load();
66
+ } else {
67
+ openedSignal.value = false;
68
+ USER.GET_MANY.abort();
69
+ }
70
+ }}
71
+ >
72
+ <summary>Users</summary>
73
+ {data.map(({ name }) => {
74
+ return <span>{name}</span>;
75
+ })}
76
+ </details>
77
+ </div>
78
+ );
79
+ };
80
+
81
+ render(<App />, document.querySelector("#root"));
82
+ </script>
83
+ </body>
84
+ </html>