pyreact-framework 1.0.0__py3-none-any.whl
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.
- pyreact/__init__.py +144 -0
- pyreact/cli/__init__.py +3 -0
- pyreact/cli/main.py +512 -0
- pyreact/core/__init__.py +80 -0
- pyreact/core/component.py +372 -0
- pyreact/core/context.py +173 -0
- pyreact/core/element.py +208 -0
- pyreact/core/error_boundary.py +145 -0
- pyreact/core/hooks.py +550 -0
- pyreact/core/memo.py +221 -0
- pyreact/core/portal.py +159 -0
- pyreact/core/reconciler.py +399 -0
- pyreact/core/refs.py +213 -0
- pyreact/core/renderer.py +112 -0
- pyreact/core/scheduler.py +304 -0
- pyreact/devtools/__init__.py +18 -0
- pyreact/devtools/debugger.py +314 -0
- pyreact/devtools/profiler.py +288 -0
- pyreact/dom/__init__.py +64 -0
- pyreact/dom/attributes.py +317 -0
- pyreact/dom/dom_operations.py +333 -0
- pyreact/dom/events.py +349 -0
- pyreact/server/__init__.py +34 -0
- pyreact/server/hydration.py +216 -0
- pyreact/server/ssr.py +344 -0
- pyreact/styles/__init__.py +19 -0
- pyreact/styles/css_module.py +231 -0
- pyreact/styles/styled.py +303 -0
- pyreact/testing/__init__.py +71 -0
- pyreact/testing/fire_event.py +355 -0
- pyreact/testing/screen.py +267 -0
- pyreact/testing/test_renderer.py +232 -0
- pyreact/utils/__init__.py +17 -0
- pyreact/utils/diff.py +182 -0
- pyreact/utils/object_pool.py +216 -0
- pyreact_framework-1.0.0.dist-info/METADATA +363 -0
- pyreact_framework-1.0.0.dist-info/RECORD +41 -0
- pyreact_framework-1.0.0.dist-info/WHEEL +5 -0
- pyreact_framework-1.0.0.dist-info/entry_points.txt +2 -0
- pyreact_framework-1.0.0.dist-info/licenses/LICENSE +21 -0
- pyreact_framework-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
"""Reconciler Module
|
|
2
|
+
|
|
3
|
+
This module implements the diff algorithm for comparing VNodes and efficiently updating the DOM.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any, Callable, Dict, List, Optional, Union, Set
|
|
7
|
+
from .element import VNode
|
|
8
|
+
from .component import Component
|
|
9
|
+
from ..dom import dom_operations
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Reconciler:
|
|
13
|
+
"""
|
|
14
|
+
Reconciler implements the diff algorithm for efficient DOM updates.
|
|
15
|
+
|
|
16
|
+
The algorithm follows these principles:
|
|
17
|
+
1. Elements of different types → replace completely
|
|
18
|
+
2. Elements of same type → update attributes/props
|
|
19
|
+
3. Children with keys → reorder/move instead of recreate
|
|
20
|
+
4. Components → compare props and decide if re-render needed
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self):
|
|
24
|
+
self._component_instances: Dict[int, Component] = {}
|
|
25
|
+
|
|
26
|
+
def diff(
|
|
27
|
+
self,
|
|
28
|
+
old_vnode: Optional[VNode],
|
|
29
|
+
new_vnode: Optional[VNode],
|
|
30
|
+
parent_dom: Any,
|
|
31
|
+
index: int = 0
|
|
32
|
+
) -> Optional[VNode]:
|
|
33
|
+
"""
|
|
34
|
+
Compare two VNodes and apply changes to DOM
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
old_vnode: Previous VNode (or None for new)
|
|
38
|
+
new_vnode: New VNode (or None for removal)
|
|
39
|
+
parent_dom: Parent DOM element
|
|
40
|
+
index: Child index in parent
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
VNode: The new VNode (may be reused or new)
|
|
44
|
+
"""
|
|
45
|
+
# Case 1: New node is None → remove
|
|
46
|
+
if new_vnode is None:
|
|
47
|
+
if old_vnode:
|
|
48
|
+
self._remove_node(parent_dom, old_vnode, index)
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
# Case 2: Old node is None → create
|
|
52
|
+
if old_vnode is None:
|
|
53
|
+
new_dom = self.create_dom(new_vnode)
|
|
54
|
+
self._insert_node(parent_dom, new_dom, index)
|
|
55
|
+
return new_vnode
|
|
56
|
+
|
|
57
|
+
# Case 3: Different types → replace
|
|
58
|
+
if self._get_type(old_vnode) != self._get_type(new_vnode):
|
|
59
|
+
new_dom = self.create_dom(new_vnode)
|
|
60
|
+
self._replace_node(parent_dom, new_dom, old_vnode, index)
|
|
61
|
+
return new_vnode
|
|
62
|
+
|
|
63
|
+
# Case 4: Same type → update
|
|
64
|
+
if self._is_component(new_vnode):
|
|
65
|
+
return self._update_component(old_vnode, new_vnode)
|
|
66
|
+
else:
|
|
67
|
+
return self._update_dom_element(old_vnode, new_vnode)
|
|
68
|
+
|
|
69
|
+
def create_dom(self, vnode: VNode) -> Any:
|
|
70
|
+
"""
|
|
71
|
+
Create a DOM node from a VNode
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
vnode: Virtual node to create
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
DOM node
|
|
78
|
+
"""
|
|
79
|
+
# Text node
|
|
80
|
+
if isinstance(vnode.type, str) and vnode.type == '#text':
|
|
81
|
+
dom = self._create_text_node(vnode.children[0] if vnode.children else '')
|
|
82
|
+
vnode._dom_node = dom
|
|
83
|
+
return dom
|
|
84
|
+
|
|
85
|
+
# Component
|
|
86
|
+
if callable(vnode.type) and not isinstance(vnode.type, str):
|
|
87
|
+
return self._create_component_dom(vnode)
|
|
88
|
+
|
|
89
|
+
# HTML element
|
|
90
|
+
dom = self._create_element(vnode.type)
|
|
91
|
+
vnode._dom_node = dom
|
|
92
|
+
|
|
93
|
+
# Apply props
|
|
94
|
+
self._apply_props(dom, {}, vnode.props)
|
|
95
|
+
|
|
96
|
+
# Apply ref
|
|
97
|
+
if vnode.ref:
|
|
98
|
+
vnode.ref.current = dom
|
|
99
|
+
|
|
100
|
+
# Create children
|
|
101
|
+
for child in vnode.children:
|
|
102
|
+
if isinstance(child, str):
|
|
103
|
+
text_node = self._create_text_node(child)
|
|
104
|
+
dom.append_child(text_node)
|
|
105
|
+
elif isinstance(child, VNode):
|
|
106
|
+
child_dom = self.create_dom(child)
|
|
107
|
+
dom.append_child(child_dom)
|
|
108
|
+
|
|
109
|
+
return dom
|
|
110
|
+
|
|
111
|
+
def _create_component_dom(self, vnode: VNode) -> Any:
|
|
112
|
+
"""Create DOM for a component"""
|
|
113
|
+
from .hooks import _set_current_component, _reset_hook_index
|
|
114
|
+
|
|
115
|
+
component_type = vnode.type
|
|
116
|
+
|
|
117
|
+
# Instantiate component
|
|
118
|
+
if isinstance(component_type, type):
|
|
119
|
+
# Class component
|
|
120
|
+
component = component_type(vnode.props)
|
|
121
|
+
else:
|
|
122
|
+
# Function component - wrap in a simple component
|
|
123
|
+
component = _FunctionComponent(vnode.type, vnode.props)
|
|
124
|
+
|
|
125
|
+
component._vnode = vnode
|
|
126
|
+
component._hooks = []
|
|
127
|
+
vnode._component_instance = component
|
|
128
|
+
|
|
129
|
+
# Set component context for hooks
|
|
130
|
+
_set_current_component(component)
|
|
131
|
+
_reset_hook_index()
|
|
132
|
+
|
|
133
|
+
# Render component
|
|
134
|
+
rendered = component.render()
|
|
135
|
+
|
|
136
|
+
# Reset context
|
|
137
|
+
_set_current_component(None)
|
|
138
|
+
|
|
139
|
+
if rendered is None:
|
|
140
|
+
# Render nothing
|
|
141
|
+
return self._create_comment('empty')
|
|
142
|
+
|
|
143
|
+
dom = self.create_dom(rendered)
|
|
144
|
+
component._dom_node = dom
|
|
145
|
+
vnode._dom_node = dom
|
|
146
|
+
|
|
147
|
+
# Call lifecycle
|
|
148
|
+
component.component_did_mount()
|
|
149
|
+
|
|
150
|
+
return dom
|
|
151
|
+
|
|
152
|
+
def _update_component(self, old_vnode: VNode, new_vnode: VNode) -> VNode:
|
|
153
|
+
"""Update a component"""
|
|
154
|
+
from .hooks import _set_current_component, _reset_hook_index
|
|
155
|
+
|
|
156
|
+
old_component = old_vnode._component_instance
|
|
157
|
+
new_props = new_vnode.props
|
|
158
|
+
|
|
159
|
+
# Check if should update
|
|
160
|
+
should_update = True
|
|
161
|
+
if hasattr(old_component, 'should_component_update'):
|
|
162
|
+
should_update = old_component.should_component_update(
|
|
163
|
+
new_props, old_component.state
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# Update props
|
|
167
|
+
old_component.props = new_props
|
|
168
|
+
new_vnode._component_instance = old_component
|
|
169
|
+
new_vnode._dom_node = old_vnode._dom_node
|
|
170
|
+
|
|
171
|
+
if should_update:
|
|
172
|
+
# Set component context for hooks
|
|
173
|
+
_set_current_component(old_component)
|
|
174
|
+
_reset_hook_index()
|
|
175
|
+
|
|
176
|
+
# Re-render
|
|
177
|
+
old_rendered = old_vnode._component_instance._vnode
|
|
178
|
+
new_rendered = old_component.render()
|
|
179
|
+
|
|
180
|
+
# Reset context
|
|
181
|
+
_set_current_component(None)
|
|
182
|
+
|
|
183
|
+
if new_rendered is None:
|
|
184
|
+
# Remove
|
|
185
|
+
if old_rendered:
|
|
186
|
+
self._remove_node(
|
|
187
|
+
old_vnode._dom_node.parent_node,
|
|
188
|
+
old_rendered,
|
|
189
|
+
0
|
|
190
|
+
)
|
|
191
|
+
else:
|
|
192
|
+
# Diff
|
|
193
|
+
self.diff(old_rendered, new_rendered, old_vnode._dom_node.parent_node)
|
|
194
|
+
old_component._vnode = new_rendered
|
|
195
|
+
|
|
196
|
+
# Call lifecycle
|
|
197
|
+
if hasattr(old_component, 'component_did_update'):
|
|
198
|
+
old_component.component_did_update(old_vnode.props, old_component.state)
|
|
199
|
+
|
|
200
|
+
return new_vnode
|
|
201
|
+
|
|
202
|
+
def _update_dom_element(self, old_vnode: VNode, new_vnode: VNode) -> VNode:
|
|
203
|
+
"""Update a DOM element"""
|
|
204
|
+
dom = old_vnode._dom_node
|
|
205
|
+
new_vnode._dom_node = dom
|
|
206
|
+
|
|
207
|
+
# Update props
|
|
208
|
+
self._apply_props(dom, old_vnode.props, new_vnode.props)
|
|
209
|
+
|
|
210
|
+
# Update ref
|
|
211
|
+
if new_vnode.ref != old_vnode.ref:
|
|
212
|
+
if old_vnode.ref:
|
|
213
|
+
old_vnode.ref.current = None
|
|
214
|
+
if new_vnode.ref:
|
|
215
|
+
new_vnode.ref.current = dom
|
|
216
|
+
|
|
217
|
+
# Reconcile children
|
|
218
|
+
self._reconcile_children(old_vnode, new_vnode, dom)
|
|
219
|
+
|
|
220
|
+
return new_vnode
|
|
221
|
+
|
|
222
|
+
def _reconcile_children(
|
|
223
|
+
self,
|
|
224
|
+
old_vnode: VNode,
|
|
225
|
+
new_vnode: VNode,
|
|
226
|
+
parent_dom: Any
|
|
227
|
+
) -> None:
|
|
228
|
+
"""
|
|
229
|
+
Reconcile children using keys for minimal DOM operations
|
|
230
|
+
"""
|
|
231
|
+
old_children = old_vnode.children
|
|
232
|
+
new_children = new_vnode.children
|
|
233
|
+
|
|
234
|
+
# Build key map for old children
|
|
235
|
+
old_keyed: Dict[Union[str, int], tuple] = {}
|
|
236
|
+
old_index = 0
|
|
237
|
+
for child in old_children:
|
|
238
|
+
if isinstance(child, VNode) and child.key is not None:
|
|
239
|
+
old_keyed[child.key] = (old_index, child)
|
|
240
|
+
old_index += 1
|
|
241
|
+
|
|
242
|
+
# Track which old children are used
|
|
243
|
+
used_keys: Set[Union[str, int]] = set()
|
|
244
|
+
|
|
245
|
+
# Process new children
|
|
246
|
+
new_index = 0
|
|
247
|
+
for new_child in new_children:
|
|
248
|
+
child_key = new_child.key if isinstance(new_child, VNode) else None
|
|
249
|
+
|
|
250
|
+
if child_key is not None and child_key in old_keyed:
|
|
251
|
+
# Reuse existing child
|
|
252
|
+
old_idx, old_child = old_keyed[child_key]
|
|
253
|
+
used_keys.add(child_key)
|
|
254
|
+
|
|
255
|
+
# Move if needed
|
|
256
|
+
if old_idx != new_index:
|
|
257
|
+
self._move_child(parent_dom, old_idx, new_index)
|
|
258
|
+
|
|
259
|
+
# Diff
|
|
260
|
+
self.diff(old_child, new_child, parent_dom, new_index)
|
|
261
|
+
else:
|
|
262
|
+
# Create new child
|
|
263
|
+
if isinstance(new_child, str):
|
|
264
|
+
text_node = self._create_text_node(new_child)
|
|
265
|
+
self._insert_node(parent_dom, text_node, new_index)
|
|
266
|
+
elif isinstance(new_child, VNode):
|
|
267
|
+
child_dom = self.create_dom(new_child)
|
|
268
|
+
self._insert_node(parent_dom, child_dom, new_index)
|
|
269
|
+
|
|
270
|
+
new_index += 1
|
|
271
|
+
|
|
272
|
+
# Remove unused old children
|
|
273
|
+
for key, (idx, child) in old_keyed.items():
|
|
274
|
+
if key not in used_keys:
|
|
275
|
+
self._remove_node(parent_dom, child, idx)
|
|
276
|
+
|
|
277
|
+
def unmount(self, vnode: VNode) -> None:
|
|
278
|
+
"""
|
|
279
|
+
Unmount a VNode and its children
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
vnode: VNode to unmount
|
|
283
|
+
"""
|
|
284
|
+
if vnode._component_instance:
|
|
285
|
+
component = vnode._component_instance
|
|
286
|
+
component.component_will_unmount()
|
|
287
|
+
|
|
288
|
+
# Unmount children
|
|
289
|
+
for child in vnode.children:
|
|
290
|
+
if isinstance(child, VNode):
|
|
291
|
+
self.unmount(child)
|
|
292
|
+
|
|
293
|
+
# Clear ref
|
|
294
|
+
if vnode.ref:
|
|
295
|
+
vnode.ref.current = None
|
|
296
|
+
|
|
297
|
+
# DOM Operations - using dom_operations module
|
|
298
|
+
|
|
299
|
+
def _create_element(self, tag: str) -> Any:
|
|
300
|
+
"""Create a DOM element"""
|
|
301
|
+
return dom_operations.create_element(tag)
|
|
302
|
+
|
|
303
|
+
def _create_text_node(self, text: str) -> Any:
|
|
304
|
+
"""Create a text node"""
|
|
305
|
+
return dom_operations.create_text_node(text)
|
|
306
|
+
|
|
307
|
+
def _create_comment(self, text: str) -> Any:
|
|
308
|
+
"""Create a comment node"""
|
|
309
|
+
# For now, use a text node as fallback
|
|
310
|
+
return dom_operations.create_text_node(f'<!-- {text} -->')
|
|
311
|
+
|
|
312
|
+
def _apply_props(self, dom: Any, old_props: Dict, new_props: Dict) -> None:
|
|
313
|
+
"""Apply props to DOM element"""
|
|
314
|
+
# Remove old props
|
|
315
|
+
for key in old_props:
|
|
316
|
+
if key not in new_props:
|
|
317
|
+
self._remove_prop(dom, key, old_props[key])
|
|
318
|
+
|
|
319
|
+
# Set new props
|
|
320
|
+
for key, value in new_props.items():
|
|
321
|
+
if old_props.get(key) != value:
|
|
322
|
+
self._set_prop(dom, key, value)
|
|
323
|
+
|
|
324
|
+
def _set_prop(self, dom: Any, key: str, value: Any) -> None:
|
|
325
|
+
"""Set a single prop on DOM element"""
|
|
326
|
+
if key == 'className':
|
|
327
|
+
dom.set_attribute('class', value)
|
|
328
|
+
elif key == 'style' and isinstance(value, dict):
|
|
329
|
+
for style_key, style_value in value.items():
|
|
330
|
+
dom.set_style(style_key, style_value)
|
|
331
|
+
elif key.startswith('on'):
|
|
332
|
+
# Event handler
|
|
333
|
+
event_name = key[2:].lower()
|
|
334
|
+
dom.add_event_listener(event_name, value)
|
|
335
|
+
elif key == 'dangerouslySetInnerHTML':
|
|
336
|
+
dom.set_inner_html(value.get('__html', ''))
|
|
337
|
+
else:
|
|
338
|
+
dom.set_attribute(key, value)
|
|
339
|
+
|
|
340
|
+
def _remove_prop(self, dom: Any, key: str, value: Any) -> None:
|
|
341
|
+
"""Remove a prop from DOM element"""
|
|
342
|
+
if key == 'className':
|
|
343
|
+
dom.remove_attribute('class')
|
|
344
|
+
elif key == 'style':
|
|
345
|
+
dom.remove_attribute('style')
|
|
346
|
+
elif key.startswith('on'):
|
|
347
|
+
event_name = key[2:].lower()
|
|
348
|
+
dom.remove_event_listener(event_name)
|
|
349
|
+
else:
|
|
350
|
+
dom.remove_attribute(key)
|
|
351
|
+
|
|
352
|
+
def _insert_node(self, parent: Any, node: Any, index: int) -> None:
|
|
353
|
+
"""Insert node at index"""
|
|
354
|
+
parent.insert_child(node, index)
|
|
355
|
+
|
|
356
|
+
def _remove_node(self, parent: Any, vnode: VNode, index: int) -> None:
|
|
357
|
+
"""Remove node at index"""
|
|
358
|
+
parent.remove_child_at(index)
|
|
359
|
+
self.unmount(vnode)
|
|
360
|
+
|
|
361
|
+
def _replace_node(self, parent: Any, new_node: Any, old_vnode: VNode, index: int) -> None:
|
|
362
|
+
"""Replace node at index"""
|
|
363
|
+
self.unmount(old_vnode)
|
|
364
|
+
parent.replace_child_at(new_node, index)
|
|
365
|
+
|
|
366
|
+
def _move_child(self, parent: Any, old_index: int, new_index: int) -> None:
|
|
367
|
+
"""Move child from old_index to new_index"""
|
|
368
|
+
parent.move_child(old_index, new_index)
|
|
369
|
+
|
|
370
|
+
def _get_type(self, vnode: VNode) -> Any:
|
|
371
|
+
"""Get the type of a VNode for comparison"""
|
|
372
|
+
return vnode.type
|
|
373
|
+
|
|
374
|
+
def _is_component(self, vnode: VNode) -> bool:
|
|
375
|
+
"""Check if VNode is a component"""
|
|
376
|
+
return callable(vnode.type) and not isinstance(vnode.type, str)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
class _FunctionComponent:
|
|
380
|
+
"""Wrapper for function components"""
|
|
381
|
+
|
|
382
|
+
def __init__(self, render_fn: Callable, props: Dict):
|
|
383
|
+
self.render_fn = render_fn
|
|
384
|
+
self.props = props
|
|
385
|
+
self.state = {}
|
|
386
|
+
self._vnode = None
|
|
387
|
+
self._dom_node = None
|
|
388
|
+
|
|
389
|
+
def render(self) -> Optional[VNode]:
|
|
390
|
+
return self.render_fn(self.props)
|
|
391
|
+
|
|
392
|
+
def component_did_mount(self) -> None:
|
|
393
|
+
pass
|
|
394
|
+
|
|
395
|
+
def component_will_unmount(self) -> None:
|
|
396
|
+
pass
|
|
397
|
+
|
|
398
|
+
def should_component_update(self, next_props, next_state) -> bool:
|
|
399
|
+
return self.props != next_props
|
pyreact/core/refs.py
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Refs Module
|
|
3
|
+
===========
|
|
4
|
+
|
|
5
|
+
This module implements the Ref system for accessing DOM nodes
|
|
6
|
+
and component instances directly.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Any, Callable, Dict, Optional, TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from .element import VNode
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Ref:
|
|
16
|
+
"""
|
|
17
|
+
Reference to a DOM node or component instance
|
|
18
|
+
|
|
19
|
+
Refs provide a way to access DOM nodes or component instances
|
|
20
|
+
directly without using props.
|
|
21
|
+
|
|
22
|
+
Example:
|
|
23
|
+
class TextInput(Component):
|
|
24
|
+
def __init__(self, props):
|
|
25
|
+
super().__init__(props)
|
|
26
|
+
self.input_ref = create_ref()
|
|
27
|
+
|
|
28
|
+
def focus(self):
|
|
29
|
+
self.input_ref.current.focus()
|
|
30
|
+
|
|
31
|
+
def render(self):
|
|
32
|
+
return h('input', {'ref': self.input_ref, 'type': 'text'})
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self):
|
|
36
|
+
self.current: Any = None
|
|
37
|
+
|
|
38
|
+
def __repr__(self) -> str:
|
|
39
|
+
return f"Ref(current={self.current!r})"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def create_ref() -> Ref:
|
|
43
|
+
"""
|
|
44
|
+
Create a new Ref object
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Ref: A new reference object
|
|
48
|
+
|
|
49
|
+
Example:
|
|
50
|
+
input_ref = create_ref()
|
|
51
|
+
h('input', {'ref': input_ref})
|
|
52
|
+
"""
|
|
53
|
+
return Ref()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def forward_ref(render: Callable) -> Callable:
|
|
57
|
+
"""
|
|
58
|
+
Forward a ref through a component to a DOM element
|
|
59
|
+
|
|
60
|
+
Allows a component to forward a ref to one of its children.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
render: Function (props, ref) -> VNode
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Component that forwards the ref
|
|
67
|
+
|
|
68
|
+
Example:
|
|
69
|
+
@forward_ref
|
|
70
|
+
def FancyInput(props, ref):
|
|
71
|
+
return h('input', {
|
|
72
|
+
'ref': ref,
|
|
73
|
+
'className': 'fancy-input',
|
|
74
|
+
**props
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
# Usage
|
|
78
|
+
input_ref = create_ref()
|
|
79
|
+
h(FancyInput, {'ref': input_ref, 'placeholder': 'Type here'})
|
|
80
|
+
"""
|
|
81
|
+
def wrapper(props: Dict[str, Any]) -> 'VNode':
|
|
82
|
+
ref = props.pop('ref', None)
|
|
83
|
+
return render(props, ref)
|
|
84
|
+
|
|
85
|
+
wrapper._forward_ref = True
|
|
86
|
+
wrapper.__name__ = render.__name__
|
|
87
|
+
wrapper.__doc__ = render.__doc__
|
|
88
|
+
|
|
89
|
+
return wrapper
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def use_imperative_handle(
|
|
93
|
+
ref: Optional[Ref],
|
|
94
|
+
create_handle: Callable[[], Any],
|
|
95
|
+
dependencies: Optional[list] = None
|
|
96
|
+
) -> None:
|
|
97
|
+
"""
|
|
98
|
+
Customize the instance value exposed to parent components
|
|
99
|
+
|
|
100
|
+
Use with forward_ref to expose specific methods to the parent.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
ref: Ref passed from parent
|
|
104
|
+
create_handle: Function that returns object to expose
|
|
105
|
+
dependencies: List of values that trigger handle update
|
|
106
|
+
|
|
107
|
+
Example:
|
|
108
|
+
@forward_ref
|
|
109
|
+
def FancyInput(props, ref):
|
|
110
|
+
input_ref = use_ref(None)
|
|
111
|
+
|
|
112
|
+
use_imperative_handle(ref, lambda: {
|
|
113
|
+
'focus': lambda: input_ref.current.focus(),
|
|
114
|
+
'scrollIntoView': lambda: input_ref.current.scrollIntoView()
|
|
115
|
+
}, [])
|
|
116
|
+
|
|
117
|
+
return h('input', {'ref': input_ref, **props})
|
|
118
|
+
"""
|
|
119
|
+
from .hooks import use_effect
|
|
120
|
+
|
|
121
|
+
def effect():
|
|
122
|
+
if ref:
|
|
123
|
+
ref.current = create_handle()
|
|
124
|
+
return lambda: None
|
|
125
|
+
|
|
126
|
+
use_effect(effect, dependencies)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class CallbackRef:
|
|
130
|
+
"""
|
|
131
|
+
Callback-based ref
|
|
132
|
+
|
|
133
|
+
Called with the DOM node when it's mounted or unmounted.
|
|
134
|
+
|
|
135
|
+
Example:
|
|
136
|
+
def set_input_ref(node):
|
|
137
|
+
if node:
|
|
138
|
+
node.focus()
|
|
139
|
+
|
|
140
|
+
h('input', {'ref': set_input_ref})
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
def __init__(self, callback: Callable[[Optional[Any]], None]):
|
|
144
|
+
self.callback = callback
|
|
145
|
+
self.current: Optional[Any] = None
|
|
146
|
+
|
|
147
|
+
def __call__(self, node: Optional[Any]) -> None:
|
|
148
|
+
"""Called when ref is set"""
|
|
149
|
+
self.current = node
|
|
150
|
+
self.callback(node)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def create_callback_ref(callback: Callable[[Optional[Any]], None]) -> CallbackRef:
|
|
154
|
+
"""
|
|
155
|
+
Create a callback ref
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
callback: Function called with the DOM node
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
CallbackRef: A callback-based reference
|
|
162
|
+
|
|
163
|
+
Example:
|
|
164
|
+
def on_input_mount(node):
|
|
165
|
+
if node:
|
|
166
|
+
node.focus()
|
|
167
|
+
|
|
168
|
+
ref = create_callback_ref(on_input_mount)
|
|
169
|
+
h('input', {'ref': ref})
|
|
170
|
+
"""
|
|
171
|
+
return CallbackRef(callback)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def is_ref(value: Any) -> bool:
|
|
175
|
+
"""
|
|
176
|
+
Check if a value is a valid ref
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
value: Value to check
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
bool: True if value is a ref
|
|
183
|
+
"""
|
|
184
|
+
return isinstance(value, (Ref, CallbackRef)) or callable(value)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def attach_ref(ref: Any, value: Any) -> None:
|
|
188
|
+
"""
|
|
189
|
+
Attach a value to a ref
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
ref: Ref object or callback
|
|
193
|
+
value: Value to attach
|
|
194
|
+
"""
|
|
195
|
+
if ref is None:
|
|
196
|
+
return
|
|
197
|
+
|
|
198
|
+
if isinstance(ref, Ref):
|
|
199
|
+
ref.current = value
|
|
200
|
+
elif callable(ref):
|
|
201
|
+
ref(value)
|
|
202
|
+
elif isinstance(ref, CallbackRef):
|
|
203
|
+
ref(value)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def detach_ref(ref: Any) -> None:
|
|
207
|
+
"""
|
|
208
|
+
Detach a value from a ref
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
ref: Ref object or callback
|
|
212
|
+
"""
|
|
213
|
+
attach_ref(ref, None)
|