diff --git a/panel/models/reactive_html.py b/panel/models/reactive_html.py index 9406635f93..e48678838d 100644 --- a/panel/models/reactive_html.py +++ b/panel/models/reactive_html.py @@ -231,6 +231,8 @@ class ReactiveHTML(HTMLBox): events = bp.Dict(bp.String, bp.Dict(bp.String, bp.Bool)) + event_params = bp.List(bp.String) + html = bp.String() looped = bp.List(bp.String) diff --git a/panel/models/reactive_html.ts b/panel/models/reactive_html.ts index c328f120ba..61b690f820 100644 --- a/panel/models/reactive_html.ts +++ b/panel/models/reactive_html.ts @@ -169,9 +169,13 @@ export class ReactiveHTMLView extends HTMLBoxView { const property = data_model.properties[attr] if (property == null) continue + const is_event_param = this.model.event_params.includes(prop) this.connect(property.change, () => { - if (!this._changing) + if (!this._changing && !(is_event_param && !data_model[prop])) { this.run_script(prop) + if (is_event_param) + data_model.setv({[prop]: false}) + } }) } } @@ -503,6 +507,7 @@ export namespace ReactiveHTML { callbacks: p.Property children: p.Property data: p.Property + event_params: p.Property events: p.Property html: p.Property looped: p.Property @@ -529,6 +534,7 @@ export class ReactiveHTML extends HTMLBox { callbacks: [ Any, {} ], children: [ Any, {} ], data: [ Any, ], + event_params: [ Array(String), [] ], events: [ Any, {} ], html: [ String, "" ], looped: [ Array(String), [] ], diff --git a/panel/reactive.py b/panel/reactive.py index d71fe24eb5..173aaad8f9 100644 --- a/panel/reactive.py +++ b/panel/reactive.py @@ -1669,23 +1669,27 @@ def _init_params(self) -> Dict[str, Any]: p : getattr(self, p) for p in list(Layoutable.param) if getattr(self, p) is not None and p != 'name' } - data_params = {} + data_params, event_params = {}, [] for k, v in self.param.values().items(): + pobj = self.param[k] if ( (k in ignored and k != 'name') or - ((self.param[k].precedence or 0) < 0) or - (isinstance(v, Viewable) and not isinstance(self.param[k], param.ClassSelector)) + ((pobj.precedence or 0) < 0) or + (isinstance(v, Viewable) and not isinstance(pobj, param.ClassSelector)) ): continue if isinstance(v, str): v = HTML_SANITIZER.clean(v) data_params[k] = v + if isinstance(pobj, param.Event): + event_params.append(k) html, nodes, self._attrs = self._get_template() params.update({ 'attrs': self._attrs, 'callbacks': self._node_callbacks, 'data': self._data_model(**self._process_param_change(data_params)), 'events': self._get_events(), + 'event_params': event_params, 'html': escape(textwrap.dedent(html)), 'nodes': nodes, 'looped': [node for node, _ in self._parser.looped], @@ -1994,6 +1998,9 @@ def _update_model( model_msg['children'] = children self._set_on_model(model_msg, root, model) self._set_on_model(data_msg, root, model.data) + reset = {p: False for p in data_msg if p in model.event_params} + if reset: + self._set_on_model(reset, root, model.data) def on_event(self, node: str, event: str, callback: Callable) -> None: """ diff --git a/panel/tests/ui/test_reactive.py b/panel/tests/ui/test_reactive.py index 6f1e6407c8..d64ad6906c 100644 --- a/panel/tests/ui/test_reactive.py +++ b/panel/tests/ui/test_reactive.py @@ -15,13 +15,16 @@ class ReactiveComponent(ReactiveHTML): count = param.Integer(default=0) + event = param.Event() + _template = """
""" _scripts = { 'render': 'data.count += 1; reactive.innerText = `${data.count}`;', - 'click': 'data.count += 1; reactive.innerText = `${data.count}`;' + 'click': 'data.count += 1; reactive.innerText = `${data.count}`;', + 'event': 'data.count += 1; reactive.innerText = `${data.count}`;', } class ReactiveLiteral(ReactiveHTML): @@ -46,6 +49,28 @@ def test_reactive_html_click_js_event(page): wait_until(lambda: component.count == 2, page) +def test_reactive_html_param_event(page): + component = ReactiveComponent() + + serve_component(page, component) + + expect(page.locator(".reactive")).to_have_text('1') + + component.param.trigger('event') + + expect(page.locator(".reactive")).to_have_text('2') + + component.param.trigger('event') + + expect(page.locator(".reactive")).to_have_text('3') + + component.param.trigger('event') + component.param.trigger('event') + + expect(page.locator(".reactive")).to_have_text('5') + + wait_until(lambda: component.count == 5, page) + def test_reactive_html_set_loading_no_rerender(page): component = ReactiveComponent()