@jsenv/navi 0.10.1 → 0.11.0
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/dist/jsenv_navi.js +13858 -23295
- package/dist/jsenv_navi.js.map +1281 -0
- package/package.json +5 -7
- package/index.js +0 -122
- package/src/action_private_properties.js +0 -11
- package/src/action_proxy_test.html +0 -353
- package/src/action_run_states.js +0 -5
- package/src/actions.js +0 -1401
- package/src/browser_integration/browser_integration.js +0 -216
- package/src/browser_integration/document_back_and_forward.js +0 -17
- package/src/browser_integration/document_loading_signal.js +0 -100
- package/src/browser_integration/document_state_signal.js +0 -9
- package/src/browser_integration/document_url_signal.js +0 -9
- package/src/browser_integration/use_is_visited.js +0 -19
- package/src/browser_integration/via_history.js +0 -232
- package/src/browser_integration/via_navigation.js +0 -168
- package/src/components/action_execution/form_context.js +0 -5
- package/src/components/action_execution/render_actionable_component.jsx +0 -29
- package/src/components/action_execution/use_action.js +0 -99
- package/src/components/action_execution/use_execute_action.js +0 -177
- package/src/components/action_execution/use_run_on_mount.js +0 -9
- package/src/components/action_renderer.jsx +0 -125
- package/src/components/callout/callout.js +0 -990
- package/src/components/callout/callout_demo.html +0 -201
- package/src/components/callout/test_dynamic_positioning.html +0 -161
- package/src/components/callout/test_html_document_iframe.html +0 -182
- package/src/components/demos/0_button_demo.html +0 -707
- package/src/components/demos/10_column_reordering_debug.html +0 -277
- package/src/components/demos/11_table_selection_debug.html +0 -432
- package/src/components/demos/1_checkbox_demo.html +0 -754
- package/src/components/demos/2_input_textual_demo.html +0 -286
- package/src/components/demos/3_radio_demo.html +0 -874
- package/src/components/demos/4_select_demo.html +0 -100
- package/src/components/demos/5_list_scrollable_demo.html +0 -153
- package/src/components/demos/6_tablist_demo.html +0 -77
- package/src/components/demos/7_table_selection_demo.html +0 -176
- package/src/components/demos/8_table_fixed_headers_demo.html +0 -584
- package/src/components/demos/9_table_column_drag_demo.html +0 -325
- package/src/components/demos/action/0_button_demo.html +0 -204
- package/src/components/demos/action/10_shortcuts_demo.html +0 -189
- package/src/components/demos/action/11_nested_shortcuts_demo.xhtml +0 -401
- package/src/components/demos/action/1_input_text_demo.html +0 -876
- package/src/components/demos/action/2_form_multiple.html +0 -303
- package/src/components/demos/action/3_details_demo.html +0 -203
- package/src/components/demos/action/4_input_checkbox_demo.html +0 -731
- package/src/components/demos/action/5_input_checkbox_state_demo.html +0 -270
- package/src/components/demos/action/6_checkbox_list_demo.html +0 -341
- package/src/components/demos/action/7_radio_list_demo.html +0 -357
- package/src/components/demos/action/8_editable_demo.html +0 -431
- package/src/components/demos/action/9_link_demo.html +0 -194
- package/src/components/demos/demo.md +0 -0
- package/src/components/demos/route/basic/basic.html +0 -14
- package/src/components/demos/route/basic/basic_route_demo.jsx +0 -224
- package/src/components/demos/route/multi/multi.html +0 -14
- package/src/components/demos/route/multi/multi_route_demo.jsx +0 -277
- package/src/components/demos/ui_transition/0_action_renderer_ui_transition_demo.html +0 -695
- package/src/components/demos/ui_transition/1_nested_ui_transition_demo.html +0 -429
- package/src/components/demos/ui_transition/2_height_transition_test.html +0 -295
- package/src/components/details/details.jsx +0 -245
- package/src/components/details/summary_marker.jsx +0 -141
- package/src/components/edition/editable.jsx +0 -186
- package/src/components/error_boundary_context.js +0 -9
- package/src/components/field/README.md +0 -247
- package/src/components/field/button.jsx +0 -429
- package/src/components/field/checkbox_list.jsx +0 -185
- package/src/components/field/collect_form_element_values.js +0 -82
- package/src/components/field/custom_field.js +0 -106
- package/src/components/field/form.jsx +0 -209
- package/src/components/field/input.jsx +0 -16
- package/src/components/field/input_checkbox.jsx +0 -434
- package/src/components/field/input_radio.jsx +0 -432
- package/src/components/field/input_textual.jsx +0 -389
- package/src/components/field/label.jsx +0 -46
- package/src/components/field/radio_list.jsx +0 -183
- package/src/components/field/select.jsx +0 -256
- package/src/components/field/use_action_events.js +0 -132
- package/src/components/field/use_form_events.js +0 -59
- package/src/components/field/use_ui_state_controller.js +0 -506
- package/src/components/item_tracker/README.md +0 -461
- package/src/components/item_tracker/use_isolated_item_tracker.jsx +0 -209
- package/src/components/item_tracker/use_isolated_item_tracker_demo.html +0 -148
- package/src/components/item_tracker/use_isolated_item_tracker_demo.jsx +0 -460
- package/src/components/item_tracker/use_item_tracker.jsx +0 -143
- package/src/components/item_tracker/use_item_tracker_demo.html +0 -207
- package/src/components/item_tracker/use_item_tracker_demo.jsx +0 -216
- package/src/components/keyboard_shortcuts/active_keyboard_shortcuts.jsx +0 -87
- package/src/components/keyboard_shortcuts/aria_key_shortcuts.js +0 -61
- package/src/components/keyboard_shortcuts/keyboard_key_meta.js +0 -17
- package/src/components/keyboard_shortcuts/keyboard_shortcuts.js +0 -371
- package/src/components/keyboard_shortcuts/os.js +0 -9
- package/src/components/layout/demos/demo_flex.html +0 -638
- package/src/components/layout/demos/demo_layout_style_buttons.html +0 -351
- package/src/components/layout/demos/demo_layout_style_input.html +0 -226
- package/src/components/layout/demos/demo_layout_style_text.html +0 -514
- package/src/components/layout/flex.jsx +0 -109
- package/src/components/layout/layout_context.jsx +0 -3
- package/src/components/layout/spacing.jsx +0 -20
- package/src/components/layout/use_layout_style.js +0 -249
- package/src/components/link/link.jsx +0 -267
- package/src/components/link/link_with_icon.jsx +0 -52
- package/src/components/loader/loader_background.jsx +0 -372
- package/src/components/loader/loading_spinner.jsx +0 -68
- package/src/components/loader/network_speed.js +0 -83
- package/src/components/loader/rectangle_loading.jsx +0 -244
- package/src/components/props_composition/demos/demo_with_props_style.html +0 -81
- package/src/components/props_composition/with_props_class_name.js +0 -37
- package/src/components/props_composition/with_props_style.js +0 -26
- package/src/components/route.jsx +0 -19
- package/src/components/selection/selection.jsx +0 -1583
- package/src/components/svg/font_sized_svg.jsx +0 -59
- package/src/components/svg/icon_and_text.jsx +0 -21
- package/src/components/svg/svg_mask_overlay.jsx +0 -105
- package/src/components/table/drag/table_drag.jsx +0 -506
- package/src/components/table/resize/table_resize.jsx +0 -650
- package/src/components/table/resize/table_size.js +0 -43
- package/src/components/table/selection/table_selection.js +0 -106
- package/src/components/table/selection/table_selection.jsx +0 -203
- package/src/components/table/sticky/sticky_group.js +0 -354
- package/src/components/table/sticky/table_sticky.js +0 -25
- package/src/components/table/sticky/table_sticky.jsx +0 -501
- package/src/components/table/table.jsx +0 -721
- package/src/components/table/table_css.js +0 -211
- package/src/components/table/table_ui.jsx +0 -49
- package/src/components/table/use_cells_and_columns.js +0 -90
- package/src/components/table/use_object_array_to_cells.js +0 -46
- package/src/components/table/z_indexes.js +0 -23
- package/src/components/tablist/tablist.jsx +0 -99
- package/src/components/text/demos/demo_text_and_icon.html +0 -421
- package/src/components/text/overflow.jsx +0 -15
- package/src/components/text/text.jsx +0 -83
- package/src/components/text/text_and_count.jsx +0 -28
- package/src/components/ui_transition.jsx +0 -128
- package/src/components/use_auto_focus.js +0 -94
- package/src/components/use_batch_during_render.js +0 -33
- package/src/components/use_debounce_true.js +0 -31
- package/src/components/use_dependencies_diff.js +0 -35
- package/src/components/use_focus_group.js +0 -20
- package/src/components/use_initial_value.js +0 -78
- package/src/components/use_is_visited.js +0 -19
- package/src/components/use_ref_array.js +0 -38
- package/src/components/use_signal_sync.js +0 -50
- package/src/components/use_stable_callback.js +0 -68
- package/src/components/use_state_array.js +0 -47
- package/src/docs/actions.md +0 -250
- package/src/docs/demos/resource/action_status.jsx +0 -42
- package/src/docs/demos/resource/demo.md +0 -1
- package/src/docs/demos/resource/resource_demo_0.html +0 -84
- package/src/docs/demos/resource/resource_demo_10_post_gc.html +0 -364
- package/src/docs/demos/resource/resource_demo_11_describe_many.html +0 -362
- package/src/docs/demos/resource/resource_demo_2.html +0 -173
- package/src/docs/demos/resource/resource_demo_3_filtered_users.html +0 -415
- package/src/docs/demos/resource/resource_demo_4_details.html +0 -284
- package/src/docs/demos/resource/resource_demo_5_renderer_lazy.html +0 -115
- package/src/docs/demos/resource/resource_demo_6_gc.html +0 -217
- package/src/docs/demos/resource/resource_demo_7_child_gc.html +0 -240
- package/src/docs/demos/resource/resource_demo_8_proxy_gc.html +0 -319
- package/src/docs/demos/resource/resource_demo_9_describe_one.html +0 -472
- package/src/docs/demos/resource/tata.jsx +0 -3
- package/src/docs/demos/resource/toto.jsx +0 -3
- package/src/docs/demos/user_nav/user_nav.html +0 -12
- package/src/docs/demos/user_nav/user_nav.jsx +0 -330
- package/src/docs/resource_dependencies.md +0 -103
- package/src/docs/resource_with_params.md +0 -80
- package/src/navi_css_vars.js +0 -14
- package/src/notes.md +0 -34
- package/src/route/route.js +0 -596
- package/src/route/route.xtest.html +0 -228
- package/src/store/array_signal_store.js +0 -537
- package/src/store/local_storage_signal.js +0 -17
- package/src/store/resource_graph.js +0 -1304
- package/src/store/tests/resource_graph_autoreload_demo.html +0 -12
- package/src/store/tests/resource_graph_autoreload_demo.jsx +0 -964
- package/src/store/tests/resource_graph_dependencies.test_manual.js +0 -95
- package/src/store/value_in_local_storage.js +0 -187
- package/src/symbol_object_signal.js +0 -1
- package/src/use_action_data.js +0 -10
- package/src/use_action_status.js +0 -47
- package/src/utils/add_many_event_listeners.js +0 -15
- package/src/utils/array_add_remove.js +0 -61
- package/src/utils/array_signal.js +0 -15
- package/src/utils/compare_two_js_values.js +0 -172
- package/src/utils/execute_with_cleanup.js +0 -21
- package/src/utils/get_caller_info.js +0 -85
- package/src/utils/is_signal.js +0 -20
- package/src/utils/js_value_weak_map.js +0 -162
- package/src/utils/js_value_weak_map_demo.html +0 -690
- package/src/utils/merge_two_js_values.js +0 -53
- package/src/utils/stringify_for_display.js +0 -131
- package/src/utils/weak_effect.js +0 -48
- package/src/validation/constraints/confirm_constraint.js +0 -14
- package/src/validation/constraints/create_unique_value_constraint.js +0 -27
- package/src/validation/constraints/native_constraints.js +0 -338
- package/src/validation/constraints/readonly_constraint.js +0 -41
- package/src/validation/constraints/same_as_constraint.js +0 -42
- package/src/validation/constraints/single_space_constraint.js +0 -13
- package/src/validation/custom_constraint_validation.js +0 -793
- package/src/validation/custom_message.js +0 -18
- package/src/validation/demos/browser_style.png +0 -0
- package/src/validation/demos/demo_same_as_constraint.html +0 -259
- package/src/validation/demos/form_validation_demo.html +0 -142
- package/src/validation/demos/form_validation_demo_preact.html +0 -87
- package/src/validation/demos/form_validation_native_popover_demo.html +0 -168
- package/src/validation/demos/form_validation_vs_native_demo.html +0 -172
- package/src/validation/hooks/use_constraints.js +0 -23
- package/src/validation/hooks/use_custom_validation_ref.js +0 -73
- package/src/validation/hooks/use_validation_message.js +0 -19
- package/src/validation/input_change_effect.js +0 -106
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* - We must keep the edited element in the DOM so that
|
|
3
|
-
* the layout remains the same (especially important for table cells)
|
|
4
|
-
* And the editable part is in absolute so that it takes the original content dimensions
|
|
5
|
-
* AND for table cells it can actually take the table cell dimensions
|
|
6
|
-
*
|
|
7
|
-
* This means an editable thing MUST have a parent with position relative that wraps the content and the eventual editable input
|
|
8
|
-
*
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { forwardRef } from "preact/compat";
|
|
12
|
-
import {
|
|
13
|
-
useCallback,
|
|
14
|
-
useImperativeHandle,
|
|
15
|
-
useLayoutEffect,
|
|
16
|
-
useRef,
|
|
17
|
-
useState,
|
|
18
|
-
} from "preact/hooks";
|
|
19
|
-
|
|
20
|
-
import { Input } from "../field/input.jsx";
|
|
21
|
-
|
|
22
|
-
import.meta.css = /* css */ `
|
|
23
|
-
.navi_editable_wrapper {
|
|
24
|
-
position: absolute;
|
|
25
|
-
inset: 0;
|
|
26
|
-
}
|
|
27
|
-
`;
|
|
28
|
-
|
|
29
|
-
export const useEditionController = () => {
|
|
30
|
-
const [editing, editingSetter] = useState(null);
|
|
31
|
-
const startEditing = useCallback((event) => {
|
|
32
|
-
editingSetter((current) => {
|
|
33
|
-
return current || { event };
|
|
34
|
-
});
|
|
35
|
-
}, []);
|
|
36
|
-
const stopEditing = useCallback(() => {
|
|
37
|
-
editingSetter(null);
|
|
38
|
-
}, []);
|
|
39
|
-
|
|
40
|
-
const prevEditingRef = useRef(editing);
|
|
41
|
-
const editionJustEnded = prevEditingRef.current && !editing;
|
|
42
|
-
prevEditingRef.current = editing;
|
|
43
|
-
|
|
44
|
-
return { editing, startEditing, stopEditing, editionJustEnded };
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
export const Editable = forwardRef((props, ref) => {
|
|
48
|
-
let {
|
|
49
|
-
children,
|
|
50
|
-
action,
|
|
51
|
-
editing,
|
|
52
|
-
name,
|
|
53
|
-
value,
|
|
54
|
-
valueSignal,
|
|
55
|
-
onEditEnd,
|
|
56
|
-
constraints,
|
|
57
|
-
type,
|
|
58
|
-
required,
|
|
59
|
-
readOnly,
|
|
60
|
-
min,
|
|
61
|
-
max,
|
|
62
|
-
step,
|
|
63
|
-
minLength,
|
|
64
|
-
maxLength,
|
|
65
|
-
pattern,
|
|
66
|
-
wrapperProps,
|
|
67
|
-
autoSelect = true,
|
|
68
|
-
width,
|
|
69
|
-
height,
|
|
70
|
-
...rest
|
|
71
|
-
} = props;
|
|
72
|
-
if (import.meta.dev && !action) {
|
|
73
|
-
console.warn(`Editable requires an action prop`);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const innerRef = useRef();
|
|
77
|
-
useImperativeHandle(ref, () => innerRef.current);
|
|
78
|
-
|
|
79
|
-
if (valueSignal) {
|
|
80
|
-
value = valueSignal.value;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const editingPreviousRef = useRef(editing);
|
|
84
|
-
const valueWhenEditStartRef = useRef(editing ? value : undefined);
|
|
85
|
-
|
|
86
|
-
if (editingPreviousRef.current !== editing) {
|
|
87
|
-
if (editing) {
|
|
88
|
-
valueWhenEditStartRef.current = value; // Always store the external value
|
|
89
|
-
}
|
|
90
|
-
editingPreviousRef.current = editing;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Simulate typing the initial value when editing starts with a custom value
|
|
94
|
-
useLayoutEffect(() => {
|
|
95
|
-
if (!editing) {
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
const editingEvent = editing.event;
|
|
99
|
-
if (!editingEvent) {
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
const editingEventInitialValue = editingEvent.detail?.initialValue;
|
|
103
|
-
if (editingEventInitialValue === undefined) {
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
const input = innerRef.current;
|
|
107
|
-
input.value = editingEventInitialValue;
|
|
108
|
-
input.dispatchEvent(
|
|
109
|
-
new CustomEvent("input", {
|
|
110
|
-
bubbles: false,
|
|
111
|
-
}),
|
|
112
|
-
);
|
|
113
|
-
}, [editing]);
|
|
114
|
-
|
|
115
|
-
const input = (
|
|
116
|
-
<Input
|
|
117
|
-
ref={innerRef}
|
|
118
|
-
{...rest}
|
|
119
|
-
type={type}
|
|
120
|
-
name={name}
|
|
121
|
-
value={value}
|
|
122
|
-
valueSignal={valueSignal}
|
|
123
|
-
autoFocus
|
|
124
|
-
autoFocusVisible
|
|
125
|
-
autoSelect={autoSelect}
|
|
126
|
-
cancelOnEscape
|
|
127
|
-
cancelOnBlurInvalid
|
|
128
|
-
constraints={constraints}
|
|
129
|
-
required={required}
|
|
130
|
-
readOnly={readOnly}
|
|
131
|
-
min={min}
|
|
132
|
-
max={max}
|
|
133
|
-
step={step}
|
|
134
|
-
minLength={minLength}
|
|
135
|
-
maxLength={maxLength}
|
|
136
|
-
pattern={pattern}
|
|
137
|
-
width={width}
|
|
138
|
-
height={height}
|
|
139
|
-
onCancel={(e) => {
|
|
140
|
-
if (valueSignal) {
|
|
141
|
-
valueSignal.value = valueWhenEditStartRef.current;
|
|
142
|
-
}
|
|
143
|
-
onEditEnd({
|
|
144
|
-
cancelled: true,
|
|
145
|
-
event: e,
|
|
146
|
-
});
|
|
147
|
-
}}
|
|
148
|
-
onBlur={(e) => {
|
|
149
|
-
const value =
|
|
150
|
-
type === "number" ? e.target.valueAsNumber : e.target.value;
|
|
151
|
-
const valueWhenEditStart = valueWhenEditStartRef.current;
|
|
152
|
-
if (value === valueWhenEditStart) {
|
|
153
|
-
onEditEnd({
|
|
154
|
-
cancelled: true,
|
|
155
|
-
event: e,
|
|
156
|
-
});
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
}}
|
|
160
|
-
action={action || (() => {})}
|
|
161
|
-
onActionEnd={(e) => {
|
|
162
|
-
onEditEnd({
|
|
163
|
-
success: true,
|
|
164
|
-
event: e,
|
|
165
|
-
});
|
|
166
|
-
}}
|
|
167
|
-
/>
|
|
168
|
-
);
|
|
169
|
-
|
|
170
|
-
return (
|
|
171
|
-
<>
|
|
172
|
-
{children || <span>{value}</span>}
|
|
173
|
-
{editing && (
|
|
174
|
-
<div
|
|
175
|
-
{...wrapperProps}
|
|
176
|
-
className={[
|
|
177
|
-
"navi_editable_wrapper",
|
|
178
|
-
...(wrapperProps?.className || "").split(" "),
|
|
179
|
-
].join(" ")}
|
|
180
|
-
>
|
|
181
|
-
{input}
|
|
182
|
-
</div>
|
|
183
|
-
)}
|
|
184
|
-
</>
|
|
185
|
-
);
|
|
186
|
-
});
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { createContext } from "preact";
|
|
2
|
-
import { useContext } from "preact/hooks";
|
|
3
|
-
|
|
4
|
-
export const ErrorBoundaryContext = createContext(null);
|
|
5
|
-
|
|
6
|
-
export const useResetErrorBoundary = () => {
|
|
7
|
-
const resetErrorBoundary = useContext(ErrorBoundaryContext);
|
|
8
|
-
return resetErrorBoundary;
|
|
9
|
-
};
|
|
@@ -1,247 +0,0 @@
|
|
|
1
|
-
# UI State Controller
|
|
2
|
-
|
|
3
|
-
The UI State Controller solves a fundamental problem in web applications: managing the relationship between what the user sees (UI state) and what the application knows (external state).
|
|
4
|
-
|
|
5
|
-
## The Problem
|
|
6
|
-
|
|
7
|
-
Traditional approaches have limitations:
|
|
8
|
-
|
|
9
|
-
1. **React limitations** - No built-in way to revert UI state back to external state when needed
|
|
10
|
-
2. **Form limitations** - Regular forms can't do immediate server calls (like PATCH) for instant feedback
|
|
11
|
-
3. **UX trade-offs** - You're forced to choose between immediate feedback OR traditional form workflow
|
|
12
|
-
|
|
13
|
-
## The Solution
|
|
14
|
-
|
|
15
|
-
UI State Controller introduces clear separation:
|
|
16
|
-
|
|
17
|
-
- **External State**: The "source of truth" (props, backend data)
|
|
18
|
-
- **UI State**: What the user currently sees and interacts with
|
|
19
|
-
- **Controller**: Manages the relationship between them
|
|
20
|
-
|
|
21
|
-
There are **two distinct usage patterns** depending on your needs:
|
|
22
|
-
|
|
23
|
-
## Pattern 1: UI with Action (Auto-revert on Error)
|
|
24
|
-
|
|
25
|
-
For interactive components that need immediate feedback with server synchronization:
|
|
26
|
-
|
|
27
|
-
```jsx
|
|
28
|
-
const [savedValue, setSavedValue] = useState(false);
|
|
29
|
-
|
|
30
|
-
<InputCheckbox
|
|
31
|
-
checked={savedValue} // External state
|
|
32
|
-
action={async (
|
|
33
|
-
// newValue is undefined when unchecked, "on" when checked (default HTML behavior)
|
|
34
|
-
// Customize checked value: <InputCheckbox value="yes" /> sends "yes" instead of "on"
|
|
35
|
-
newValue,
|
|
36
|
-
) => {
|
|
37
|
-
// PATCH to update existing resource
|
|
38
|
-
const response = await fetch("/api/user/preferences", {
|
|
39
|
-
method: "PATCH",
|
|
40
|
-
headers: { "Content-Type": "application/json" },
|
|
41
|
-
body: JSON.stringify({ emailNotifications: newValue }),
|
|
42
|
-
});
|
|
43
|
-
const result = await response.json();
|
|
44
|
-
setSavedValue(result.emailNotifications);
|
|
45
|
-
}}
|
|
46
|
-
/>;
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
**How it works:**
|
|
50
|
-
|
|
51
|
-
1. User clicks checkbox → UI updates immediately (responsive)
|
|
52
|
-
2. Action executes in background
|
|
53
|
-
3. **Success**: External state updates, UI stays in sync
|
|
54
|
-
4. **Error**: UI automatically reverts to match external state (auto-revert)
|
|
55
|
-
|
|
56
|
-
**Use when:** You want immediate feedback with automatic error recovery.
|
|
57
|
-
|
|
58
|
-
## Pattern 2: UI within `<form>` (User Choice on Error)
|
|
59
|
-
|
|
60
|
-
For traditional form workflows where users control submission:
|
|
61
|
-
|
|
62
|
-
```jsx
|
|
63
|
-
<Form
|
|
64
|
-
action={async ({ email, consent }) => {
|
|
65
|
-
const response = await fetch("/api/user/settings", {
|
|
66
|
-
method: "POST",
|
|
67
|
-
headers: { "Content-Type": "application/json" },
|
|
68
|
-
body: JSON.stringify({
|
|
69
|
-
email,
|
|
70
|
-
consent,
|
|
71
|
-
}),
|
|
72
|
-
});
|
|
73
|
-
if (!response.ok) {
|
|
74
|
-
throw new Error("Failed to save settings");
|
|
75
|
-
}
|
|
76
|
-
// Update your app state here
|
|
77
|
-
const result = await response.json();
|
|
78
|
-
updateUserSettings(result);
|
|
79
|
-
}}
|
|
80
|
-
>
|
|
81
|
-
<label>
|
|
82
|
-
Email Address:
|
|
83
|
-
<Input type="email" name="email" defaultValue="user@example.com" required />
|
|
84
|
-
</label>
|
|
85
|
-
|
|
86
|
-
<label>
|
|
87
|
-
<InputCheckbox name="consent" />I agree to receive marketing emails
|
|
88
|
-
</label>
|
|
89
|
-
|
|
90
|
-
<button type="submit">Save Settings</button>
|
|
91
|
-
<button type="reset">Reset Form</button>
|
|
92
|
-
</Form>
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
**Key differences:**
|
|
96
|
-
|
|
97
|
-
- **Form submission errors**: UI state is NOT reverted
|
|
98
|
-
- **Reasoning**: User might want to fix the issue and re-submit as-is
|
|
99
|
-
- **Form reset**: UI state is properly restored to original values
|
|
100
|
-
- **Navigation**: Form state persists during page navigation
|
|
101
|
-
|
|
102
|
-
**Use when:** You want traditional form behavior with user control over submission.
|
|
103
|
-
|
|
104
|
-
## When to Use Each Pattern
|
|
105
|
-
|
|
106
|
-
### Use Action Pattern When:
|
|
107
|
-
|
|
108
|
-
- Building interactive dashboards or real-time interfaces
|
|
109
|
-
- Each change should be immediately persisted
|
|
110
|
-
- You want automatic error recovery
|
|
111
|
-
- User expects instant feedback
|
|
112
|
-
|
|
113
|
-
### Use Form Pattern When:
|
|
114
|
-
|
|
115
|
-
- Building traditional forms with submit/reset workflow
|
|
116
|
-
- Users need to make multiple changes before saving
|
|
117
|
-
- You want standard form validation behavior
|
|
118
|
-
- Users should control when changes are persisted
|
|
119
|
-
|
|
120
|
-
## Advanced APIs
|
|
121
|
-
|
|
122
|
-
### Tracking State Changes
|
|
123
|
-
|
|
124
|
-
Use `onUIStateChange` to track what the user has selected (like in our demo). This can be useful for showing what would be submitted/reset, though it's not always needed:
|
|
125
|
-
|
|
126
|
-
```jsx
|
|
127
|
-
const [colorChoices, setColorChoices] = useState([
|
|
128
|
-
{ id: 1, color: "red", selected: true },
|
|
129
|
-
{ id: 2, color: "blue", selected: false },
|
|
130
|
-
{ id: 3, color: "green", selected: true },
|
|
131
|
-
]);
|
|
132
|
-
|
|
133
|
-
// What's currently saved
|
|
134
|
-
const selectedColors = colorChoices
|
|
135
|
-
.filter((choice) => choice.selected)
|
|
136
|
-
.map((choice) => choice.color);
|
|
137
|
-
|
|
138
|
-
// What user has selected in UI (may differ)
|
|
139
|
-
const [uiSelectedColors, setUiSelectedColors] = useState(selectedColors);
|
|
140
|
-
|
|
141
|
-
<Form action={submitColorPreferences}>
|
|
142
|
-
<CheckboxList
|
|
143
|
-
name="colors"
|
|
144
|
-
onUIStateChange={(colors) => {
|
|
145
|
-
// Track what user has selected
|
|
146
|
-
setUiSelectedColors(colors);
|
|
147
|
-
// Can be used for UI feedback or internal logic
|
|
148
|
-
}}
|
|
149
|
-
>
|
|
150
|
-
{colorChoices.map(({ id, color }) => (
|
|
151
|
-
<Label key={id}>
|
|
152
|
-
{color}
|
|
153
|
-
<Checkbox value={color} checked={selectedColors.includes(color)} />
|
|
154
|
-
</Label>
|
|
155
|
-
))}
|
|
156
|
-
</CheckboxList>
|
|
157
|
-
|
|
158
|
-
<button type="submit">Submit ({uiSelectedColors.join(", ")})</button>
|
|
159
|
-
<button type="reset">Reset to saved ({selectedColors.join(", ")})</button>
|
|
160
|
-
</Form>;
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
### External Control via Custom Events
|
|
164
|
-
|
|
165
|
-
Programmatically control UI state from outside the component:
|
|
166
|
-
|
|
167
|
-
```jsx
|
|
168
|
-
// Set UI state externally
|
|
169
|
-
const checkbox = document.querySelector("#my-checkbox");
|
|
170
|
-
checkbox.dispatchEvent(
|
|
171
|
-
new CustomEvent("setuistate", {
|
|
172
|
-
detail: { value: true },
|
|
173
|
-
}),
|
|
174
|
-
);
|
|
175
|
-
|
|
176
|
-
// Reset UI state to match external state
|
|
177
|
-
checkbox.dispatchEvent(new CustomEvent("resetuistate"));
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
### Error Recovery Patterns
|
|
181
|
-
|
|
182
|
-
```jsx
|
|
183
|
-
<InputCheckbox
|
|
184
|
-
checked={savedValue}
|
|
185
|
-
action={updateServer}
|
|
186
|
-
onActionError={(error) => {
|
|
187
|
-
// UI already reverted automatically
|
|
188
|
-
showErrorMessage("Failed to save: " + error.message);
|
|
189
|
-
}}
|
|
190
|
-
onActionAbort={() => {
|
|
191
|
-
// UI reverted when action was cancelled
|
|
192
|
-
console.log("Action was cancelled");
|
|
193
|
-
}}
|
|
194
|
-
/>
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
### Group Controllers (Checkbox Lists)
|
|
198
|
-
|
|
199
|
-
Coordinate multiple related inputs:
|
|
200
|
-
|
|
201
|
-
```jsx
|
|
202
|
-
const [selectedOptions, setSelectedOptions] = useState([]);
|
|
203
|
-
|
|
204
|
-
<CheckboxList
|
|
205
|
-
values={selectedOptions} // External state
|
|
206
|
-
onUIStateChange={(uiState) => {
|
|
207
|
-
// Track what user has selected (internal use)
|
|
208
|
-
console.log("Currently selected:", uiState);
|
|
209
|
-
}}
|
|
210
|
-
action={async (newValues) => {
|
|
211
|
-
const response = await fetch("/api/options", {
|
|
212
|
-
method: "PATCH",
|
|
213
|
-
headers: { "Content-Type": "application/json" },
|
|
214
|
-
body: JSON.stringify({ selectedOptions: newValues }),
|
|
215
|
-
});
|
|
216
|
-
const result = await response.json();
|
|
217
|
-
setSelectedOptions(result.selectedOptions);
|
|
218
|
-
}}
|
|
219
|
-
>
|
|
220
|
-
<InputCheckbox value="option1" />
|
|
221
|
-
<InputCheckbox value="option2" />
|
|
222
|
-
<InputCheckbox value="option3" />
|
|
223
|
-
</CheckboxList>;
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
**Group features:**
|
|
227
|
-
|
|
228
|
-
- Aggregates individual checkbox states into arrays
|
|
229
|
-
- Coordinate reset operations across all children
|
|
230
|
-
- Single action handles all checkbox changes
|
|
231
|
-
|
|
232
|
-
## Key Benefits
|
|
233
|
-
|
|
234
|
-
- **Instant feedback**: UI updates immediately, no lag
|
|
235
|
-
- **Flexible error handling**: Auto-revert for actions, user choice for forms
|
|
236
|
-
- **Form compatibility**: Works seamlessly with native form behavior
|
|
237
|
-
- **External control**: Programmatic state control when needed
|
|
238
|
-
- **Group coordination**: Multiple inputs work together naturally
|
|
239
|
-
|
|
240
|
-
## Summary
|
|
241
|
-
|
|
242
|
-
Choose the pattern that fits your use case:
|
|
243
|
-
|
|
244
|
-
- **Action pattern**: For immediate persistence with auto-revert
|
|
245
|
-
- **Form pattern**: For traditional submit/reset workflows with user control
|
|
246
|
-
|
|
247
|
-
Both patterns provide responsive UI while maintaining data consistency.
|