From 92acbe26d49c4e3adf67ba21fa526672f3f3996d Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Thu, 10 Mar 2022 14:16:39 -0600 Subject: [PATCH 1/4] Improve support for loops in ReactiveHTML --- panel/models/reactive_html.ts | 6 +++--- panel/reactive.py | 39 ++++++++++++++++++++++++----------- 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/panel/models/reactive_html.ts b/panel/models/reactive_html.ts index 247c3a05c3..67ff3fda39 100644 --- a/panel/models/reactive_html.ts +++ b/panel/models/reactive_html.ts @@ -389,7 +389,7 @@ export class ReactiveHTMLView extends PanelHTMLBoxView { for (const callback of this.model.callbacks[elname]) { const [cb, method] = callback; let definition: string - htm = htm.replace('${'+method, '$--{'+method) + htm = htm.replaceAll('${'+method, '$--{'+method) if (method.startsWith('script(')) { const meth = ( method @@ -397,8 +397,8 @@ export class ReactiveHTMLView extends PanelHTMLBoxView { .replace('("', "_").replace('")', "") .replace('-', '_') ) - const script_name = meth.replace("script_", "") - htm = htm.replace(method, meth) + const script_name = meth.replaceAll("script_", "") + htm = htm.replaceAll(method, meth) definition = ` const ${meth} = (event) => { view._state.event = event diff --git a/panel/reactive.py b/panel/reactive.py index 1e358b4ab2..737379dbc5 100644 --- a/panel/reactive.py +++ b/panel/reactive.py @@ -1108,15 +1108,15 @@ def __init__(mcs, name, bases, dict_): "ensure there is a matching tag." ) - mcs._attrs, mcs._node_callbacks = {}, {} + mcs._node_callbacks = {} mcs._inline_callbacks = [] for node, attrs in mcs._parser.attrs.items(): for (attr, parameters, template) in attrs: param_attrs = [] for p in parameters: if p in mcs.param or '.' in p: - param_attrs.append(p) - elif re.match(mcs._script_regex, p): + continue + if re.match(mcs._script_regex, p): name = re.findall(mcs._script_regex, p)[0] if name not in mcs._scripts: raise ValueError( @@ -1140,9 +1140,7 @@ def __init__(mcs, name, bases, dict_): f"parameter or method '{p}', similar parameters " f"and methods include {matches}." ) - if node not in mcs._attrs: - mcs._attrs[node] = [] - mcs._attrs[node].append((attr, param_attrs, template)) + ignored = list(Reactive.param) types = {} for child in mcs._parser.children.values(): @@ -1247,7 +1245,7 @@ class ReactiveHTML(Reactive, metaclass=ReactiveHTMLMetaclass): Additionally we can invoke pure JS scripts defined on the class, e.g.: - + This will invoke the following script if it is defined on the class: @@ -1326,6 +1324,7 @@ def __init__(self, **params): else: params[children_param] = panel(child_value) super().__init__(**params) + self._attrs = {} self._panes = {} self._event_callbacks = defaultdict(lambda: defaultdict(list)) @@ -1366,13 +1365,14 @@ def _init_params(self): if isinstance(v, str): v = bleach.clean(v) data_params[k] = v + html, nodes, attrs = self._get_template() params.update({ - 'attrs': self._attrs, + 'attrs': attrs, 'callbacks': self._node_callbacks, 'data': self._data_model(**self._process_param_change(data_params)), 'events': self._get_events(), - 'html': escape(textwrap.dedent(self._get_template())), - 'nodes': self._parser.nodes, + 'html': escape(textwrap.dedent(html)), + 'nodes': nodes, 'looped': [node for node, _ in self._parser.looped], 'scripts': {} }) @@ -1509,6 +1509,18 @@ def _get_template(self): .replace(f'id="{name}"', f'id="{name}-${{id}}"') ) + # Parse attrs + p_attrs = {} + for node, attrs in parser.attrs.items(): + for (attr, parameters, template) in attrs: + param_attrs = [] + for p in parameters: + if p in self.param or '.' in p: + param_attrs.append(p) + if node not in p_attrs: + p_attrs[node] = [] + p_attrs[node].append((attr, param_attrs, template)) + # Remove child node template syntax for parent, child_name in self._parser.children.items(): if (parent, child_name) in self._parser.looped: @@ -1516,7 +1528,7 @@ def _get_template(self): html = html.replace('${%s[%d]}' % (child_name, i), '') else: html = html.replace('${%s}' % child_name, '') - return html + return html, parser.nodes, p_attrs def _linked_properties(self): linked_properties = [p for pss in self._attrs.values() for _, ps, _ in pss for p in ps] @@ -1609,7 +1621,10 @@ def _update_model(self, events, msg, root, model, doc, comm): data_msg[prop] = v if new_children: if self._parser.looped: - model_msg['html'] = escape(self._get_template()) + html, nodes, attrs = self._get_template() + model_msg['attrs'] = attrs + model_msg['nodes'] = nodes + model_msg['html'] = escape(textwrap.dedent(html)) children = self._get_children(doc, root, model, comm) else: children = None From f44f970e617bd88c015ffb5777a9e84ea74fa965 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 14 Mar 2022 16:02:38 +0100 Subject: [PATCH 2/4] Fix flake --- panel/reactive.py | 1 - 1 file changed, 1 deletion(-) diff --git a/panel/reactive.py b/panel/reactive.py index 737379dbc5..8e92b55c37 100644 --- a/panel/reactive.py +++ b/panel/reactive.py @@ -1112,7 +1112,6 @@ def __init__(mcs, name, bases, dict_): mcs._inline_callbacks = [] for node, attrs in mcs._parser.attrs.items(): for (attr, parameters, template) in attrs: - param_attrs = [] for p in parameters: if p in mcs.param or '.' in p: continue From 84172f696cd8d7788ee75d6ec6492b735e6352cf Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Tue, 15 Mar 2022 19:06:34 +0100 Subject: [PATCH 3/4] Fix attrs --- panel/reactive.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/panel/reactive.py b/panel/reactive.py index 8e92b55c37..b2e3c841ad 100644 --- a/panel/reactive.py +++ b/panel/reactive.py @@ -1364,9 +1364,9 @@ def _init_params(self): if isinstance(v, str): v = bleach.clean(v) data_params[k] = v - html, nodes, attrs = self._get_template() + html, nodes, self._attrs = self._get_template() params.update({ - 'attrs': attrs, + 'attrs': self._attrs, 'callbacks': self._node_callbacks, 'data': self._data_model(**self._process_param_change(data_params)), 'events': self._get_events(), @@ -1620,8 +1620,8 @@ def _update_model(self, events, msg, root, model, doc, comm): data_msg[prop] = v if new_children: if self._parser.looped: - html, nodes, attrs = self._get_template() - model_msg['attrs'] = attrs + html, nodes, self._attrs = self._get_template() + model_msg['attrs'] = self._attrs model_msg['nodes'] = nodes model_msg['html'] = escape(textwrap.dedent(html)) children = self._get_children(doc, root, model, comm) From 33ae3a5728b2105a38aaadb33e35a628a01962ac Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Thu, 17 Mar 2022 11:32:32 +0100 Subject: [PATCH 4/4] Update tests --- panel/tests/test_reactive.py | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/panel/tests/test_reactive.py b/panel/tests/test_reactive.py index 0b538cae4c..f10c4ac313 100644 --- a/panel/tests/test_reactive.py +++ b/panel/tests/test_reactive.py @@ -169,12 +169,11 @@ class Test(ReactiveHTML): assert isinstance(float_prop.property, bp.Float) assert float_prop.class_default(data_model) == 3.14 - assert Test._attrs == {'div': [('width', ['int'], '{int}')]} assert Test._node_callbacks == {} - test = Test() root = test.get_root() + assert test._attrs == {'div': [('width', ['int'], '{int}')]} assert root.callbacks == {} assert root.events == {} @@ -226,11 +225,11 @@ class TestDOMEvents(ReactiveHTML): assert isinstance(float_prop.property, bp.Float) assert float_prop.class_default(data_model) == 3.14 - assert TestDOMEvents._attrs == {'div': [('width', ['int'], '{int}')]} assert TestDOMEvents._node_callbacks == {} test = TestDOMEvents() root = test.get_root() + assert test._attrs == {'div': [('width', ['int'], '{int}')]} assert root.callbacks == {} assert root.events == {'div': {'change': True}} @@ -255,17 +254,17 @@ def _div_change(self, event): assert isinstance(int_prop.property, bp.Int) assert int_prop.class_default(data_model) == 3 - assert TestInline._attrs == { - 'div': [ - ('onchange', [], '{_div_change}'), - ('width', ['int'], '{int}') - ] - } assert TestInline._node_callbacks == {'div': [('onchange', '_div_change')]} assert TestInline._inline_callbacks == [('div', 'onchange', '_div_change')] test = TestInline() root = test.get_root() + assert test._attrs == { + 'div': [ + ('onchange', [], '{_div_change}'), + ('width', ['int'], '{int}') + ] + } assert root.callbacks == {'div': [('onchange', '_div_change')]} assert root.events == {} @@ -281,7 +280,6 @@ class TestChildren(ReactiveHTML): _template = '
${children}
' - assert TestChildren._attrs == {} assert TestChildren._node_callbacks == {} assert TestChildren._inline_callbacks == [] assert TestChildren._parser.children == {'div': 'children'} @@ -289,6 +287,7 @@ class TestChildren(ReactiveHTML): widget = TextInput() test = TestChildren(children=[widget]) root = test.get_root() + assert test._attrs == {} assert root.children == {'div': [widget._models[root.ref['id']][0]]} assert len(widget._models) == 1 assert test._panes == {'children': [widget]} @@ -318,7 +317,6 @@ class TestTemplatedChildren(ReactiveHTML): """ - assert TestTemplatedChildren._attrs == {} assert TestTemplatedChildren._node_callbacks == {} assert TestTemplatedChildren._inline_callbacks == [] assert TestTemplatedChildren._parser.children == {'option': 'children'} @@ -326,6 +324,7 @@ class TestTemplatedChildren(ReactiveHTML): widget = TextInput() test = TestTemplatedChildren(children=[widget]) root = test.get_root() + assert test._attrs == {} assert root.looped == ['option'] assert root.children == {'option': [widget._models[root.ref['id']][0]]} assert test._panes == {'children': [widget]} @@ -351,7 +350,6 @@ class TestTemplatedChildren(ReactiveHTML): """ - assert TestTemplatedChildren._attrs == {} assert TestTemplatedChildren._node_callbacks == {} assert TestTemplatedChildren._inline_callbacks == [] assert TestTemplatedChildren._parser.children == {'option': 'children'} @@ -359,6 +357,7 @@ class TestTemplatedChildren(ReactiveHTML): widget = TextInput() test = TestTemplatedChildren(children={'test': widget}) root = test.get_root() + assert test._attrs == {} assert root.looped == ['option'] assert root.children == {'option': [widget._models[root.ref['id']][0]]} assert test._panes == {'children': [widget]} @@ -390,14 +389,13 @@ class TestTemplatedChildren(ReactiveHTML): """ - assert TestTemplatedChildren._attrs == {} assert TestTemplatedChildren._node_callbacks == {} assert TestTemplatedChildren._inline_callbacks == [] assert TestTemplatedChildren._parser.children == {'option': 'children'} test = TestTemplatedChildren(children=['A', 'B', 'C']) - assert test._get_template() == """ + assert test._get_template()[0] == """ """ - assert TestTemplatedChildren._attrs == {} assert TestTemplatedChildren._node_callbacks == {} assert TestTemplatedChildren._inline_callbacks == [] assert TestTemplatedChildren._parser.children == {'option': 'children'} test = TestTemplatedChildren(children=['A', 'B', 'C']) - assert test._get_template() == """ + assert test._get_template()[0] == """ """ model = test.get_root() + assert test._attrs == {} assert model.looped == ['option']