@jsenv/navi 0.0.1 → 0.1.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 (139) hide show
  1. package/dist/jsenv_navi.js +22959 -0
  2. package/index.js +66 -16
  3. package/package.json +23 -11
  4. package/src/actions.js +50 -26
  5. package/src/browser_integration/browser_integration.js +31 -6
  6. package/src/browser_integration/via_history.js +42 -9
  7. package/src/components/action_execution/render_actionable_component.jsx +6 -4
  8. package/src/components/action_execution/use_action.js +51 -282
  9. package/src/components/action_execution/use_execute_action.js +106 -92
  10. package/src/components/action_execution/use_run_on_mount.js +9 -0
  11. package/src/components/action_renderer.jsx +21 -32
  12. package/src/components/demos/0_button_demo.html +574 -103
  13. package/src/components/demos/10_column_reordering_debug.html +277 -0
  14. package/src/components/demos/11_table_selection_debug.html +432 -0
  15. package/src/components/demos/1_checkbox_demo.html +579 -202
  16. package/src/components/demos/2_input_textual_demo.html +81 -138
  17. package/src/components/demos/3_radio_demo.html +0 -2
  18. package/src/components/demos/4_select_demo.html +19 -23
  19. package/src/components/demos/6_tablist_demo.html +77 -0
  20. package/src/components/demos/7_table_selection_demo.html +176 -0
  21. package/src/components/demos/8_table_fixed_headers_demo.html +584 -0
  22. package/src/components/demos/9_table_column_drag_demo.html +325 -0
  23. package/src/components/demos/action/0_button_demo.html +2 -4
  24. package/src/components/demos/action/1_input_text_demo.html +643 -222
  25. package/src/components/demos/action/3_details_demo.html +146 -115
  26. package/src/components/demos/action/4_input_checkbox_demo.html +442 -322
  27. package/src/components/demos/action/5_input_checkbox_state_demo.html +270 -0
  28. package/src/components/demos/action/6_checkbox_list_demo.html +304 -72
  29. package/src/components/demos/action/7_radio_list_demo.html +310 -170
  30. package/src/components/demos/action/{8_editable_text_demo.html → 8_editable_demo.html} +65 -76
  31. package/src/components/demos/action/9_link_demo.html +84 -62
  32. package/src/components/demos/ui_transition/0_action_renderer_ui_transition_demo.html +695 -0
  33. package/src/components/demos/ui_transition/1_nested_ui_transition_demo.html +429 -0
  34. package/src/components/demos/ui_transition/2_height_transition_test.html +295 -0
  35. package/src/components/details/details.jsx +62 -64
  36. package/src/components/edition/editable.jsx +186 -0
  37. package/src/components/field/README.md +247 -0
  38. package/src/components/{input → field}/button.jsx +151 -130
  39. package/src/components/field/checkbox_list.jsx +184 -0
  40. package/src/components/{collect_form_element_values.js → field/collect_form_element_values.js} +7 -4
  41. package/src/components/{input → field}/field_css.js +4 -1
  42. package/src/components/field/form.jsx +211 -0
  43. package/src/components/{input → field}/input.jsx +1 -0
  44. package/src/components/{input → field}/input_checkbox.jsx +132 -155
  45. package/src/components/{input → field}/input_radio.jsx +135 -46
  46. package/src/components/field/input_textual.jsx +418 -0
  47. package/src/components/field/label.jsx +32 -0
  48. package/src/components/field/radio_list.jsx +182 -0
  49. package/src/components/{input → field}/select.jsx +17 -32
  50. package/src/components/field/use_action_events.js +132 -0
  51. package/src/components/field/use_form_events.js +55 -0
  52. package/src/components/field/use_ui_state_controller.js +506 -0
  53. package/src/components/item_tracker/README.md +461 -0
  54. package/src/components/item_tracker/use_isolated_item_tracker.jsx +209 -0
  55. package/src/components/item_tracker/use_isolated_item_tracker_demo.html +148 -0
  56. package/src/components/item_tracker/use_isolated_item_tracker_demo.jsx +460 -0
  57. package/src/components/item_tracker/use_item_tracker.jsx +143 -0
  58. package/src/components/item_tracker/use_item_tracker_demo.html +207 -0
  59. package/src/components/item_tracker/use_item_tracker_demo.jsx +216 -0
  60. package/src/components/keyboard_shortcuts/active_keyboard_shortcuts.jsx +87 -0
  61. package/src/components/keyboard_shortcuts/aria_key_shortcuts.js +61 -0
  62. package/src/components/keyboard_shortcuts/keyboard_key_meta.js +17 -0
  63. package/src/components/keyboard_shortcuts/keyboard_shortcuts.js +371 -0
  64. package/src/components/link/link.jsx +65 -102
  65. package/src/components/link/link_with_icon.jsx +52 -0
  66. package/src/components/loader/loader_background.jsx +85 -64
  67. package/src/components/loader/rectangle_loading.jsx +38 -19
  68. package/src/components/route.jsx +8 -4
  69. package/src/components/selection/selection.jsx +1583 -0
  70. package/src/components/svg/font_sized_svg.jsx +45 -0
  71. package/src/components/svg/icon_and_text.jsx +21 -0
  72. package/src/components/svg/svg_mask_overlay.jsx +105 -0
  73. package/src/components/table/drag/table_drag.jsx +506 -0
  74. package/src/components/table/resize/table_resize.jsx +650 -0
  75. package/src/components/table/resize/table_size.js +43 -0
  76. package/src/components/table/selection/table_selection.js +106 -0
  77. package/src/components/table/selection/table_selection.jsx +203 -0
  78. package/src/components/table/sticky/sticky_group.js +354 -0
  79. package/src/components/table/sticky/table_sticky.js +25 -0
  80. package/src/components/table/sticky/table_sticky.jsx +501 -0
  81. package/src/components/table/table.jsx +721 -0
  82. package/src/components/table/table_css.js +211 -0
  83. package/src/components/table/table_ui.jsx +49 -0
  84. package/src/components/table/use_cells_and_columns.js +90 -0
  85. package/src/components/table/use_object_array_to_cells.js +46 -0
  86. package/src/components/table/z_indexes.js +23 -0
  87. package/src/components/tablist/tablist.jsx +99 -0
  88. package/src/components/text/overflow.jsx +15 -0
  89. package/src/components/text/text_and_count.jsx +28 -0
  90. package/src/components/ui_transition.jsx +128 -0
  91. package/src/components/use_auto_focus.js +58 -7
  92. package/src/components/use_batch_during_render.js +33 -0
  93. package/src/components/use_debounce_true.js +7 -7
  94. package/src/components/use_dependencies_diff.js +35 -0
  95. package/src/components/use_focus_group.js +4 -3
  96. package/src/components/use_initial_value.js +8 -34
  97. package/src/components/use_signal_sync.js +1 -1
  98. package/src/components/use_stable_callback.js +68 -0
  99. package/src/components/use_state_array.js +16 -9
  100. package/src/docs/actions.md +22 -0
  101. package/src/notes.md +33 -12
  102. package/src/route/route.js +97 -47
  103. package/src/store/resource_graph.js +2 -1
  104. package/src/store/tests/{resource_graph_dependencies.test.js → resource_graph_dependencies.test_manual.js} +13 -13
  105. package/src/utils/is_signal.js +20 -0
  106. package/src/utils/stringify_for_display.js +4 -23
  107. package/src/validation/constraints/confirm_constraint.js +14 -0
  108. package/src/validation/constraints/create_unique_value_constraint.js +27 -0
  109. package/src/validation/constraints/native_constraints.js +313 -0
  110. package/src/validation/constraints/readonly_constraint.js +36 -0
  111. package/src/validation/constraints/single_space_constraint.js +13 -0
  112. package/src/validation/custom_constraint_validation.js +599 -0
  113. package/src/validation/custom_message.js +18 -0
  114. package/src/validation/demos/browser_style.png +0 -0
  115. package/src/validation/demos/form_validation_demo.html +142 -0
  116. package/src/validation/demos/form_validation_demo_preact.html +87 -0
  117. package/src/validation/demos/form_validation_native_popover_demo.html +168 -0
  118. package/src/validation/demos/form_validation_vs_native_demo.html +172 -0
  119. package/src/validation/demos/validation_message_demo.html +203 -0
  120. package/src/validation/hooks/use_constraints.js +23 -0
  121. package/src/validation/hooks/use_custom_validation_ref.js +73 -0
  122. package/src/validation/hooks/use_validation_message.js +19 -0
  123. package/src/validation/validation_message.js +741 -0
  124. package/src/components/editable_text/editable_text.jsx +0 -96
  125. package/src/components/form.jsx +0 -144
  126. package/src/components/input/checkbox_list.jsx +0 -294
  127. package/src/components/input/field.jsx +0 -61
  128. package/src/components/input/input_textual.jsx +0 -338
  129. package/src/components/input/radio_list.jsx +0 -283
  130. package/src/components/input/use_form_event.js +0 -20
  131. package/src/components/input/use_on_change.js +0 -12
  132. package/src/components/selection/selection.js +0 -5
  133. package/src/components/selection/selection_context.jsx +0 -262
  134. package/src/components/shortcut/shortcut_context.jsx +0 -390
  135. package/src/components/use_action_events.js +0 -37
  136. package/src/utils/iterable_weak_set.js +0 -62
  137. /package/src/components/demos/action/{11_nested_shortcuts_demo.html → 11_nested_shortcuts_demo.xhtml} +0 -0
  138. /package/src/components/{shortcut → keyboard_shortcuts}/os.js +0 -0
  139. /package/src/route/{route.test.html → route.xtest.html} +0 -0
@@ -1,200 +1,38 @@
1
- import { signal } from "@preact/signals";
1
+ import { useSignal } from "@preact/signals";
2
2
  import { useCallback, useRef } from "preact/hooks";
3
+
3
4
  import { createAction } from "../../actions.js";
4
5
  import { addIntoArray, removeFromArray } from "../../utils/array_add_remove.js";
5
- import { isSignal } from "../../utils/stringify_for_display.js";
6
- import { useInitialValue } from "../use_initial_value.js";
7
- import { useFormContext } from "./form_context.js";
8
-
9
- let debug = false;
10
- let componentActionIdCounter = 0;
11
- const useComponentActionCacheKey = () => {
12
- const componentActionCacheKeyRef = useRef(null);
13
- // It's very important to use an object here as componentId and not just in integer
14
- // because this key will be used as a key in a WeakMap
15
- // and if we pass an integer the browser allows itself to garbage collect it
16
- // but not if it's an object as the useRef above keeps referencing it
17
- // this is a subtle different and might be the reason why WeakMap does not accept primitive as keys
18
- if (!componentActionCacheKeyRef.current) {
19
- const id = ++componentActionIdCounter;
20
- componentActionCacheKeyRef.current = {
21
- id,
22
- toString: () => `component_action_id_${id}`,
23
- };
24
- }
25
- return componentActionCacheKeyRef.current;
26
- };
27
-
28
- // used by <form> to have their own action bound to many parameters
29
- // any form element within the <form> will update these params
30
- // these params are also assigned just before executing the action to ensure they are in sync
31
- // (could also be used by <fieldset> but I think fieldset are not going to be used this way and
32
- // we will reserve this behavior to <form>)
33
- export const useFormActionBoundToFormParams = (action) => {
34
- const actionCacheKey = useComponentActionCacheKey();
35
- const cacheKey = typeof action === "function" ? actionCacheKey : action;
36
- const [formParamsSignal, updateFormParams] = useActionParamsSignal(cacheKey);
37
- const formActionBoundActionToFormParams = useBoundAction(
38
- action,
39
- formParamsSignal,
40
- );
41
- return [
42
- formActionBoundActionToFormParams,
43
- formParamsSignal,
44
- updateFormParams,
45
- ];
46
- };
47
- export const useOneFormParam = (
48
- name,
49
- externalValue,
50
- fallbackValue,
51
- defaultValue,
52
- ) => {
53
- const { formParamsSignal } = useFormContext();
54
- const previousFormParamsSignalRef = useRef(null);
55
- const formActionChanged =
56
- previousFormParamsSignalRef.current !== null &&
57
- previousFormParamsSignalRef.current !== formParamsSignal;
58
- previousFormParamsSignalRef.current = formParamsSignal;
59
-
60
- const getValue = useCallback(
61
- () => formParamsSignal.value[name],
62
- [formParamsSignal],
63
- );
64
- const setValue = useCallback(
65
- (value) => updateParamsSignal(formParamsSignal, { [name]: value }),
66
- [formParamsSignal],
67
- );
68
-
69
- const initialValue = useInitialValue(
70
- name,
71
- externalValue,
72
- fallbackValue,
73
- defaultValue,
74
- setValue,
75
- );
76
- if (formActionChanged) {
77
- if (debug) {
78
- console.debug(
79
- `useOneFormParam(${name}) form action changed, re-initializing with: ${initialValue}`,
80
- );
81
- }
82
- setValue(initialValue);
83
- }
84
-
85
- const resetValue = useCallback(() => {
86
- setValue(initialValue);
87
- }, [initialValue, formParamsSignal]);
88
-
89
- return [getValue(), setValue, resetValue];
90
- };
91
6
 
92
7
  // used by form elements such as <input>, <select>, <textarea> to have their own action bound to a single parameter
93
8
  // when inside a <form> the form params are updated when the form element single param is updated
94
- export const useActionBoundToOneParam = (
95
- action,
96
- name,
97
- externalValue,
98
- fallbackValue,
99
- defaultValue,
100
- ) => {
101
- const externalValueIsSignal = isSignal(externalValue);
102
- let externalValueSignal;
103
- if (externalValueIsSignal) {
104
- externalValueSignal = externalValue;
105
- externalValue = externalValueSignal.peek();
106
- }
107
-
108
- if (action.isProxy && !externalValueSignal) {
109
- throw new Error(
110
- `useActionBoundToOneParam(${name}) action is a proxy but no valueSignal provided`,
111
- );
112
- }
113
-
114
- const actionCacheKey = useComponentActionCacheKey();
115
- const cacheKey = typeof action === "function" ? actionCacheKey : action;
116
- const [paramsSignal, updateParams] = useActionParamsSignal(
117
- cacheKey,
118
- externalValueSignal,
119
- );
120
- const previousParamsSignalRef = useRef(null);
121
- const actionChanged =
122
- previousParamsSignalRef.current !== null &&
123
- previousParamsSignalRef.current !== paramsSignal;
124
- previousParamsSignalRef.current = paramsSignal;
125
-
126
- const boundAction = useBoundAction(
127
- action,
128
- externalValueSignal ? null : paramsSignal,
129
- );
130
- const getValue = externalValueSignal
131
- ? useCallback(() => paramsSignal.value, [paramsSignal])
132
- : useCallback(() => paramsSignal.value[name], [paramsSignal]);
133
- const setValue = externalValueSignal
134
- ? useCallback(
135
- (value) => {
136
- paramsSignal.value = value;
137
- },
138
- [paramsSignal],
139
- )
140
- : useCallback(
141
- (value) => {
142
- if (debug) {
143
- console.debug(
144
- `useActionBoundToOneParam(${name}) set value to ${value} (old value is ${getValue()} )`,
145
- );
146
- }
147
- return updateParams({ [name]: value });
148
- },
149
- [updateParams],
150
- );
151
-
152
- const initialValue = useInitialValue(
153
- name,
154
- externalValue,
155
- fallbackValue,
156
- defaultValue,
157
- setValue,
158
- );
159
- if (actionChanged) {
160
- if (debug) {
161
- console.debug(
162
- `useActionBoundToOneParam(${name}) action changed, re-initializing with: ${initialValue}`,
163
- );
164
- }
165
- setValue(initialValue);
9
+ export const useActionBoundToOneParam = (action, externalValue) => {
10
+ const actionFirstArgSignal = useSignal(externalValue);
11
+ const boundAction = useBoundAction(action, actionFirstArgSignal);
12
+ const getValue = useCallback(() => actionFirstArgSignal.value, []);
13
+ const setValue = useCallback((value) => {
14
+ actionFirstArgSignal.value = value;
15
+ }, []);
16
+ const externalValueRef = useRef(externalValue);
17
+ if (externalValue !== externalValueRef.current) {
18
+ externalValueRef.current = externalValue;
19
+ setValue(externalValue);
166
20
  }
167
21
 
168
- const reset = useCallback(() => {
169
- setValue(initialValue);
170
- }, [initialValue, paramsSignal]);
171
-
172
- return [boundAction, getValue(), setValue, reset];
22
+ const value = getValue();
23
+ return [boundAction, value, setValue];
173
24
  };
174
-
175
- // export const useActionBoundToOneBooleanParam = (action, name, value) => {
176
- // const [boundAction, getValue, setValue, resetValue] =
177
- // useActionBoundToOneParam(action, name, Boolean(value));
178
-
179
- // return [
180
- // boundAction,
181
- // () => Boolean(getValue()),
182
- // (value) => setValue(Boolean(value)),
183
- // resetValue,
184
- // ];
185
- // };
186
-
187
25
  export const useActionBoundToOneArrayParam = (
188
26
  action,
189
27
  name,
190
- initialValue,
28
+ externalValue,
191
29
  fallbackValue,
192
- defaultValue = [],
30
+ defaultValue,
193
31
  ) => {
194
- const [boundAction, value, setValue, resetValue] = useActionBoundToOneParam(
32
+ const [boundAction, value, setValue, initialValue] = useActionBoundToOneParam(
195
33
  action,
196
34
  name,
197
- initialValue,
35
+ externalValue,
198
36
  fallbackValue,
199
37
  defaultValue,
200
38
  );
@@ -207,101 +45,16 @@ export const useActionBoundToOneArrayParam = (
207
45
  setValue(removeFromArray(valueArray, valueToRemove));
208
46
  };
209
47
 
210
- return [boundAction, value, add, remove, resetValue];
211
- };
212
- export const useOneFormArrayParam = (
213
- name,
214
- initialValue,
215
- fallbackValue,
216
- defaultValue = [],
217
- ) => {
218
- const [getValue, setValue, resetValue] = useOneFormParam(
219
- name,
220
- initialValue,
221
- fallbackValue,
222
- defaultValue,
223
- );
224
- const add = (valueToAdd, valueArray = getValue()) => {
225
- setValue(addIntoArray(valueArray, valueToAdd));
226
- };
227
- const remove = (valueToRemove, valueArray = getValue()) => {
228
- setValue(removeFromArray(valueArray, valueToRemove));
229
- };
230
- return [getValue, add, remove, resetValue];
48
+ const result = [boundAction, value, setValue, initialValue];
49
+ result.add = add;
50
+ result.remove = remove;
51
+ return result;
231
52
  };
232
-
233
53
  // used by <details> to just call their action
234
54
  export const useAction = (action, paramsSignal) => {
235
55
  return useBoundAction(action, paramsSignal);
236
56
  };
237
57
 
238
- const sharedSignalCache = new WeakMap();
239
- const useActionParamsSignal = (cacheKey, valueSignal) => {
240
- if (valueSignal) {
241
- return [
242
- valueSignal,
243
- (value) => {
244
- valueSignal.value = value;
245
- },
246
- ];
247
- }
248
-
249
- // ✅ cacheKey peut être componentId (Symbol) ou action (objet)
250
- const fromCache = sharedSignalCache.get(cacheKey);
251
- if (fromCache) {
252
- return fromCache;
253
- }
254
-
255
- const paramsSignal = signal({});
256
- const result = [
257
- paramsSignal,
258
- (value) => updateParamsSignal(paramsSignal, value, cacheKey),
259
- ];
260
- sharedSignalCache.set(cacheKey, result);
261
- if (debug) {
262
- console.debug(`Created params signal for ${cacheKey} with params:`, {});
263
- }
264
- return result;
265
- };
266
-
267
- export const updateParamsSignal = (paramsSignal, object, cacheKey) => {
268
- const currentParams = paramsSignal.peek();
269
- const paramsCopy = { ...currentParams };
270
- let modified = false;
271
- for (const key of Object.keys(object)) {
272
- const value = object[key];
273
- const currentValue = currentParams[key];
274
- if (Object.hasOwn(currentParams, key)) {
275
- if (value !== currentValue) {
276
- modified = true;
277
- paramsCopy[key] = value;
278
- }
279
- } else {
280
- modified = true;
281
- paramsCopy[key] = value;
282
- }
283
- }
284
- if (modified) {
285
- if (debug) {
286
- console.debug(
287
- `Updating params for ${cacheKey} with new params:`,
288
- object,
289
- `result:`,
290
- paramsCopy,
291
- );
292
- }
293
- paramsSignal.value = paramsCopy;
294
- } else if (debug) {
295
- console.debug(
296
- `No change in params for ${cacheKey}, not updating.`,
297
- `current params:`,
298
- currentParams,
299
- `new params:`,
300
- object,
301
- );
302
- }
303
- };
304
-
305
58
  const useBoundAction = (action, actionParamsSignal) => {
306
59
  const actionRef = useRef();
307
60
  const actionCallbackRef = useRef();
@@ -309,22 +62,38 @@ const useBoundAction = (action, actionParamsSignal) => {
309
62
  if (!action) {
310
63
  return null;
311
64
  }
312
- if (typeof action === "function" && !action.isAction) {
313
- let actionInstance = actionRef.current;
314
- if (!actionInstance) {
315
- actionInstance = createAction((...args) => {
316
- return actionCallbackRef.current(...args);
317
- });
318
- if (actionParamsSignal) {
319
- actionInstance = actionInstance.bindParams(actionParamsSignal);
320
- }
321
- actionRef.current = actionInstance;
322
- }
65
+ if (isFunctionButNotAnActionFunction(action)) {
323
66
  actionCallbackRef.current = action;
324
- return actionInstance;
67
+ const existingAction = actionRef.current;
68
+ if (existingAction) {
69
+ return existingAction;
70
+ }
71
+ const actionFromFunction = createAction(
72
+ (...args) => {
73
+ return actionCallbackRef.current?.(...args);
74
+ },
75
+ {
76
+ name: action.name,
77
+ // We don't want to give empty params by default
78
+ // we want to give undefined for regular functions
79
+ params: undefined,
80
+ },
81
+ );
82
+ if (!actionParamsSignal) {
83
+ actionRef.current = actionFromFunction;
84
+ return actionFromFunction;
85
+ }
86
+ const actionBoundToParams =
87
+ actionFromFunction.bindParams(actionParamsSignal);
88
+ actionRef.current = actionBoundToParams;
89
+ return actionBoundToParams;
325
90
  }
326
91
  if (actionParamsSignal) {
327
92
  return action.bindParams(actionParamsSignal);
328
93
  }
329
94
  return action;
330
95
  };
96
+
97
+ const isFunctionButNotAnActionFunction = (action) => {
98
+ return typeof action === "function" && !action.isAction;
99
+ };
@@ -1,5 +1,8 @@
1
- import { addCustomMessage, removeCustomMessage } from "@jsenv/validation";
2
1
  import { useCallback, useLayoutEffect, useRef, useState } from "preact/hooks";
2
+ import {
3
+ addCustomMessage,
4
+ removeCustomMessage,
5
+ } from "../../validation/custom_message.js";
3
6
  import { useResetErrorBoundary } from "../error_boundary_context.js";
4
7
 
5
8
  let debug = false;
@@ -57,105 +60,116 @@ export const useExecuteAction = (
57
60
  };
58
61
  });
59
62
 
60
- const executeAction = useCallback((actionEvent) => {
61
- const { action, requester, event, method } = actionEvent.detail;
62
-
63
- if (debug) {
64
- console.debug(
65
- "executing action, requested by",
66
- requester,
67
- `(event: ${event?.type})`,
68
- );
69
- }
70
-
71
- const dispatchCustomEvent = (type, options) => {
72
- const element = elementRef.current;
73
- const customEvent = new CustomEvent(type, options);
74
- element.dispatchEvent(customEvent);
75
- };
76
- if (resetErrorBoundary) {
77
- resetErrorBoundary();
78
- }
79
- removeErrorMessage();
80
- setError(null);
81
-
82
- const validationMessageTarget = requester || elementRef.current;
83
- validationMessageTargetRef.current = validationMessageTarget;
84
-
85
- dispatchCustomEvent("actionstart", {
86
- detail: {
63
+ // const errorEffectRef = useRef();
64
+ // errorEffectRef.current = errorEffect;
65
+ const executeAction = useCallback(
66
+ (actionEvent) => {
67
+ const { action, actionOrigin, requester, event, method } =
68
+ actionEvent.detail;
69
+ const sharedActionEventDetail = {
87
70
  action,
71
+ actionOrigin,
88
72
  requester,
89
73
  event,
90
74
  method,
91
- },
92
- });
75
+ };
76
+
77
+ if (debug) {
78
+ console.debug(
79
+ "executing action, requested by",
80
+ requester,
81
+ `(event: ${event?.type})`,
82
+ );
83
+ }
93
84
 
94
- return action[method]({
95
- onAbort: (reason) => {
96
- if (
97
- // at this stage the action side effect might have removed the <element> from the DOM
98
- // (in theory no because action side effect are batched to happen after)
99
- // but other side effects might do this
100
- elementRef.current
101
- ) {
102
- dispatchCustomEvent("actionabort", {
103
- detail: {
104
- reason,
85
+ const dispatchCustomEvent = (type, options) => {
86
+ const element = elementRef.current;
87
+ const customEvent = new CustomEvent(type, options);
88
+ element.dispatchEvent(customEvent);
89
+ };
90
+ if (resetErrorBoundary) {
91
+ resetErrorBoundary();
92
+ }
93
+ removeErrorMessage();
94
+ setError(null);
105
95
 
106
- action,
107
- requester,
108
- event,
109
- method,
110
- },
111
- });
112
- }
113
- },
114
- onError: (error) => {
115
- if (
116
- // at this stage the action side effect might have removed the <element> from the DOM
117
- // (in theory no because action side effect are batched to happen after)
118
- // but other side effects might do this
119
- elementRef.current
120
- ) {
121
- dispatchCustomEvent("actionerror", {
122
- detail: {
123
- error,
96
+ const validationMessageTarget = requester || elementRef.current;
97
+ validationMessageTargetRef.current = validationMessageTarget;
124
98
 
125
- action,
126
- requester,
127
- event,
128
- method,
129
- },
130
- });
131
- }
132
- if (errorEffect === "show_validation_message") {
133
- addErrorMessage(error);
134
- } else if (errorEffect === "throw") {
135
- setError(error);
136
- }
137
- },
138
- onComplete: (data) => {
139
- if (
140
- // at this stage the action side effect might have removed the <element> from the DOM
141
- // (in theory no because action side effect are batched to happen after)
142
- // but other side effects might do this
143
- elementRef.current
144
- ) {
145
- dispatchCustomEvent("actionend", {
146
- detail: {
147
- data,
99
+ dispatchCustomEvent("actionstart", {
100
+ detail: sharedActionEventDetail,
101
+ });
148
102
 
149
- action,
150
- requester,
151
- event,
152
- method,
153
- },
154
- });
155
- }
156
- },
157
- });
158
- }, []);
103
+ return action[method]({
104
+ reason: `"${event.type}" event on ${(() => {
105
+ const target = event.target;
106
+ const tagName = target.tagName.toLowerCase();
107
+
108
+ if (target.id) {
109
+ return `${tagName}#${target.id}`;
110
+ }
111
+
112
+ const uiName = target.getAttribute("data-ui-name");
113
+ if (uiName) {
114
+ return `${tagName}[data-ui-name="${uiName}"]`;
115
+ }
116
+
117
+ return `<${tagName}>`;
118
+ })()}`,
119
+ onAbort: (reason) => {
120
+ if (
121
+ // at this stage the action side effect might have removed the <element> from the DOM
122
+ // (in theory no because action side effect are batched to happen after)
123
+ // but other side effects might do this
124
+ elementRef.current
125
+ ) {
126
+ dispatchCustomEvent("actionabort", {
127
+ detail: {
128
+ ...sharedActionEventDetail,
129
+ reason,
130
+ },
131
+ });
132
+ }
133
+ },
134
+ onError: (error) => {
135
+ if (
136
+ // at this stage the action side effect might have removed the <element> from the DOM
137
+ // (in theory no because action side effect are batched to happen after)
138
+ // but other side effects might do this
139
+ elementRef.current
140
+ ) {
141
+ dispatchCustomEvent("actionerror", {
142
+ detail: {
143
+ ...sharedActionEventDetail,
144
+ error,
145
+ },
146
+ });
147
+ }
148
+ if (errorEffect === "show_validation_message") {
149
+ addErrorMessage(error);
150
+ } else if (errorEffect === "throw") {
151
+ setError(error);
152
+ }
153
+ },
154
+ onComplete: (data) => {
155
+ if (
156
+ // at this stage the action side effect might have removed the <element> from the DOM
157
+ // (in theory no because action side effect are batched to happen after)
158
+ // but other side effects might do this
159
+ elementRef.current
160
+ ) {
161
+ dispatchCustomEvent("actionend", {
162
+ detail: {
163
+ ...sharedActionEventDetail,
164
+ data,
165
+ },
166
+ });
167
+ }
168
+ },
169
+ });
170
+ },
171
+ [errorEffect],
172
+ );
159
173
 
160
174
  return executeAction;
161
175
  };
@@ -0,0 +1,9 @@
1
+ import { useEffect } from "preact/hooks";
2
+
3
+ export const useRunOnMount = (action, Component) => {
4
+ useEffect(() => {
5
+ action.run({
6
+ reason: `<${Component.name} /> mounted`,
7
+ });
8
+ }, []);
9
+ };
@@ -1,27 +1,3 @@
1
- /**
2
- * TODO: when switching from one state to another we should preserve the dimensions to prevent layout shift
3
- * the exact way to do this is not yet clear but I suspect something as follow:
4
- *
5
- *
6
- * While content is loading we don't know (except if we are given an size)
7
- * When reloading the content will be gone, we should keep a placeholder taking the same space
8
- * When there is an error the error should take the same space as the content
9
- * but be displayed on top
10
- * (If error is bigger it can take more space? I guess so, maybe an overflow would be better to prevent layout shit again)
11
- *
12
- * And once we know the new content size ideally we could have some sort of transition
13
- * (like an height transition from current height to new height)
14
- *
15
- * consider https://motion.dev/docs/react-layout-animations
16
- *
17
- * but might be too complexe for what we want.
18
- * we want ability to transit from anything to anything, it's not a layout change
19
- * it's more view transition but with a very simple behavior
20
- *
21
- * And certainly this https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API#pseudo-elements
22
- *
23
- */
24
-
25
1
  import { useErrorBoundary, useLayoutEffect } from "preact/hooks";
26
2
  import { getActionPrivateProperties } from "../action_private_properties.js";
27
3
  import { useActionStatus } from "../use_action_status.js";
@@ -45,7 +21,7 @@ const renderErrorDefault = (error) => {
45
21
  };
46
22
  const renderCompletedDefault = () => null;
47
23
 
48
- export const ActionRenderer = ({ action, children }) => {
24
+ export const ActionRenderer = ({ action, children, disabled }) => {
49
25
  const {
50
26
  idle: renderIdle = renderIdleDefault,
51
27
  loading: renderLoading = renderLoadingDefault,
@@ -54,7 +30,12 @@ export const ActionRenderer = ({ action, children }) => {
54
30
  completed: renderCompleted,
55
31
  always: renderAlways,
56
32
  } = typeof children === "function" ? { completed: children } : children || {};
57
- if (!action) {
33
+
34
+ if (disabled) {
35
+ return null;
36
+ }
37
+
38
+ if (action === undefined) {
58
39
  throw new Error(
59
40
  "ActionRenderer requires an action to render, but none was provided.",
60
41
  );
@@ -67,8 +48,10 @@ export const ActionRenderer = ({ action, children }) => {
67
48
  // This tells the action system that errors should be caught and stored
68
49
  // in the action's error state rather than bubbling up
69
50
  useLayoutEffect(() => {
70
- const { ui } = getActionPrivateProperties(action);
71
- ui.hasRenderers = true;
51
+ if (action) {
52
+ const { ui } = getActionPrivateProperties(action);
53
+ ui.hasRenderers = true;
54
+ }
72
55
  }, [action]);
73
56
 
74
57
  useLayoutEffect(() => {
@@ -80,7 +63,7 @@ export const ActionRenderer = ({ action, children }) => {
80
63
  return () => {
81
64
  actionUIRenderedPromiseWeakMap.delete(action);
82
65
  };
83
- }, []);
66
+ }, [action]);
84
67
 
85
68
  // If renderAlways is provided, it wins and handles all rendering
86
69
  if (renderAlways) {
@@ -120,9 +103,15 @@ export const ActionRenderer = ({ action, children }) => {
120
103
  return renderCompletedSafe(data, action);
121
104
  };
122
105
 
106
+ const defaultPromise = Promise.resolve();
107
+ defaultPromise.resolve = () => {};
108
+
123
109
  const actionUIRenderedPromiseWeakMap = new WeakMap();
124
- const useUIRenderedPromise = (route) => {
125
- const actionUIRenderedPromise = actionUIRenderedPromiseWeakMap.get(route);
110
+ const useUIRenderedPromise = (action) => {
111
+ if (!action) {
112
+ return defaultPromise;
113
+ }
114
+ const actionUIRenderedPromise = actionUIRenderedPromiseWeakMap.get(action);
126
115
  if (actionUIRenderedPromise) {
127
116
  return actionUIRenderedPromise;
128
117
  }
@@ -131,6 +120,6 @@ const useUIRenderedPromise = (route) => {
131
120
  resolve = res;
132
121
  });
133
122
  promise.resolve = resolve;
134
- actionUIRenderedPromiseWeakMap.set(route, promise);
123
+ actionUIRenderedPromiseWeakMap.set(action, promise);
135
124
  return promise;
136
125
  };