From ece20df2425fd0aed57807577bfea17b4be8034a Mon Sep 17 00:00:00 2001 From: "Maarten A. Breddels" Date: Tue, 3 Oct 2023 16:02:17 +0200 Subject: [PATCH] feat: event support in component_vue for calling python callbacks. Argument like 'event_foo' will be available as the function foo in the vue template. --- solara/components/component_vue.py | 36 ++++++++++++++++--- .../website/pages/examples/general/mycard.vue | 2 +- .../pages/examples/general/vue_component.py | 18 +++++++--- tests/unit/component_frontend_test.py | 16 +++++++++ 4 files changed, 62 insertions(+), 10 deletions(-) diff --git a/solara/components/component_vue.py b/solara/components/component_vue.py index 48c04a4d0..b2975df94 100644 --- a/solara/components/component_vue.py +++ b/solara/components/component_vue.py @@ -12,11 +12,23 @@ P = typing_extensions.ParamSpec("P") -def _widget_from_signature(name, base_class: Type[widgets.Widget], func: Callable[..., None]) -> Type[widgets.Widget]: - traits = {} +def _widget_from_signature(classname, base_class: Type[widgets.Widget], func: Callable[..., None], event_prefix: str) -> Type[widgets.Widget]: + classprops = {} parameters = inspect.signature(func).parameters for name, param in parameters.items(): + if name.startswith("event_"): + event_name = name[6:] + + def event_handler(self, data, buffers=None, event_name=event_name): + callback = self._event_callbacks.get(event_name) + if callback: + if buffers: + callback(data, buffers) + else: + callback(data) + + classprops[f"vue_{event_name}"] = event_handler if name.startswith("on_") and name[3:] in parameters: # callback, will be handled by reacton continue @@ -24,8 +36,11 @@ def _widget_from_signature(name, base_class: Type[widgets.Widget], func: Callabl trait = traitlets.Any() else: trait = traitlets.Any(default_value=param.default) - traits[name] = trait.tag(sync=True, **widgets.widget_serialization) - widget_class = type(name, (base_class,), traits) + classprops[name] = trait.tag(sync=True, **widgets.widget_serialization) + # maps event_foo to a callable + classprops["_event_callbacks"] = traitlets.Dict(default_value={}) + + widget_class = type(classname, (base_class,), classprops) return widget_class @@ -38,7 +53,7 @@ class VueWidgetSolara(vue.VueTemplate): template_file = (inspect.getfile(func), vue_path) base_class = VuetifyWidgetSolara if vuetify else VueWidgetSolara - widget_class = _widget_from_signature("VueWidgetSolaraSub", base_class, func) + widget_class = _widget_from_signature("VueWidgetSolaraSub", base_class, func, "vue_") return widget_class @@ -57,6 +72,9 @@ def component_vue(vue_path: str, vuetify=True) -> Callable[[Callable[P, None]], are assumed by refer to the same vue property, with `on_foo` being the event handler when `foo` changes from the vue template. + Arguments or the form `event_foo` should be callbacks that can be called from the vue template. They are + available as the function `foo` in the vue template. + [See the vue v2 api](https://v2.vuejs.org/v2/api/) for more information on how to use Vue, like `watch`, `methods` and lifecycle hooks such as `mounted` and `destroyed`. @@ -73,6 +91,14 @@ def decorator(func: Callable[P, None]): VueWidgetSolaraSub = _widget_vue(vue_path, vuetify=vuetify)(func) def wrapper(*args, **kwargs): + event_callbacks = {} + kwargs = kwargs.copy() + # take out all events named like event_foo and put them in a separate dict + for name in list(kwargs): + if name.startswith("event_"): + event_callbacks[name[6:]] = kwargs.pop(name) + if event_callbacks: + kwargs["_event_callbacks"] = event_callbacks return VueWidgetSolaraSub.element(*args, **kwargs) # type: ignore return wrapper diff --git a/solara/website/pages/examples/general/mycard.vue b/solara/website/pages/examples/general/mycard.vue index d927e54cb..f68d47b05 100644 --- a/solara/website/pages/examples/general/mycard.vue +++ b/solara/website/pages/examples/general/mycard.vue @@ -18,7 +18,7 @@ - + Go to Report diff --git a/solara/website/pages/examples/general/vue_component.py b/solara/website/pages/examples/general/vue_component.py index e3e05cb4e..6aa5cf011 100644 --- a/solara/website/pages/examples/general/vue_component.py +++ b/solara/website/pages/examples/general/vue_component.py @@ -11,6 +11,8 @@ """ +from typing import Callable + import numpy as np import solara @@ -20,6 +22,7 @@ @solara.component_vue("mycard.vue") def MyCard( + event_goto_report: Callable[[dict], None], value=[1, 10, 30, 20, 3], caption="My Card", color="red", @@ -31,11 +34,18 @@ def MyCard( def Page(): gen = np.random.RandomState(seed=seed.value) sales_data = np.floor(np.cumsum(gen.random(7) - 0.5) * 100 + 100) + show_report = solara.use_reactive(False) + with solara.Column(style={"min-width": "600px"}): + if show_report.value: + with solara.Card("Report"): + solara.Markdown("Lorum ipsum dolor sit amet") + solara.Button("Go back", on_click=lambda: show_report.set(False)) + else: - def new_seed(): - seed.value = np.random.randint(0, 100) + def new_seed(): + seed.value = np.random.randint(0, 100) - solara.Button("Generate new data", on_click=new_seed) + solara.Button("Generate new data", on_click=new_seed) - MyCard(value=sales_data.tolist(), color="green", caption="Sales Last 7 Days") + MyCard(value=sales_data.tolist(), color="green", caption="Sales Last 7 Days", event_goto_report=lambda data: show_report.set(True)) diff --git a/tests/unit/component_frontend_test.py b/tests/unit/component_frontend_test.py index 395510530..4f39a0287 100644 --- a/tests/unit/component_frontend_test.py +++ b/tests/unit/component_frontend_test.py @@ -32,3 +32,19 @@ def ComponentVueTest(value: int, on_value=None): mock.assert_called_once_with(2) widget.value = 3 mock.assert_called_with(3) + + +def test_component_vue_event(): + mock = unittest.mock.Mock() + + @solara._component_vue("component_vue_test.vue") + def ComponentVueTest(event_foo=None): + pass + + box, rc = solara.render(ComponentVueTest(event_foo=mock), handle_error=False) + widget = box.children[0] + mock.assert_not_called() + widget._handle_event(None, {"event": "foo", "data": 42}, None) + mock.assert_called_once_with(42) + widget._handle_event(None, {"event": "foo", "data": 42}, [b"bar"]) + mock.assert_called_with(42, [b"bar"])