diff --git a/reacton/core.py b/reacton/core.py index 228f487..aa3599b 100644 --- a/reacton/core.py +++ b/reacton/core.py @@ -139,7 +139,7 @@ def close_widget(widget: widgets.Widget): logger.warning("Widget %r does not have a close method, possibly a close trait was added", widget) -def _event_handler_exception_wrapper(f): +def _event_handler_exception_wrapper_and_batch(f): """Wrap an event handler to catch exceptions and put them in a reacton context. This allows a component to catch the exception of a direct child""" @@ -149,7 +149,8 @@ def _event_handler_exception_wrapper(f): def wrapper(*args, **kwargs): try: - f(*args, **kwargs) + with rc: + f(*args, **kwargs) except Exception as e: assert context is not None # because widgets don't have a context, but are a child of a component @@ -161,6 +162,10 @@ def wrapper(*args, **kwargs): return wrapper +# for backwards compatibility +_event_handler_exception_wrapper = _event_handler_exception_wrapper_and_batch + + def join_key(parent_key, key): return f"{parent_key}{key}" diff --git a/reacton/core_test.py b/reacton/core_test.py index d19390b..455f7ac 100644 --- a/reacton/core_test.py +++ b/reacton/core_test.py @@ -3104,6 +3104,47 @@ def Test(): rc.close() +def test_batch_update_from_event(): + @reacton.component + def Test(): + state, set_state = reacton.use_state(0) + + def increment_twice(): + set_state(2) + set_state(3) + + w.Button(description=str(state), on_click=increment_twice) + + box, rc = react.render(Test(), handle_error=False) + assert rc.find(widgets.Button).widget.description == "0" + assert rc.render_count == 1 + rc.find(widgets.Button).widget.click() + assert rc.render_count == 2 + assert rc.find(widgets.Button).widget.description == "3" + rc.close() + + +def test_batch_update_from_event_vue(): + @reacton.component + def Test(): + state, set_state = reacton.use_state(0) + + def increment_twice(): + set_state(2) + set_state(3) + + btn = v.Btn(children=[str(state)], on_click=increment_twice) + v.use_event(btn, "click", lambda *_ignore: increment_twice()) + + box, rc = react.render(Test(), handle_error=False) + assert rc.find(ipyvuetify.Btn).widget.children[0] == "0" + assert rc.render_count == 1 + rc.find(ipyvuetify.Btn).widget.fire_event("click", {}) + assert rc.render_count == 2 + assert rc.find(ipyvuetify.Btn).widget.children[0] == "3" + rc.close() + + def test_event_multiple(): @reacton.component def Test(): diff --git a/reacton/ipyvue.py b/reacton/ipyvue.py index 580c21e..d800e98 100644 --- a/reacton/ipyvue.py +++ b/reacton/ipyvue.py @@ -3,7 +3,7 @@ import ipyvue import reacton as react -from reacton.core import get_render_context +from reacton.core import _event_handler_exception_wrapper_and_batch def use_event(el: react.core.Element, event_and_modifiers, callback: Callable[[Any], Any]): @@ -13,26 +13,14 @@ def use_event(el: react.core.Element, event_and_modifiers, callback: Callable[[A 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 - rc = get_render_context() - context = rc.context - assert context is not None - - def handler(*args): - try: - callback_ref.current(*args) - except Exception as e: - assert context is not None - # because widgets don't have a context, but are a child of a component - # we add it to exceptions_children, not exception_self - # this allows a component to catch the exception of a direct child - context.exceptions_children.append(e) - rc.force_update() - - vue_widget.on_event(event_and_modifiers, handler) + + def wrapper(*args): + callback_ref.current(*args) + + vue_widget.on_event(event_and_modifiers, _event_handler_exception_wrapper_and_batch(wrapper)) def cleanup(): - vue_widget.on_event(event_and_modifiers, handler, remove=True) + vue_widget.on_event(event_and_modifiers, wrapper, remove=True) return cleanup