Skip to content

Commit 998171a

Browse files
committed
add clientside callback hook
1 parent d3d88c1 commit 998171a

File tree

4 files changed

+63
-6
lines changed

4 files changed

+63
-6
lines changed

dash/_callback.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import collections
22
import hashlib
33
from functools import wraps
4-
from typing import Callable, Optional, Any
4+
from typing import Callable, Optional, Any, Union
55

66
import flask
77

88
from .dependencies import (
99
handle_callback_args,
1010
handle_grouped_callback_args,
1111
Output,
12+
ClientsideFunction,
1213
)
1314
from .development.base_component import ComponentRegistry
1415
from .exceptions import (
@@ -209,7 +210,10 @@ def validate_long_inputs(deps):
209210
)
210211

211212

212-
def clientside_callback(clientside_function, *args, **kwargs):
213+
ClientsideFuncType = Union[str, ClientsideFunction]
214+
215+
216+
def clientside_callback(clientside_function: ClientsideFuncType, *args, **kwargs):
213217
return register_clientside_callback(
214218
GLOBAL_CALLBACK_LIST,
215219
GLOBAL_CALLBACK_MAP,
@@ -603,7 +607,7 @@ def register_clientside_callback(
603607
callback_map,
604608
config_prevent_initial_callbacks,
605609
inline_scripts,
606-
clientside_function,
610+
clientside_function: ClientsideFuncType,
607611
*args,
608612
**kwargs,
609613
):

dash/_hooks.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import typing as _t
2+
23
from importlib import metadata as _importlib_metadata
34

5+
import typing_extensions as _tx
46
import flask as _f
57

68
from .exceptions import HookError
79
from .resources import ResourceType
10+
from ._callback import ClientsideFuncType
811

912
if _t.TYPE_CHECKING:
1013
from .dash import Dash
@@ -18,9 +21,12 @@
1821
Dash = None
1922

2023

24+
HookDataType = _tx.TypeVar("HookDataType")
25+
26+
2127
# pylint: disable=too-few-public-methods
22-
class _Hook:
23-
def __init__(self, func, priority, final=False, data=None):
28+
class _Hook(_tx.Generic[HookDataType]):
29+
def __init__(self, func, priority, final=False, data: HookDataType = None):
2430
self.func = func
2531
self.final = final
2632
self.data = data
@@ -42,6 +48,9 @@ def __init__(self) -> None:
4248
}
4349
self._js_dist = []
4450
self._css_dist = []
51+
self._clientside_callbacks: _t.List[
52+
_t.Tuple[ClientsideFuncType, _t.Any, _t.Any]
53+
] = []
4554
self._finals = {}
4655

4756
def add_hook(
@@ -143,6 +152,14 @@ def wrap(func):
143152

144153
return wrap
145154

155+
def clientside_callback(
156+
self, clientside_function: ClientsideFuncType, *args, **kwargs
157+
):
158+
"""
159+
Add a callback to all the apps with the hook installed.
160+
"""
161+
self._clientside_callbacks.append((clientside_function, args, kwargs))
162+
146163
def script(self, distribution: _t.List[ResourceType]):
147164
"""Add js scripts to the page."""
148165
self._js_dist.extend(distribution)

dash/dash.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -591,7 +591,7 @@ def __init__( # pylint: disable=too-many-statements
591591
self.setup_startup_routes()
592592

593593
def _setup_hooks(self):
594-
# pylint: disable=import-outside-toplevel
594+
# pylint: disable=import-outside-toplevel,protected-access
595595
from ._hooks import HooksManager
596596

597597
self._hooks = HooksManager
@@ -604,6 +604,21 @@ def _setup_hooks(self):
604604
callback_args, callback_kwargs = hook.data
605605
self.callback(*callback_args, **callback_kwargs)(hook.func)
606606

607+
for (
608+
clientside_function,
609+
args,
610+
kwargs,
611+
) in self._hooks.hooks._clientside_callbacks:
612+
_callback.register_clientside_callback(
613+
self._callback_list,
614+
self.callback_map,
615+
self.config.prevent_initial_callbacks,
616+
self._inline_scripts,
617+
clientside_function,
618+
*args,
619+
**kwargs,
620+
)
621+
607622
if self._hooks.get_hooks("error"):
608623
self._on_error = self._hooks.HookErrorHandler(self._on_error)
609624

tests/integration/test_hooks.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ def hook_cleanup():
1717
hooks._css_dist = []
1818
hooks._js_dist = []
1919
hooks._finals = {}
20+
hooks._clientside_callbacks = []
2021

2122

2223
def test_hook001_layout(hook_cleanup, dash_duo):
@@ -165,3 +166,23 @@ def test_hook008_hook_distributions(hook_cleanup, dash_duo):
165166

166167
assert dash_duo.find_element(f'script[src="{js_uri}"]')
167168
assert dash_duo.find_element(f'link[href="{css_uri}"]')
169+
170+
171+
def test_hook009_hook_clientside_callback(hook_cleanup, dash_duo):
172+
hooks.clientside_callback(
173+
"(n) => `Called ${n}`",
174+
Output("hook-output", "children"),
175+
Input("hook-start", "n_clicks"),
176+
prevent_initial_call=True,
177+
)
178+
179+
app = Dash()
180+
app.layout = [
181+
html.Button("start", id="hook-start"),
182+
html.Div(id="hook-output"),
183+
]
184+
185+
dash_duo.start_server(app)
186+
187+
dash_duo.wait_for_element("#hook-start").click()
188+
dash_duo.wait_for_text_to_equal("#hook-output", "Called 1")

0 commit comments

Comments
 (0)