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

Add ReactiveHTML #1894

Merged
merged 38 commits into from
Mar 16, 2021
Merged

Add ReactiveHTML #1894

merged 38 commits into from
Mar 16, 2021

Conversation

philippjfr
Copy link
Member

@philippjfr philippjfr commented Jan 9, 2021

Allows wrapping arbitrary HTML and subscribing to attributes on specified DOM nodes. The way it works is that you provide an HTML template and declare a number of parameters, which are linked via template variables. Any DOM nodes you want to sync with Python must have an id of the form id='name and any additional variables may also be templated in based on declared parameters. The ReactiveHTML bokeh model will install a MutationObserver on all objects which is linked to a parameter. Additionally you may also declare DOM events to subscribe to, e.g. for a button you subscribe to a 'click' event.

ReactiveHTML

_html

The _html template supports a number of features via template variables (${variable}):

  • To allow attributes and callbacks on a specific DOM node to be added it must declare a unique id which can be used to refer to the specific node.
  • Template variables of the form ${parameter_name} can be used in the template to set and link attributes/properties (this works bi-directionally)
  • Template variables which match the name of a method set on a inline callback such as onclick or onchange will call that Python method
  • Template variables used as a child of HTML tag, e.g. <div>${children}</div> will render those children, however these must be list parameters and all contents will be treated as other panel ccomponents.

The Preact (re-)rendering will only be triggered if one or more of the template variables has changed.

_scripts

In addition to templating HTML an arbitrary set of scripts may be declared on the _scripts attribute. These too will only execute if any of the template variables changes. _scripts takes the form of {parameter: ['JavaScript code']} and are executed when the declared parameter/attribute changes. The scripts are given the data model as an argument in the namespace allowing it to access the current parameter/attribute state. Multiple scripts can be added to support distinct actions.

_dom_events

Named nodes can be subscribed to by declaring _dom_events, which takes the form of {node_name: [event, ...]}. Available events depend on the type of HTML node but include things like click and change. These events may then be watched by adding a method to the class _{node_name}_{event}, which will be called whenever the event is triggered. Additionally these events may be watched using the on_event method. Note that after a DOM event fires on a particular node all the parameters linked to that node are also updated, this allows non-reflected attributes to be synced, which do not fire a MutationObserver.

Here is an example using Microsoft's Fast Design system:

import param
import panel as pn
from panel.reactive import ReactiveHTML

# Set up custom model

class FastForm(ReactiveHTML):

    slider_min = param.Number(0)
    
    slider_max = param.Number(1)

    slider_step = param.Number(0.01)

    slider_value = param.Number()

    text_value = param.String('')

    bool_value = param.Boolean(True)

    button_title = param.String('Click me!')

    _html = """
    <fast-design-system-provider use-defaults>
      <fast-text-field id="text" placeholder="name" value="${text_value}"></fast-text-field>
      <br/>
      <fast-checkbox id="checkbox" checked="${bool_value}">Checkbox</fast-checkbox>
      <br/>
      <fast-slider id="slider" style="width:200px" min="${slider_min}" max="${slider_max}" step="${slider_step}" value="${slider_value}"></fast-slider>
      <br/>
      <fast-button id="button">${button_title}</fast-button>
    </fast-design-system-provider>
    """

    _dom_events = {'button': ['click'], 'text': ['change']}

form = FastForm()

# Setup template and display events and changes

template = """
{% extends base %}

<!-- goes in body -->
{% block preamble %}
<script type="module" src="https://unpkg.com/@microsoft/fast-components"></script>
{% endblock %}
"""

tmpl = pn.Template(template)

event_json = pn.pane.JSON(width=500)

def event(event):
    event_json.object = event.event

changes = pn.pane.Str('', width=500)

def change(event):
    changes.object += f'\n {event.name}: {event.new}'

for obj, events in form._event_map.items():
    for e in events:
        form.on_event(obj, e, event)

form.param.watch(change, ['bool_value', 'text_value', 'slider_value'])

tmpl.add_panel('custom', pn.Row(pn.Param(form.param, parameters=['button_title', 'text_value', 'slider_value']), form, event_json, changes))

tmpl.servable()

events

@codecov
Copy link

codecov bot commented Jan 9, 2021

Codecov Report

Merging #1894 (58b11ff) into master (238199f) will increase coverage by 0.00%.
The diff coverage is 87.64%.

Impacted file tree graph

@@           Coverage Diff            @@
##           master    #1894    +/-   ##
========================================
  Coverage   84.42%   84.43%            
========================================
  Files         179      180     +1     
  Lines       20776    21109   +333     
========================================
+ Hits        17541    17824   +283     
- Misses       3235     3285    +50     
Impacted Files Coverage Δ
panel/pane/markup.py 97.15% <ø> (ø)
setup.py 0.00% <ø> (ø)
panel/links.py 87.08% <60.00%> (-0.50%) ⬇️
panel/models/reactive_html.py 82.55% <82.55%> (ø)
panel/reactive.py 77.12% <85.00%> (+2.61%) ⬆️
panel/tests/test_reactive.py 98.85% <98.82%> (-0.05%) ⬇️
panel/models/__init__.py 100.00% <100.00%> (ø)
panel/models/markup.py 100.00% <100.00%> (ø)
panel/tests/pane/test_base.py 88.23% <0.00%> (-11.77%) ⬇️
panel/io/reload.py 64.36% <0.00%> (-2.30%) ⬇️
... and 2 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 238199f...58b11ff. Read the comment docs.

panel/models/custom_html.ts Outdated Show resolved Hide resolved
panel/models/custom_html.ts Outdated Show resolved Hide resolved
@philippjfr
Copy link
Member Author

@mattpap I'd actually love to hear how crazy you think what I'm doing here is.

@mattpap
Copy link
Collaborator

mattpap commented Jan 10, 2021

I'd actually love to hear how crazy you think what I'm doing here is.

I've been considering implementing something similar in bokeh together with the new CSS-based layout. As to the implementation, I need to look closer at this first. One thing that I would like to support in my approach, is allowing observing changes to CSS styles (actual stylesheets not style attribute), but I'm not sure if this can be done efficiently.

@philippjfr philippjfr changed the title Add CustomHTML model Add ReactiveHTML model Jan 10, 2021
@philippjfr
Copy link
Member Author

philippjfr commented Jan 10, 2021

Examples

Slideshow

Advances image on click

class Slideshow(ReactiveHTML):
    
    index = param.Integer(default=0)
    
    _html = '<img id="img" width=400 src="https://picsum.photos/800/300?image=${index}"></img>'
    
    _dom_events = {'img': ['click']}
    
    def _img_click(self, event):
        self.index += 1

Slideshow(height=150, index=0)

slideshow

Simple Text Input

class Input(ReactiveHTML):
    
    value = param.String()
    
    _html = '<input id="input" value="${value}"></input>'

    _dom_events = {'input': ['change']}
    
i = Input()
pn.Row(i, i.param.value)

input

Adding children

class Div(ReactiveHTML):
    
    children = param.List()
    
    _html = '<div id="div">${children}</div>'
    
Div(children=[Slideshow(index=4, width=300, height=150),
              Slideshow(index=2, width=300, height=150)],
    height=300)

div

@philippjfr philippjfr changed the title Add ReactiveHTML model Add ReactiveHTML and IDOM support Jan 11, 2021
@MarcSkovMadsen
Copy link
Collaborator

MarcSkovMadsen commented Jan 11, 2021

Seems quite powerful. Did you invent the API your self? Is it inspired by IDOM or something else?

I'm just thinking that when you/ we want to communicate this. It would be powerful if there was already an existing user base familiar with the api or wanting to learn the api. And existing documentation and examples.

Could we say that now you can build components in Panel using the React, Preact or IDOM API? Or what is this?

@MarcSkovMadsen
Copy link
Collaborator

Would this replace a lot of the custom bokeh extensions that we create? For example would I wrap the Fast widgets using this api instead of as custom bokeh extensions?

@MarcSkovMadsen
Copy link
Collaborator

This API is close to a game changer because it will let users easily extend and customize Panel. But then the "competition" will more be on how performant the server and bokeh js library is for example. Some of the "competing solutions" are using other servers that might be even faster than tornado and maybe have a smaller code base which can run faster or take advantage or never features of the Python language. And there is also the Bokeh layout engine slowing things down. And then maybe the confusion between the "original" components implemented using bokeh extensions and then these components.

@mattpap
Copy link
Collaborator

mattpap commented Jan 11, 2021

And there is also the Bokeh layout engine slowing things down.

Not for long anymore. A CSS-based layout is scheduled to be worked on in near future.

@philippjfr
Copy link
Member Author

philippjfr commented Jan 11, 2021

I'm just thinking that when you/ we want to communicate this. It would be powerful if there was already an existing user base familiar with the api or wanting to learn the api

It's pretty similar to React htm syntax templating, except the state is represented as a bokeh DataModel, which is in turn synced to the parameterized object. So I'd say this provides the ability to write React-like components in Python. Being able to use arbitrary React components isn't quite exposed here but I guess is possible via the IDOM API. I'm still struggling a bit whether to delegate everything to IDOM here or whether my solution and IDOM have distinct use cases and tradeoffs.

Would this replace a lot of the custom bokeh extensions that we create? For example would I wrap the Fast widgets using this api instead of as custom bokeh extensions?

I would say yes, that's the goal, but we should have a detailed look at performance tradeoffs.

But then the "competition" will more be on how performant the server and bokeh js library is for example.

As @mattpap said I'm hoping we can close the gap on the layout performance front in the near future. In terms of server support I think the performance tradeoffs aren't huge for real world applications (correct me if you think I'm wrong here). That said the server infrastructure in Bokeh is definitely pluggable, and if a massively more performant server were to exist I think we could use that instead.

@MarcSkovMadsen
Copy link
Collaborator

MarcSkovMadsen commented Jan 11, 2021

How would a user/ package include css and js with a ReactiveHTML component? For example if they want to distribute it as a package.

For example with paulopes components the css and js is a part of the component and when served the css and js files are extracted and included in the template. But of course only once for each css or js file.

@MarcSkovMadsen
Copy link
Collaborator

If you delegate to IDOM you will also be dependent on IDOM and it's development+success.

@MarcSkovMadsen
Copy link
Collaborator

I've tried to get the branch up and running. But I'm stuck when trying to build the .js. I have run npm install.

$ panel build panel
Working directory: C:\repos\private\panel\panel
Using different version of bokeh, rebuilding from scratch.
Running npm install.
audited 53 packages in 0.794s

1 package is looking for funding
  run `npm fund` for details

found 0 vulnerabilities

Using C:\repos\private\panel\panel\tsconfig.json
Compiling TypeScript (41 files)
panel/models/file_download.ts:3:26 - error TS2306: File 'C:/repos/private/panel/panel/node_modules/@bokeh/bokehjs/build/js/types/styles/buttons.css.d.ts' is not a module.

3 import * as buttons from "@bokehjs/styles/buttons.css"
                           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
panel/models/reactive_html.ts:28:17 - error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.

28   return !isNaN(str) && !isNaN(parseFloat(str))
                   ~~~
panel/models/singleselect.ts:6:25 - error TS2306: File 'C:/repos/private/panel/panel/node_modules/@bokeh/bokehjs/build/js/types/styles/widgets/inputs.css.d.ts' is not a module.

6 import * as inputs from "@bokehjs/styles/widgets/inputs.css"
                          ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Linking modules
Output written to C:\repos\private\panel\panel\dist
 npm list
@holoviz/panel@0.11.0-a6 C:\repos\private\panel\panel
+-- @bokeh/bokehjs@2.3.0-dev.9
| +-- @bokeh/numbro@1.6.2
| +-- @bokeh/slickgrid@2.4.2702
| | +-- @types/slickgrid@2.1.30
| | | `-- @types/jquery@3.5.5
| | |   `-- @types/sizzle@2.3.2
| | +-- jquery@3.5.1
| | +-- jquery-ui@1.12.1
| | `-- tslib@1.14.1

@philippjfr
Copy link
Member Author

I've tried to get the branch up and running. But I'm stuck when trying to build the .js. I have run npm install.

See #1889, I'm seeing the same thing, hopefully those will be fixed once Bokeh 2.3.0dev10 is out. That said those do not prevent the compiled bundle from being emitted which means despite those errors it should all still work.

For example with paulopes components the css and js is a part of the component and when served the css and js files are extracted and included in the template. But of course only once for each css or js file.

Would imagine a similar mechanism, basically I would bundle them like all other CSS/JS dependencies, serve them on the server and then automatically interpolate them into the template.

@MarcSkovMadsen
Copy link
Collaborator

MarcSkovMadsen commented Jan 12, 2021

Thanks. I got the Slideshow example working. It's so neat.

Can you also use Panel components as parameter values? If yes then you would have solved the TemplatedHTML PR also. And it could be really powerful.

@MarcSkovMadsen
Copy link
Collaborator

MarcSkovMadsen commented Jan 12, 2021

I've created a preliminary example here #1896 of a Lottie Files player. There are things I cannot get working. Some hints would be appreciated. Then I will continue such that it could end up as a reference example.

@MarcSkovMadsen
Copy link
Collaborator

I've created a preliminary and alternative Lottie example using the Lottie web component. #1897. There are some thing that I cannot get working.

@mattpap
Copy link
Collaborator

mattpap commented Jan 15, 2021

class Input(ReactiveHTML):
   
   value = param.String()
   
   _html = '<input id="input-${id}" value="${value}"></input>'

   _dom_events = {'input': ['change']}
   
i = Input()
pn.Row(i, i.param.value)

Do those interpolations do any sanitizations at all? If not, it will be easy to mix HTML into values, leading to possibility for cross-site scripting. The reason IDOM's API is bulky (and why bokehjs' DOM APIs are designed the way they are designed), is because they don't allow mixing HTML with raw strings, in principle not allowing cross-site scripting.

@philippjfr
Copy link
Member Author

philippjfr commented Jan 15, 2021

Do those interpolations do any sanitizations at all? If not, it will be easy to mix HTML into values, leading to possibility for cross-site scripting.

This is absolutely vulnerable to potential cross-site scripting right now. It's also very much experimental for now and before deciding in what form to merge it I will add sanitization. If you have suggestions on how best to do that I'd love to see it. There's zero need to ever interpolate raw HTML into one of these templates.

@philippjfr philippjfr force-pushed the custom_html_model branch 2 times, most recently from 7aea258 to a503c2a Compare February 25, 2021 20:17
@philippjfr philippjfr changed the title Add ReactiveHTML and IDOM support Add ReactiveHTML Feb 25, 2021
@philippjfr
Copy link
Member Author

I now use bleach to sanitize the template variables and have removed the need for the -${id} on the id attribute of tags.

@philippjfr philippjfr added this to the v0.12.0 milestone Mar 8, 2021
@philippjfr philippjfr merged commit f242809 into master Mar 16, 2021
@philippjfr philippjfr deleted the custom_html_model branch March 16, 2021 10:46
@xavArtley
Copy link
Collaborator

xavArtley commented Mar 16, 2021

When compiling panel I get this error:

Compiling TypeScript (45 files)
panel/models/reactive_html.ts:193:23 - error TS7006: Parameter 'view' implicitly has an 'any' type.

193   private _align_view(view): void {
                          ~~~~
panel/models/reactive_html.ts:197:16 - error TS2322: Type 'unknown' is not assignable to type 'string'.

197       [halign, valign] = align
                   ~~~~~~

Linking modules

@philippjfr
Copy link
Member Author

Yeah, will push a fix up soon. Note that Typescript checking is not strict though and it still emits a working panel.js bundle.

@xavArtley
Copy link
Collaborator

xavArtley commented Mar 16, 2021

I tried to play with the PR however I was tring to make a custom button:

class Button(ReactiveHTML):
    
    title = param.String()
    
    _html = '<button id="button">${title}</button>'

    _dom_events = {'button': ['click']}
    
    def _button_click(self, event):
        print(event)
    
b = Button(title="test")
pn.Row(b, b.param.title)

Howver when I click I have this error:

image

And if I don't use at least one param in the _html I get the warning:
image

@philippjfr
Copy link
Member Author

Thanks, will fix both issues shortly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants