Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion reacton/ipyvue.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,28 @@
import ipyvue

import reacton as react
from reacton.core import get_render_context
from reacton.core import ComponentWidget, get_render_context


def use_event(el: react.core.Element, event_and_modifiers, callback: Callable[[Any], Any]):
# to avoid add_event_handler having a stale reference to callback
callback_ref = react.use_ref(callback)
callback_ref.current = callback

# Put the event name in the widget constructor arguments: the synced _events
# trait then goes along with the comm open message. The later on_event call
# (in the effect below) only updates _events when the event set differs, so
# this saves one update message per widget per event. When the element is
# reused from a previous render (memoized) and the widget already exists,
# on_event falls back to syncing _events itself.
if isinstance(el.component, ComponentWidget) and issubclass(el.component.widget, ipyvue.VueWidget):
events = el.kwargs.get("_events")
if events is None:
el.kwargs["_events"] = [event_and_modifiers]
elif event_and_modifiers not in events:
# do not mutate the list, it could be shared with a previous element
el.kwargs["_events"] = [*events, event_and_modifiers]

def add_event_handler():
vue_widget = cast(ipyvue.VueWidget, react.core.get_widget(el))
# we are basically copying the logic from reacton.core._event_handler_exception_wrapper
Expand All @@ -32,6 +46,11 @@ def handler(*args):
vue_widget.on_event(event_and_modifiers, handler)

def cleanup():
if rc._closing:
# the whole tree is going away: removing the handler would sync
# the _events trait to the frontend (one message per widget)
# right before the comm is closed anyway
return
vue_widget.on_event(event_and_modifiers, handler, remove=True)

return cleanup
Expand Down
90 changes: 90 additions & 0 deletions reacton/ipyvue_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import unittest.mock

import ipyvuetify
import ipyvue

import reacton as react

from . import ipyvuetify as v
from .ipyvue import use_event


def test_use_event_no_sync_messages():
"""The _events trait should be set at construction and never re-assigned.

Re-assigning it (by on_event during the effect, or on_event(remove=True)
during close) sends one widget update message per widget per event.
"""
on_click = unittest.mock.Mock()

@react.component
def Test():
btn = v.Btn(children=["click me"])
use_event(btn, "click", on_click)
return btn

box, rc = react.render(Test(), handle_error=False)
btn = rc.find(ipyvuetify.Btn).widget

events_changes = unittest.mock.Mock()
btn.observe(events_changes, "_events")

# the event name went along with the constructor arguments
assert btn._events == ["click"]
# and the handler works
btn.fire_event("click", {})
on_click.assert_called_once()

rc.force_update()
events_changes.assert_not_called()

rc.close()
events_changes.assert_not_called()


def test_use_event_removed_on_rerender():
"""When the widget persists but the event changes, _events must sync."""
on_event = unittest.mock.Mock()
set_event_name = None

@react.component
def Test():
nonlocal set_event_name
event_name, set_event_name = react.use_state("click")
btn = v.Btn(children=["click me"])
use_event(btn, event_name, on_event)
return btn

box, rc = react.render(Test(), handle_error=False)
btn = rc.find(ipyvuetify.Btn).widget
assert btn._events == ["click"]
assert set_event_name is not None
set_event_name("dblclick")
assert btn._events == ["dblclick"]
btn.fire_event("dblclick", {})
on_event.assert_called_once()
rc.close()


def test_use_event_component_element():
"""use_event on a component element (not a widget element) keeps working."""
on_click = unittest.mock.Mock()

@react.component
def Button():
return v.Btn(children=["click me"])

@react.component
def Test():
btn = Button()
use_event(btn, "click", on_click)
return btn

box, rc = react.render(Test(), handle_error=False)
btn = rc.find(ipyvuetify.Btn).widget
assert isinstance(btn, ipyvue.VueWidget)
# falls back to syncing _events from the effect
assert btn._events == ["click"]
btn.fire_event("click", {})
on_click.assert_called_once()
rc.close()
Loading