@jsenv/navi 0.10.2 → 0.11.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.
- package/dist/jsenv_navi.js +13838 -23291
- package/dist/jsenv_navi.js.map +1281 -0
- package/package.json +6 -8
- 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 -193
- 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,461 +0,0 @@
|
|
|
1
|
-
# Item Tracker
|
|
2
|
-
|
|
3
|
-
A Preact hook system for tracking dynamic lists, designed to prevent infinite re-renders while enabling component composition similar to native HTML elements.
|
|
4
|
-
|
|
5
|
-
## The Problem
|
|
6
|
-
|
|
7
|
-
In React/Preact, you can wrap elements into components while maintaining the same compositional flexibility:
|
|
8
|
-
|
|
9
|
-
```jsx
|
|
10
|
-
// Native HTML
|
|
11
|
-
<select>
|
|
12
|
-
<option>Eastern</option>
|
|
13
|
-
<option>Central</option>
|
|
14
|
-
<option disabled>Mountain</option>
|
|
15
|
-
<option className="highlighted">Pacific</option>
|
|
16
|
-
</select>
|
|
17
|
-
|
|
18
|
-
// Component abstraction - same flexibility
|
|
19
|
-
<TimezoneSelect>
|
|
20
|
-
<TimezoneOption value="est">Eastern</TimezoneOption>
|
|
21
|
-
<TimezoneOption value="cst">Central</TimezoneOption>
|
|
22
|
-
<TimezoneOption value="mst" disabled>Mountain</TimezoneOption>
|
|
23
|
-
<TimezoneOption value="pst" className="highlighted">Pacific</TimezoneOption>
|
|
24
|
-
</TimezoneSelect>
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
However, when building components that need to coordinate between children, the parent component needs to know about its children's properties.
|
|
28
|
-
|
|
29
|
-
Consider a table where column definitions need to be shared with table cells:
|
|
30
|
-
|
|
31
|
-
```jsx
|
|
32
|
-
// Desired API - clean composition
|
|
33
|
-
<Table>
|
|
34
|
-
<colgroup>
|
|
35
|
-
<Col id="name" width="200px" sortable />
|
|
36
|
-
<Col id="email" width="300px" resizable />
|
|
37
|
-
<Col id="status" width="100px" />
|
|
38
|
-
</colgroup>
|
|
39
|
-
<tbody>
|
|
40
|
-
<tr>
|
|
41
|
-
<Cell column="name">{user.name}</Cell>
|
|
42
|
-
<Cell column="email">{user.email}</Cell>
|
|
43
|
-
<Cell column="status">{user.status}</Cell>
|
|
44
|
-
</tr>
|
|
45
|
-
</tbody>
|
|
46
|
-
</Table>
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
The challenge: `<Cell>` components need access to column configuration defined by `<Col>` components, but they're not in a parent-child relationship.
|
|
50
|
-
|
|
51
|
-
## Alternative Approaches
|
|
52
|
-
|
|
53
|
-
Most libraries use configuration objects instead of components:
|
|
54
|
-
|
|
55
|
-
```jsx
|
|
56
|
-
<Table
|
|
57
|
-
columns={[
|
|
58
|
-
{ id: "name", width: "200px", sortable: true },
|
|
59
|
-
{ id: "email", width: "300px", resizable: true },
|
|
60
|
-
{ id: "status", width: "100px" },
|
|
61
|
-
]}
|
|
62
|
-
data={tableData}
|
|
63
|
-
/>
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
This approach works but requires complex APIs when you need to customize individual columns:
|
|
67
|
-
|
|
68
|
-
```jsx
|
|
69
|
-
<Table
|
|
70
|
-
columns={columns}
|
|
71
|
-
getColumnProps={(column, index) => {
|
|
72
|
-
if (column.id === "email") return { className: "email-column" };
|
|
73
|
-
return {};
|
|
74
|
-
}}
|
|
75
|
-
cellRenderers={{
|
|
76
|
-
email: (value) => value.toLowerCase(),
|
|
77
|
-
}}
|
|
78
|
-
/>
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
With component composition, the same customization is more straightforward:
|
|
82
|
-
|
|
83
|
-
```jsx
|
|
84
|
-
<Table>
|
|
85
|
-
<Colgroup>
|
|
86
|
-
<Col id="name" width="200px" sortable />
|
|
87
|
-
<Col id="email" width="300px" resizable className="email-column" />
|
|
88
|
-
<Col id="status" width="100px" />
|
|
89
|
-
</Colgroup>
|
|
90
|
-
<Tbody>
|
|
91
|
-
<Tr>
|
|
92
|
-
<TableCell column="name">{user.name}</TableCell>
|
|
93
|
-
<TableCell column="email">{user.email.toLowerCase()}</TableCell>
|
|
94
|
-
<TableCell column="status">{user.status}</TableCell>
|
|
95
|
-
</Tr>
|
|
96
|
-
</Tbody>
|
|
97
|
-
</Table>
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
The problem is: how does the parent component discover its children's properties without causing infinite re-renders?
|
|
101
|
-
|
|
102
|
-
## Our Solution
|
|
103
|
-
|
|
104
|
-
The Item Tracker provides hooks that enable component composition while preventing infinite re-renders. It offers two approaches depending on your component structure:
|
|
105
|
-
|
|
106
|
-
**For library authors**, the system provides hooks to build components with clean APIs:
|
|
107
|
-
|
|
108
|
-
```jsx
|
|
109
|
-
// Internal implementation
|
|
110
|
-
function Table({ children }) {
|
|
111
|
-
const ColTrackerProvider = useItemTrackerProvider();
|
|
112
|
-
|
|
113
|
-
return (
|
|
114
|
-
<ColTrackerProvider>
|
|
115
|
-
<Table>
|
|
116
|
-
<TableHeaders /> {/* Reads column data */}
|
|
117
|
-
<Tbody>{children}</Tbody>
|
|
118
|
-
</Table>
|
|
119
|
-
</ColTrackerProvider>
|
|
120
|
-
);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function Col({ id, width, sortable, ...props }) {
|
|
124
|
-
const colIndex = useTrackItem({ id, width, sortable, ...props });
|
|
125
|
-
return <col style={{ width }} />;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function TableCell({ column, children }) {
|
|
129
|
-
const columns = useTrackedItems();
|
|
130
|
-
const colData = columns.find((col) => col.id === column);
|
|
131
|
-
return <td style={{ width: colData.width }}>{children}</td>;
|
|
132
|
-
}
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
**For end users**, this enables clean, component-based APIs:
|
|
136
|
-
|
|
137
|
-
```jsx
|
|
138
|
-
// User-facing API
|
|
139
|
-
<Table>
|
|
140
|
-
<Colgroup>
|
|
141
|
-
<Col id="name" width="200px" sortable className="name-col" />
|
|
142
|
-
<Col id="email" width="300px" resizable />
|
|
143
|
-
<Col id="status" width="100px" />
|
|
144
|
-
</Colgroup>
|
|
145
|
-
<Tbody>
|
|
146
|
-
<Tr>
|
|
147
|
-
<TableCell column="name">{user.name}</TableCell>
|
|
148
|
-
<TableCell column="email">{user.email}</TableCell>
|
|
149
|
-
<TableCell column="status">{user.status}</TableCell>
|
|
150
|
-
</Tr>
|
|
151
|
-
</Tbody>
|
|
152
|
-
</Table>
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
Key features:
|
|
156
|
-
|
|
157
|
-
- **Prevents infinite re-renders** through ref-based item registration
|
|
158
|
-
- **Enables true composition** - items can be registered anywhere in the component tree
|
|
159
|
-
- **No configuration objects** - each component configures itself through props
|
|
160
|
-
- **Dynamic lists** - supports adding, removing, and reordering items
|
|
161
|
-
|
|
162
|
-
## Two Systems for Different Use Cases
|
|
163
|
-
|
|
164
|
-
We provide two complementary systems:
|
|
165
|
-
|
|
166
|
-
### 1. Simple Item Tracker (Colocated)
|
|
167
|
-
|
|
168
|
-
For scenarios where registration and consumption happen in the same component tree:
|
|
169
|
-
|
|
170
|
-
```jsx
|
|
171
|
-
import { createItemTracker } from "./use_item_tracker.jsx";
|
|
172
|
-
|
|
173
|
-
const [useRowTrackerProvider, useRegisterRow, useRow, useRows] =
|
|
174
|
-
createItemTracker();
|
|
175
|
-
|
|
176
|
-
function App() {
|
|
177
|
-
const RowTrackerProvider = useRowTrackerProvider();
|
|
178
|
-
|
|
179
|
-
return (
|
|
180
|
-
<RowTrackerProvider>
|
|
181
|
-
<table>
|
|
182
|
-
<tbody>
|
|
183
|
-
{rows.map((data) => (
|
|
184
|
-
<TableRow key={data.id} data={data}>
|
|
185
|
-
<TableCell column="name" />
|
|
186
|
-
<TableCell column="email" />
|
|
187
|
-
</TableRow>
|
|
188
|
-
))}
|
|
189
|
-
</tbody>
|
|
190
|
-
</table>
|
|
191
|
-
</RowTrackerProvider>
|
|
192
|
-
);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function TableRow({ data, children }) {
|
|
196
|
-
const rowIndex = useRegisterRow(data);
|
|
197
|
-
return (
|
|
198
|
-
<RowContext.Provider value={rowIndex}>
|
|
199
|
-
<tr>{children}</tr>
|
|
200
|
-
</RowContext.Provider>
|
|
201
|
-
);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
function TableCell({ column }) {
|
|
205
|
-
const rowIndex = useContext(RowContext);
|
|
206
|
-
const rowData = useRow(rowIndex);
|
|
207
|
-
return <td>{rowData[column]}</td>;
|
|
208
|
-
}
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
### 2. Isolated Item Tracker (Separated Trees)
|
|
212
|
-
|
|
213
|
-
For complex scenarios where registration and consumption are in completely separate component trees:
|
|
214
|
-
|
|
215
|
-
```jsx
|
|
216
|
-
import { createIsolatedItemTracker } from "./use_item_tracker_isolated.jsx";
|
|
217
|
-
|
|
218
|
-
const [useColumnTrackerProviders, useRegisterColumn, useColumn, useColumns] =
|
|
219
|
-
createIsolatedItemTracker();
|
|
220
|
-
|
|
221
|
-
function App() {
|
|
222
|
-
const [ColumnProducerProvider, ColumnConsumerProvider] =
|
|
223
|
-
useColumnTrackerProviders();
|
|
224
|
-
|
|
225
|
-
return (
|
|
226
|
-
<div>
|
|
227
|
-
{/* Producer tree: Registers column data */}
|
|
228
|
-
<Table>
|
|
229
|
-
<Colgroup>
|
|
230
|
-
<ColumnProducerProvider>
|
|
231
|
-
{columns.map((col) => (
|
|
232
|
-
<Col key={col.id} {...col} />
|
|
233
|
-
))}
|
|
234
|
-
</ColumnProducerProvider>
|
|
235
|
-
</Colgroup>
|
|
236
|
-
{/* Table content */}
|
|
237
|
-
</Table>
|
|
238
|
-
|
|
239
|
-
{/* Consumer tree: Reads column data */}
|
|
240
|
-
<ColumnConsumerProvider>
|
|
241
|
-
<TableControls />
|
|
242
|
-
<ColumnSummary />
|
|
243
|
-
</ColumnConsumerProvider>
|
|
244
|
-
</div>
|
|
245
|
-
);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function Col({ id, label, width, sortable }) {
|
|
249
|
-
const columnIndex = useRegisterColumn({ id, label, width, sortable });
|
|
250
|
-
return <col style={{ width }} />;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
function TableControls() {
|
|
254
|
-
const columns = useColumns(); // Access all columns
|
|
255
|
-
return (
|
|
256
|
-
<div>
|
|
257
|
-
{columns.map((col, index) => (
|
|
258
|
-
<ColumnToggle key={col.id} columnIndex={index} />
|
|
259
|
-
))}
|
|
260
|
-
</div>
|
|
261
|
-
);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
function ColumnToggle({ columnIndex }) {
|
|
265
|
-
const column = useColumn(columnIndex); // Access specific column
|
|
266
|
-
return (
|
|
267
|
-
<button>
|
|
268
|
-
Toggle {column.label} ({column.width})
|
|
269
|
-
</button>
|
|
270
|
-
);
|
|
271
|
-
}
|
|
272
|
-
```
|
|
273
|
-
|
|
274
|
-
## Key Architecture Benefits
|
|
275
|
-
|
|
276
|
-
### ✅ No Infinite Re-renders
|
|
277
|
-
|
|
278
|
-
**Producer side uses refs** - Item registration doesn't trigger re-renders:
|
|
279
|
-
|
|
280
|
-
```jsx
|
|
281
|
-
// This is safe - no state updates during render
|
|
282
|
-
const index = useRegisterItem(data);
|
|
283
|
-
```
|
|
284
|
-
|
|
285
|
-
**Consumer side uses state** - Only consumers re-render when data changes:
|
|
286
|
-
|
|
287
|
-
```jsx
|
|
288
|
-
// Only this component re-renders when items change
|
|
289
|
-
const items = useTrackedItems();
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
### ✅ True Component Composition
|
|
293
|
-
|
|
294
|
-
Register items anywhere in the producer tree:
|
|
295
|
-
|
|
296
|
-
```jsx
|
|
297
|
-
<ColumnProducerProvider>
|
|
298
|
-
<div>
|
|
299
|
-
<SomeWrapper>
|
|
300
|
-
<Col id="name" width="200px" />
|
|
301
|
-
</SomeWrapper>
|
|
302
|
-
</div>
|
|
303
|
-
<AnotherComponent>
|
|
304
|
-
<Col id="email" width="300px" />
|
|
305
|
-
</AnotherComponent>
|
|
306
|
-
</ColumnProducerProvider>
|
|
307
|
-
```
|
|
308
|
-
|
|
309
|
-
Consume items anywhere in the consumer tree:
|
|
310
|
-
|
|
311
|
-
```jsx
|
|
312
|
-
<ColumnConsumerProvider>
|
|
313
|
-
<Header>
|
|
314
|
-
<ColumnSummary /> {/* Reads all columns */}
|
|
315
|
-
</Header>
|
|
316
|
-
<Sidebar>
|
|
317
|
-
<ColumnFilter columnIndex={0} /> {/* Reads specific column */}
|
|
318
|
-
</Sidebar>
|
|
319
|
-
</ColumnConsumerProvider>
|
|
320
|
-
```
|
|
321
|
-
|
|
322
|
-
### ✅ Controlled Synchronization
|
|
323
|
-
|
|
324
|
-
The isolated tracker synchronizes data at controlled moments:
|
|
325
|
-
|
|
326
|
-
- After producer tree renders completely
|
|
327
|
-
- Before consumer tree renders
|
|
328
|
-
- No intermediate state or partial updates
|
|
329
|
-
|
|
330
|
-
### ✅ Handles Dynamic Lists
|
|
331
|
-
|
|
332
|
-
Add, remove, and reorder items without breaking:
|
|
333
|
-
|
|
334
|
-
```jsx
|
|
335
|
-
// Columns can be added/removed dynamically
|
|
336
|
-
{
|
|
337
|
-
dynamicColumns.map((col) => <ColumnDefinition key={col.id} {...col} />);
|
|
338
|
-
}
|
|
339
|
-
```
|
|
340
|
-
|
|
341
|
-
## When to Use Which System
|
|
342
|
-
|
|
343
|
-
### Use Simple Item Tracker when:
|
|
344
|
-
|
|
345
|
-
- Registration and consumption happen in the same component tree
|
|
346
|
-
- Parent-child relationships exist between producers and consumers
|
|
347
|
-
- You need a straightforward, lightweight solution
|
|
348
|
-
|
|
349
|
-
**Examples:**
|
|
350
|
-
|
|
351
|
-
- Table rows registering themselves for cell access
|
|
352
|
-
- Navigation items registering for keyboard navigation
|
|
353
|
-
- Form fields registering for validation
|
|
354
|
-
|
|
355
|
-
### Use Isolated Item Tracker when:
|
|
356
|
-
|
|
357
|
-
- Registration and consumption happen in separate component trees
|
|
358
|
-
- No parent-child relationship exists between producers and consumers
|
|
359
|
-
- You need complex synchronization between distant components
|
|
360
|
-
|
|
361
|
-
**Examples:**
|
|
362
|
-
|
|
363
|
-
- HTML table colgroup → tbody communication
|
|
364
|
-
- Sidebar filters reading main content structure
|
|
365
|
-
- Toolbar controls accessing editor content
|
|
366
|
-
- Dashboard widgets reading data source definitions
|
|
367
|
-
|
|
368
|
-
## HTML Table Use Case (Primary Motivation)
|
|
369
|
-
|
|
370
|
-
The isolated tracker was specifically designed for HTML table structures:
|
|
371
|
-
|
|
372
|
-
```jsx
|
|
373
|
-
<Table>
|
|
374
|
-
{/* PRODUCER: Column definitions register metadata */}
|
|
375
|
-
<ColumnProducerProvider>
|
|
376
|
-
<Colgroup>
|
|
377
|
-
<Col
|
|
378
|
-
id="name"
|
|
379
|
-
label="Full Name"
|
|
380
|
-
width="200px"
|
|
381
|
-
sortable={true}
|
|
382
|
-
filterable={true}
|
|
383
|
-
/>
|
|
384
|
-
<Col
|
|
385
|
-
id="email"
|
|
386
|
-
label="Email Address"
|
|
387
|
-
width="300px"
|
|
388
|
-
sortable={true}
|
|
389
|
-
filterable={false}
|
|
390
|
-
/>
|
|
391
|
-
</Colgroup>
|
|
392
|
-
</ColumnProducerProvider>
|
|
393
|
-
|
|
394
|
-
{/* CONSUMER: Table cells read column metadata */}
|
|
395
|
-
<ColumnConsumerProvider>
|
|
396
|
-
<Thead>
|
|
397
|
-
<Tr>
|
|
398
|
-
<TableCell columnIndex={0} /> {/* Reads name column */}
|
|
399
|
-
<TableCell columnIndex={1} /> {/* Reads email column */}
|
|
400
|
-
</Tr>
|
|
401
|
-
</Thead>
|
|
402
|
-
<Tbody>
|
|
403
|
-
{data.map((row) => (
|
|
404
|
-
<Tr key={row.id}>
|
|
405
|
-
<TableCell columnIndex={0} value={row.name} />
|
|
406
|
-
<TableCell columnIndex={1} value={row.email} />
|
|
407
|
-
</Tr>
|
|
408
|
-
))}
|
|
409
|
-
</Tbody>
|
|
410
|
-
</ColumnConsumerProvider>
|
|
411
|
-
</Table>
|
|
412
|
-
```
|
|
413
|
-
|
|
414
|
-
This enables:
|
|
415
|
-
|
|
416
|
-
- **Semantic HTML structure** - Proper `<colgroup>` usage
|
|
417
|
-
- **Column metadata sharing** - Headers and cells access the same data
|
|
418
|
-
- **Dynamic column management** - Add/remove columns without breaking
|
|
419
|
-
- **Rich interactions** - Sorting, filtering, resizing based on column config
|
|
420
|
-
- **Accessibility** - ARIA attributes based on column metadata
|
|
421
|
-
|
|
422
|
-
## API Reference
|
|
423
|
-
|
|
424
|
-
### Simple Item Tracker
|
|
425
|
-
|
|
426
|
-
```jsx
|
|
427
|
-
const [
|
|
428
|
-
useItemTrackerProvider, // () => Provider component
|
|
429
|
-
useTrackItem, // (data) => index
|
|
430
|
-
useTrackedItem, // (index) => data
|
|
431
|
-
useTrackedItems, // () => data[]
|
|
432
|
-
] = createItemTracker();
|
|
433
|
-
```
|
|
434
|
-
|
|
435
|
-
### Isolated Item Tracker
|
|
436
|
-
|
|
437
|
-
```jsx
|
|
438
|
-
const [
|
|
439
|
-
useItemTrackerProviders, // () => [ProducerProvider, ConsumerProvider]
|
|
440
|
-
useRegisterItem, // (data) => index (use in producer)
|
|
441
|
-
useTrackedItem, // (index) => data (use in consumer)
|
|
442
|
-
useTrackedItems, // () => data[] (use in consumer)
|
|
443
|
-
] = createIsolatedItemTracker();
|
|
444
|
-
```
|
|
445
|
-
|
|
446
|
-
## Performance Characteristics
|
|
447
|
-
|
|
448
|
-
- **Producer registration**: O(1) - No re-renders, direct ref updates
|
|
449
|
-
- **Consumer access**: O(1) - Direct array access by index
|
|
450
|
-
- **Synchronization**: O(n) - One-time copy from refs to state
|
|
451
|
-
- **Memory**: O(n) - Stores one copy in refs, one copy in state during sync
|
|
452
|
-
|
|
453
|
-
## Browser Support
|
|
454
|
-
|
|
455
|
-
Requires modern JavaScript features:
|
|
456
|
-
|
|
457
|
-
- ES Modules
|
|
458
|
-
- Preact/React hooks
|
|
459
|
-
- `useLayoutEffect` for synchronization timing
|
|
460
|
-
|
|
461
|
-
Compatible with all modern browsers and Node.js environments.
|
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
// https://github.com/reach/reach-ui/tree/b3d94d22811db6b5c0f272b9a7e2e3c1bb4699ae/packages/descendants
|
|
2
|
-
// https://github.com/pacocoursey/use-descendants/tree/master
|
|
3
|
-
|
|
4
|
-
/*
|
|
5
|
-
* Item Tracker Isolated System - A Preact hook for tracking dynamic lists without infinite re-renders
|
|
6
|
-
*
|
|
7
|
-
* USE CASE:
|
|
8
|
-
* This is specifically designed for scenarios where item registration and usage are SEPARATE,
|
|
9
|
-
* such as HTML tables with colgroup elements where colgroup registers columns and tbody uses them.
|
|
10
|
-
*
|
|
11
|
-
* For simpler cases where item definition and usage are colocated (same component tree),
|
|
12
|
-
* prefer useItemTracker instead.
|
|
13
|
-
*
|
|
14
|
-
* SOLUTION ARCHITECTURE:
|
|
15
|
-
* This system uses a Producer/Consumer pattern with separate context trees:
|
|
16
|
-
*
|
|
17
|
-
* 1. PRODUCER SIDE (ref-based, no re-renders):
|
|
18
|
-
* - ItemProducerProvider: Manages item registration without causing re-renders
|
|
19
|
-
* - useTrackItem: Registers individual items using refs
|
|
20
|
-
* - Items are stored in a mutable array via useRef
|
|
21
|
-
*
|
|
22
|
-
* 2. CONSUMER SIDE (state-based, re-renders when needed):
|
|
23
|
-
* - ItemConsumerProvider: Manages reactive state for consumers
|
|
24
|
-
* - useTrackedItems/useTrackedItem: Read the tracked items with reactivity
|
|
25
|
-
* - State is synchronized from producer side at controlled intervals
|
|
26
|
-
*
|
|
27
|
-
* RENDER ORDER REQUIREMENT:
|
|
28
|
-
* Producer MUST render before Consumer in the React tree for proper synchronization.
|
|
29
|
-
*/
|
|
30
|
-
|
|
31
|
-
import { createContext } from "preact";
|
|
32
|
-
import {
|
|
33
|
-
useContext,
|
|
34
|
-
useLayoutEffect,
|
|
35
|
-
useMemo,
|
|
36
|
-
useRef,
|
|
37
|
-
useState,
|
|
38
|
-
} from "preact/hooks";
|
|
39
|
-
|
|
40
|
-
import { compareTwoJsValues } from "../../utils/compare_two_js_values.js";
|
|
41
|
-
|
|
42
|
-
export const createIsolatedItemTracker = () => {
|
|
43
|
-
// Producer contexts (ref-based, no re-renders)
|
|
44
|
-
const ProducerTrackerContext = createContext();
|
|
45
|
-
const ProducerItemCountRefContext = createContext();
|
|
46
|
-
const ProducerListRenderIdContext = createContext();
|
|
47
|
-
|
|
48
|
-
// Consumer contexts (state-based, re-renders)
|
|
49
|
-
const ConsumerItemsContext = createContext();
|
|
50
|
-
|
|
51
|
-
const useIsolatedItemTrackerProvider = () => {
|
|
52
|
-
const itemsRef = useRef([]);
|
|
53
|
-
const items = itemsRef.current;
|
|
54
|
-
const itemCountRef = useRef();
|
|
55
|
-
const pendingFlushRef = useRef(false);
|
|
56
|
-
const producerIsRenderingRef = useRef(false);
|
|
57
|
-
|
|
58
|
-
const itemTracker = useMemo(() => {
|
|
59
|
-
const registerItem = (index, value) => {
|
|
60
|
-
const hasValue = index in items;
|
|
61
|
-
if (hasValue) {
|
|
62
|
-
const currentValue = items[index];
|
|
63
|
-
if (compareTwoJsValues(currentValue, value)) {
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
items[index] = value;
|
|
69
|
-
|
|
70
|
-
if (producerIsRenderingRef.current) {
|
|
71
|
-
// Consumer will sync after producer render completes
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
pendingFlushRef.current = true;
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
const getProducerItem = (itemIndex) => {
|
|
79
|
-
return items[itemIndex];
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
const ItemProducerProvider = ({ children }) => {
|
|
83
|
-
items.length = 0;
|
|
84
|
-
itemCountRef.current = 0;
|
|
85
|
-
pendingFlushRef.current = false;
|
|
86
|
-
producerIsRenderingRef.current = true;
|
|
87
|
-
const listRenderId = {};
|
|
88
|
-
|
|
89
|
-
useLayoutEffect(() => {
|
|
90
|
-
producerIsRenderingRef.current = false;
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
// CRITICAL: Sync consumer state on subsequent renders
|
|
94
|
-
const renderedOnce = useRef(false);
|
|
95
|
-
useLayoutEffect(() => {
|
|
96
|
-
if (!renderedOnce.current) {
|
|
97
|
-
renderedOnce.current = true;
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
pendingFlushRef.current = true;
|
|
101
|
-
itemTracker.flushToConsumers();
|
|
102
|
-
}, [listRenderId]);
|
|
103
|
-
|
|
104
|
-
return (
|
|
105
|
-
<ProducerItemCountRefContext.Provider value={itemCountRef}>
|
|
106
|
-
<ProducerListRenderIdContext.Provider value={listRenderId}>
|
|
107
|
-
<ProducerTrackerContext.Provider value={itemTracker}>
|
|
108
|
-
{children}
|
|
109
|
-
</ProducerTrackerContext.Provider>
|
|
110
|
-
</ProducerListRenderIdContext.Provider>
|
|
111
|
-
</ProducerItemCountRefContext.Provider>
|
|
112
|
-
);
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
const ItemConsumerProvider = ({ children }) => {
|
|
116
|
-
const [consumerItems, setConsumerItems] = useState(items);
|
|
117
|
-
|
|
118
|
-
const flushToConsumers = () => {
|
|
119
|
-
if (!pendingFlushRef.current) {
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
const itemsCopy = [...items];
|
|
123
|
-
pendingFlushRef.current = false;
|
|
124
|
-
setConsumerItems(itemsCopy);
|
|
125
|
-
};
|
|
126
|
-
itemTracker.flushToConsumers = flushToConsumers;
|
|
127
|
-
|
|
128
|
-
useLayoutEffect(() => {
|
|
129
|
-
flushToConsumers();
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
return (
|
|
133
|
-
<ConsumerItemsContext.Provider value={consumerItems}>
|
|
134
|
-
{children}
|
|
135
|
-
</ConsumerItemsContext.Provider>
|
|
136
|
-
);
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
return {
|
|
140
|
-
pendingFlushRef,
|
|
141
|
-
registerItem,
|
|
142
|
-
getProducerItem,
|
|
143
|
-
ItemProducerProvider,
|
|
144
|
-
ItemConsumerProvider,
|
|
145
|
-
};
|
|
146
|
-
}, []);
|
|
147
|
-
|
|
148
|
-
const { ItemProducerProvider, ItemConsumerProvider } = itemTracker;
|
|
149
|
-
|
|
150
|
-
return [ItemProducerProvider, ItemConsumerProvider, items];
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
// Hook for producers to register items (ref-based, no re-renders)
|
|
154
|
-
const useTrackIsolatedItem = (data) => {
|
|
155
|
-
const listRenderId = useContext(ProducerListRenderIdContext);
|
|
156
|
-
const itemCountRef = useContext(ProducerItemCountRefContext);
|
|
157
|
-
const itemTracker = useContext(ProducerTrackerContext);
|
|
158
|
-
const listRenderIdRef = useRef();
|
|
159
|
-
const itemIndexRef = useRef();
|
|
160
|
-
const dataRef = useRef();
|
|
161
|
-
const prevListRenderId = listRenderIdRef.current;
|
|
162
|
-
|
|
163
|
-
useLayoutEffect(() => {
|
|
164
|
-
if (itemTracker.pendingFlushRef.current) {
|
|
165
|
-
itemTracker.flushToConsumers();
|
|
166
|
-
}
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
if (prevListRenderId === listRenderId) {
|
|
170
|
-
const itemIndex = itemIndexRef.current;
|
|
171
|
-
itemTracker.registerItem(itemIndex, data);
|
|
172
|
-
dataRef.current = data;
|
|
173
|
-
return itemIndex;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
listRenderIdRef.current = listRenderId;
|
|
177
|
-
const itemCount = itemCountRef.current;
|
|
178
|
-
const itemIndex = itemCount;
|
|
179
|
-
itemCountRef.current = itemIndex + 1;
|
|
180
|
-
itemIndexRef.current = itemIndex;
|
|
181
|
-
dataRef.current = data;
|
|
182
|
-
itemTracker.registerItem(itemIndex, data);
|
|
183
|
-
return itemIndex;
|
|
184
|
-
};
|
|
185
|
-
|
|
186
|
-
const useTrackedIsolatedItem = (itemIndex) => {
|
|
187
|
-
const items = useTrackedIsolatedItems();
|
|
188
|
-
const item = items[itemIndex];
|
|
189
|
-
return item;
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
// Hooks for consumers to read items (state-based, re-renders)
|
|
193
|
-
const useTrackedIsolatedItems = () => {
|
|
194
|
-
const consumerItems = useContext(ConsumerItemsContext);
|
|
195
|
-
if (!consumerItems) {
|
|
196
|
-
throw new Error(
|
|
197
|
-
"useTrackedIsolatedItems must be used within <ItemConsumerProvider />",
|
|
198
|
-
);
|
|
199
|
-
}
|
|
200
|
-
return consumerItems;
|
|
201
|
-
};
|
|
202
|
-
|
|
203
|
-
return [
|
|
204
|
-
useIsolatedItemTrackerProvider,
|
|
205
|
-
useTrackIsolatedItem,
|
|
206
|
-
useTrackedIsolatedItem,
|
|
207
|
-
useTrackedIsolatedItems,
|
|
208
|
-
];
|
|
209
|
-
};
|