Skip to content

Commit

Permalink
feat: event support in component_vue for calling python callbacks.
Browse files Browse the repository at this point in the history
Argument like 'event_foo' will be available as the function foo
in the vue template.
  • Loading branch information
maartenbreddels committed Oct 3, 2023
1 parent 6ec38eb commit ece20df
Show file tree
Hide file tree
Showing 4 changed files with 62 additions and 10 deletions.
36 changes: 31 additions & 5 deletions solara/components/component_vue.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,35 @@
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
if param.default == inspect.Parameter.empty:
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


Expand All @@ -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

Expand All @@ -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`.
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion solara/website/pages/examples/general/mycard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
<v-divider></v-divider>

<v-card-actions class="justify-center">
<v-btn block text>
<v-btn block text @click="goto_report">
Go to Report
</v-btn>
</v-card-actions>
Expand Down
18 changes: 14 additions & 4 deletions solara/website/pages/examples/general/vue_component.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
"""

from typing import Callable

import numpy as np

import solara
Expand All @@ -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",
Expand All @@ -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))
16 changes: 16 additions & 0 deletions tests/unit/component_frontend_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"])

0 comments on commit ece20df

Please sign in to comment.