Skip to content

Commit 6c70a9e

Browse files
committed
add schedule_render() method to AbstractComponent
1 parent 3b58120 commit 6c70a9e

File tree

4 files changed

+78
-6
lines changed

4 files changed

+78
-6
lines changed

idom/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
from .utils import Ref, html_to_vdom
1313

14-
from .core.component import component, Component
14+
from .core.component import component, Component, AbstractComponent
1515
from .core.events import event, Events
1616
from .core.layout import Layout
1717
from .core.vdom import vdom, VdomDict
@@ -64,4 +64,5 @@
6464
"widgets",
6565
"client",
6666
"install",
67+
"AbstractComponent",
6768
]

idom/core/component.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
import inspect
33
from functools import wraps
44
from typing import TYPE_CHECKING, Dict, Callable, Any, Tuple, Union
5+
from weakref import ReferenceType
56

7+
from .hooks import LifeCycleHook
68

79
if TYPE_CHECKING: # pragma: no cover
810
from .vdom import VdomDict # noqa
@@ -28,16 +30,41 @@ def constructor(*args: Any, **kwargs: Any) -> Component:
2830

2931

3032
class AbstractComponent(abc.ABC):
33+
"""A base class for all component implementations"""
3134

32-
__slots__ = [] if hasattr(abc.ABC, "__weakref__") else ["__weakref__"]
35+
__slots__ = ["_life_cycle_hook"]
36+
if not hasattr(abc.ABC, "__weakref__"):
37+
__slots__.append("__weakref__")
38+
39+
# When a LifeCyleHook is created it will bind a WeakReference of itself to the its
40+
# component. This is only useful for class-based component implementations. For
41+
# functional components, the LifeCycleHook is accessed by getting the current_hook().
42+
_life_cycle_hook: "ReferenceType[LifeCycleHook]"
3343

3444
@abc.abstractmethod
3545
def render(self) -> "VdomDict":
3646
"""Render the component's :ref:`VDOM <VDOM Mimetype>` model."""
3747

48+
def schedule_render(self):
49+
"""Schedule a re-render of this component
50+
51+
This is only used by class-based component implementations. Most components
52+
should be functional components that use hooks to schedule renders and save
53+
state.
54+
"""
55+
try:
56+
hook = self._life_cycle_hook()
57+
except AttributeError:
58+
raise RuntimeError(
59+
f"Component {self} has no hook. Are you rendering in a layout?"
60+
)
61+
else:
62+
assert hook is not None, f"LifeCycleHook for {self} no longer exists"
63+
hook.schedule_render()
64+
3865

3966
class Component(AbstractComponent):
40-
"""An object for rending component models."""
67+
"""A functional component"""
4168

4269
__slots__ = (
4370
"_function",

idom/core/hooks.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,20 @@
1313
Union,
1414
NamedTuple,
1515
List,
16+
TYPE_CHECKING,
1617
overload,
1718
cast,
1819
)
20+
from weakref import ref
21+
1922
from typing_extensions import Protocol
2023

2124
from loguru import logger
2225

2326
from idom.utils import Ref
2427

25-
from .component import AbstractComponent
28+
if TYPE_CHECKING: # pragma: no cover
29+
from .component import AbstractComponent
2630

2731

2832
__all__ = [
@@ -382,10 +386,11 @@ class LifeCycleHook:
382386

383387
def __init__(
384388
self,
385-
component: AbstractComponent,
386-
schedule_render: Callable[[AbstractComponent], None],
389+
component: "AbstractComponent",
390+
schedule_render: Callable[["AbstractComponent"], None],
387391
) -> None:
388392
self.component = component
393+
component._life_cycle_hook = ref(self)
389394
self._schedule_render_callback = schedule_render
390395
self._schedule_render_later = False
391396
self._is_rendering = False

tests/test_core/test_element.py renamed to tests/test_core/test_component.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import pytest
2+
13
import idom
24

35

@@ -68,3 +70,40 @@ def PreFormated():
6870
pre.get_attribute("innerHTML")
6971
== "<span>this<span>is</span>some</span>pre-formated text"
7072
)
73+
74+
75+
def test_class_component(driver, display, driver_wait):
76+
class Counter(idom.AbstractComponent):
77+
def __init__(self):
78+
self.count = 0
79+
80+
def render(self):
81+
return idom.html.button(
82+
{"onClick": lambda event: self._increment_count(), "id": "counter"},
83+
f"Clicked {self.count} times",
84+
)
85+
86+
def _increment_count(self):
87+
self.count += 1
88+
self.schedule_render()
89+
90+
display(Counter)
91+
92+
client_counter = driver.find_element_by_id("counter")
93+
94+
for i in range(5):
95+
driver_wait.until(
96+
lambda d: client_counter.get_attribute("innerHTML") == f"Clicked {i} times"
97+
)
98+
client_counter.click()
99+
100+
101+
def test_class_component_has_no_hook():
102+
class MyComponent(idom.AbstractComponent):
103+
def render(self):
104+
...
105+
106+
component = MyComponent()
107+
108+
with pytest.raises(RuntimeError, match="no hook"):
109+
component.schedule_render()

0 commit comments

Comments
 (0)