diff --git a/panel/io/embed.py b/panel/io/embed.py index fad7e4d5d16..cc7e1df025b 100644 --- a/panel/io/embed.py +++ b/panel/io/embed.py @@ -73,7 +73,7 @@ def param_to_jslink(model, widget): Converts Param pane widget links into JS links if possible. """ from ..viewable import Reactive - from ..widgets import LiteralInput + from ..widgets import Widget, LiteralInput param_pane = widget._param_pane pobj = param_pane.object @@ -81,12 +81,17 @@ def param_to_jslink(model, widget): watchers = [w for w in get_watchers(widget) if w not in widget._callbacks and w not in param_pane._callbacks] + tgt_links = [Watcher(*l[:-3]) for l in pobj._links] + tgt_watchers = [w for w in get_watchers(pobj) if w not in pobj._callbacks + and w not in tgt_links and w not in param_pane._callbacks] + for widget in param_pane._widgets.values(): if isinstance(widget, LiteralInput): widget.serializer = 'json' if (not pname or not isinstance(pobj, Reactive) or watchers or - pname[0] not in pobj._linkable_params): + pname[0] not in pobj._linkable_params or + (not isinstance(pobj, Widget) and tgt_watchers)): return return link_to_jslink(model, widget, 'value', pobj, pname[0]) @@ -97,13 +102,10 @@ def link_to_jslink(model, source, src_spec, target, tgt_spec): declared forward and reverse JS transforms on the source and target. """ ref = model.ref['id'] - tgt_links = [Watcher(*l[:-3]) for l in target._links] - tgt_watchers = [w for w in get_watchers(target) if w not in target._callbacks - and w not in tgt_links] if ((source._source_transforms.get(src_spec, False) is None) or (target._target_transforms.get(tgt_spec, False) is None) or - ref not in source._models or ref not in target._models or tgt_watchers): + ref not in source._models or ref not in target._models): # We cannot jslink if either source or target declare # that they apply Python transforms return @@ -116,18 +118,24 @@ def link_to_jslink(model, source, src_spec, target, tgt_spec): def links_to_jslinks(model, widget): + from ..widgets import Widget + src_links = [Watcher(*l[:-3]) for l in widget._links] if any(w not in widget._callbacks and w not in src_links for w in get_watchers(widget)): return links = [] for link in widget._links: - if link.transformed: + target = link.target + tgt_watchers = [w for w in get_watchers(target) if w not in target._callbacks] + if link.transformed or (tgt_watchers and not isinstance(target, Widget)): return + mappings = [] for pname, tgt_spec in link.links.items(): if Watcher(*link[:-3]) in widget._param_watchers[pname]['value']: mappings.append((pname, tgt_spec)) + if mappings: links.append((link, mappings)) jslinks = [] @@ -199,15 +207,12 @@ def embed_state(panel, model, doc, max_states=1000, max_opts=3, # Replace parameter links with JS links link = param_to_jslink(model, widget) if link is not None: + pobj = widget._param_pane.object + if isinstance(pobj, Widget): + if not any(w not in pobj._callbacks and w not in widget._param_pane._callbacks + for w in get_watchers(pobj)): + ignore.append(pobj) continue # Skip if we were able to attach JS link - pobj = widget._param_pane.object - if isinstance(pobj, Widget): - watchers = [w for w in get_watchers(pobj) if widget not in pobj._callbacks - and widget not in widget._param_pane._callbacks] - if not watchers: - # If underlying parameterized object is a widget - # which has no other links ensure it is skipped later - ignore.append(pobj) if widget._links: jslinks = links_to_jslinks(model, widget) @@ -242,7 +247,7 @@ def embed_state(panel, model, doc, max_states=1000, max_opts=3, add_to_doc(model, doc, True) doc._held_events = [] - if not values: + if not widget_data: return restore = [w.value for w, _, _, _ in values] diff --git a/panel/tests/test_io.py b/panel/tests/test_io.py index b34bfec3690..b8023b8397d 100644 --- a/panel/tests/test_io.py +++ b/panel/tests/test_io.py @@ -13,11 +13,55 @@ from panel.config import config from panel.io.embed import embed_state from panel.pane import Str +from panel.param import Param from panel.widgets import Select, FloatSlider, Checkbox from .util import jb_available +def test_embed_param_jslink(document, comm): + select = Select(options=['A', 'B', 'C']) + params = Param(select, parameters=['disabled']).layout + panel = Row(select, params) + with config.set(embed=True): + model = panel.get_root(document, comm) + embed_state(panel, model, document) + assert len(document.roots) == 1 + + ref = model.ref['id'] + cbs = list(model.select({'type': CustomJS})) + assert len(cbs) == 2 + cb1, cb2 = cbs + cb1, cb2 = (cb1, cb2) if select._models[ref][0] is cb1.args['target'] else (cb2, cb1) + assert cb1.code == """ + value = source['active']; + value = value.indexOf(0) >= 0; + value = value; + try { + property = target.properties['disabled']; + if (property !== undefined) { property.validate(value); } + } catch(err) { + console.log('WARNING: Could not set disabled on target, raised error: ' + err); + return; + } + target['disabled'] = value; + """ + + assert cb2.code == """ + value = source['disabled']; + value = value; + value = value ? [0] : []; + try { + property = target.properties['active']; + if (property !== undefined) { property.validate(value); } + } catch(err) { + console.log('WARNING: Could not set active on target, raised error: ' + err); + return; + } + target['active'] = value; + """ + + def test_embed_select_str_link(document, comm): select = Select(options=['A', 'B', 'C']) string = Str() @@ -42,6 +86,36 @@ def link(target, event): assert event['new'] == '<pre>%s</pre>' % k +def test_embed_select_str_link_two_steps(document, comm): + select = Select(options=['A', 'B', 'C']) + string1 = Str() + select.link(string1, value='object') + string2 = Str() + string1.link(string2, object='object') + panel = Row(select, string1, string2) + with config.set(embed=True): + model = panel.get_root(document, comm) + embed_state(panel, model, document) + _, state = document.roots + assert set(state.state) == {'A', 'B', 'C'} + for k, v in state.state.items(): + content = json.loads(v['content']) + assert 'events' in content + events = content['events'] + assert len(events) == 2 + event = events[0] + assert event['kind'] == 'ModelChanged' + assert event['attr'] == 'text' + assert event['model'] == model.children[1].ref + assert event['new'] == '<pre>%s</pre>' % k + + event = events[1] + assert event['kind'] == 'ModelChanged' + assert event['attr'] == 'text' + assert event['model'] == model.children[2].ref + assert event['new'] == '<pre>%s</pre>' % k + + def test_embed_select_str_link_with_secondary_watch(document, comm): select = Select(options=['A', 'B', 'C']) string = Str() diff --git a/panel/tests/test_reactive.py b/panel/tests/test_reactive.py index 55126aeff61..048ee8d9726 100644 --- a/panel/tests/test_reactive.py +++ b/panel/tests/test_reactive.py @@ -3,7 +3,9 @@ import param from bokeh.models import Div -from panel.viewable import Reactive +from panel.layout import Tabs, WidgetBox +from panel.viewable import Layoutable, Reactive +from panel.widgets import Checkbox, StaticText, TextInput def test_link(): @@ -76,3 +78,54 @@ class ReactiveLink(Reactive): assert isinstance(cb, partial) assert cb.args == (document, div.ref['id']) assert cb.func == obj._server_change + + +def test_text_input_controls(): + text_input = TextInput() + + controls = text_input.controls() + + assert isinstance(controls, Tabs) + assert len(controls) == 2 + wb1, wb2 = controls + assert isinstance(wb1, WidgetBox) + assert len(wb1) == 4 + name, disabled, value, placeholder = wb1 + + assert isinstance(name, StaticText) + assert isinstance(disabled, Checkbox) + assert isinstance(value, TextInput) + assert isinstance(placeholder, TextInput) + + text_input.disabled = True + assert disabled.value + + text_input.placeholder = "Test placeholder..." + assert placeholder.value == "Test placeholder..." + + text_input.value = "New value" + assert value.value == "New value" + + assert isinstance(wb2, WidgetBox) + assert len(wb2) == len(list(Layoutable.param)) + 1 + + + +def test_text_input_controls_explicit(): + text_input = TextInput() + + controls = text_input.controls(['placeholder', 'disabled']) + + assert isinstance(controls, WidgetBox) + assert len(controls) == 3 + name, disabled, placeholder = controls + + assert isinstance(name, StaticText) + assert isinstance(disabled, Checkbox) + assert isinstance(placeholder, TextInput) + + text_input.disabled = True + assert disabled.value + + text_input.placeholder = "Test placeholder..." + assert placeholder.value == "Test placeholder..." diff --git a/panel/viewable.py b/panel/viewable.py index 20403891df7..f72d335d826 100644 --- a/panel/viewable.py +++ b/panel/viewable.py @@ -886,18 +886,17 @@ def controls(self, parameters=[], jslink=True): from .widgets import LiteralInput if parameters: - return Param(self.param, parameters=parameters, default_layout=WidgetBox, - name='Controls').layout - - if jslink: + linkable = parameters + elif jslink: linkable = self._linkable_params else: linkable = list(self.param) + params = [p for p in linkable if p not in Layoutable.param] controls = Param(self.param, parameters=params, default_layout=WidgetBox, name='Controls') layout_params = [p for p in linkable if p in Layoutable.param] - if 'name' not in layout_params and self._rename.get('name', False) is not None: + if 'name' not in layout_params and self._rename.get('name', False) is not None and not parameters: layout_params.insert(0, 'name') style = Param(self.param, parameters=layout_params, default_layout=WidgetBox, name='Layout') @@ -912,8 +911,11 @@ def controls(self, parameters=[], jslink=True): widget.jslink(self, value=p, bidirectional=True) if isinstance(widget, LiteralInput): widget.serializer = 'json' - if params: + + if params and layout_params: return Tabs(controls.layout[0], style.layout[0]) + elif params: + return controls.layout[0] return style.layout[0] def link(self, target, callbacks=None, **links):