Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve support for jinja2 loops in ReactiveHTML #3236

Merged
merged 4 commits into from
Mar 17, 2022
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
6 changes: 3 additions & 3 deletions panel/models/reactive_html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -389,16 +389,16 @@ 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
.replace("('", "_").replace("')", "")
.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
Expand Down
38 changes: 26 additions & 12 deletions panel/reactive.py
Original file line number Diff line number Diff line change
Expand Up @@ -1108,15 +1108,14 @@ def __init__(mcs, name, bases, dict_):
"ensure there is a matching </div> 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(
Expand All @@ -1140,9 +1139,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():
Expand Down Expand Up @@ -1247,7 +1244,7 @@ class ReactiveHTML(Reactive, metaclass=ReactiveHTMLMetaclass):

Additionally we can invoke pure JS scripts defined on the class, e.g.:

<input id="input" onchange="${run_script('some_script')}"></input>
<input id="input" onchange="${script('some_script')}"></input>

This will invoke the following script if it is defined on the class:

Expand Down Expand Up @@ -1326,6 +1323,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))

Expand Down Expand Up @@ -1366,13 +1364,14 @@ def _init_params(self):
if isinstance(v, str):
v = bleach.clean(v)
data_params[k] = v
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(),
'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': {}
})
Expand Down Expand Up @@ -1509,14 +1508,26 @@ 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:
for i, _ in enumerate(getattr(self, child_name)):
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]
Expand Down Expand Up @@ -1609,7 +1620,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, 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)
else:
children = None
Expand Down
31 changes: 15 additions & 16 deletions panel/tests/test_reactive.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 == {}

Expand Down Expand Up @@ -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}}

Expand All @@ -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 == {}

Expand All @@ -281,14 +280,14 @@ class TestChildren(ReactiveHTML):

_template = '<div id="div">${children}</div>'

assert TestChildren._attrs == {}
assert TestChildren._node_callbacks == {}
assert TestChildren._inline_callbacks == []
assert TestChildren._parser.children == {'div': 'children'}

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]}
Expand Down Expand Up @@ -318,14 +317,14 @@ class TestTemplatedChildren(ReactiveHTML):
</div>
"""

assert TestTemplatedChildren._attrs == {}
assert TestTemplatedChildren._node_callbacks == {}
assert TestTemplatedChildren._inline_callbacks == []
assert TestTemplatedChildren._parser.children == {'option': 'children'}

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]}
Expand All @@ -351,14 +350,14 @@ class TestTemplatedChildren(ReactiveHTML):
</div>
"""

assert TestTemplatedChildren._attrs == {}
assert TestTemplatedChildren._node_callbacks == {}
assert TestTemplatedChildren._inline_callbacks == []
assert TestTemplatedChildren._parser.children == {'option': 'children'}

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]}
Expand Down Expand Up @@ -390,14 +389,13 @@ class TestTemplatedChildren(ReactiveHTML):
</select>
"""

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] == """
<select id="select-${id}">

<option id="option-0-${id}"></option>
Expand All @@ -410,6 +408,7 @@ class TestTemplatedChildren(ReactiveHTML):
"""

model = test.get_root()
assert test._attrs == {}
assert model.looped == ['option']


Expand All @@ -427,14 +426,13 @@ class TestTemplatedChildren(ReactiveHTML):
</select>
"""

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] == """
<select id="select-${id}">

<option id="option-0-${id}"></option>
Expand All @@ -446,6 +444,7 @@ class TestTemplatedChildren(ReactiveHTML):
</select>
"""
model = test.get_root()
assert test._attrs == {}
assert model.looped == ['option']


Expand Down