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

Refactor ReactiveHTML docs #5448

Merged
merged 29 commits into from Jan 20, 2024
Merged

Refactor ReactiveHTML docs #5448

merged 29 commits into from Jan 20, 2024

Conversation

@codecov
Copy link

codecov bot commented Aug 26, 2023

Codecov Report

All modified and coverable lines are covered by tests ✅

Comparison is base (2ff66b7) 83.60% compared to head (3471493) 81.09%.
Report is 367 commits behind head on main.

❗ Current head 3471493 differs from pull request most recent head c3f4be7. Consider uploading reports for the commit c3f4be7 to get more accurate results

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #5448      +/-   ##
==========================================
- Coverage   83.60%   81.09%   -2.52%     
==========================================
  Files         276      277       +1     
  Lines       40251    40394     +143     
==========================================
- Hits        33653    32757     -896     
- Misses       6598     7637    +1039     
Flag Coverage Δ
ui-tests 41.36% <55.55%> (-0.11%) ⬇️
unitexamples-tests 69.97% <100.00%> (-2.48%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

class LayoutSingleObject(pn.reactive.ReactiveHTML):
object = param.Parameter()

_ignored_refs = ("object",)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I don't add object to _ignored_refs its shown as a literal value. Is that a regression/ bug @philippjfr ?

"end": "delete state.start",
"save": """
data.value = canvas_el.toDataURL()
data.save=false""",
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need confirmation from @philippjfr that its OK and good practice to use param.Event parameters in the way I do with save and reset. I.e. be setting data.save=false in the script to enable triggering/ clicking it again.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You technically shouldn't have to do that, but I guess since the value isn't synced when it is reset to false it won't trigger the next time. We should fix this and ensure it does reset automatically.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in #6247

@philippjfr
Copy link
Member

Starting to think we should just turn this guide and the streamlit migration guide into tutorials.

@MarcSkovMadsen
Copy link
Collaborator Author

My plan was to create a tutorial highly inspired by AnyWidget then I would not have to invent things and it would be easy to compare to AnyWidget.

});
self.style()
const mainEle = document.querySelector("body")
mainEle.addEventListener("scrollend", (event) => {state.cy.resize().fit()})
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we lack a life cycle function after after_layout @philippjfr ? Why is a hack like listening for scrollend or adding a timeOut sometimes needed?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In #5195 (comment) @derrikmcs also notes that scrollend event is not supported by all browsers.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There sadly is no event after after_layout as that's the last step in the Bokeh rendering pipeline.


- `model` (default): Create child and render as Panel component.
- `literal`: Create child and insert the string value as `innerHTML`
- `template`: Create child and insert the string value as `innerText`
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need help to define these @philippjfr . I've seen different definitions and they are not consistently aligned with what I experience when using them. See for example examples and questions below. I'm so confused.

  • Can you confirm the are all rendered on js side. Sometimes I'm thinking either literal or template is rendered on python side using Jinja2.
  • I've tried to look at the .py and .ts code many times. But I cannot find the logic that distinguishes between literal and template. So I cannot learn from the code. Where is that code?
  • What does the names literal and template refer too? Please give me something so that I can remember the difference.
  • Why does the escaping not work consistently for literal and template values? Is it a feature or a bug? It makes it so hard to understand and trust that this is robust. See examples below.
  • Why can I set the v_model parameter value below to a markdown string that is rendered as a Markdown pane. But if I try setting it to an svg string then I get an error because a String parameter cannot be assigned a SVG pane? Is is a bug or a feature. I'm so confused here.
  • Why does ReactiveHTML even try to change parameter values to their Panel pane? It makes it so hard to use DataFrame etc. parameters.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the example that confuses me so much

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

pn.extension()

class CustomComponent(ReactiveHTML):
    v_model = param.String(default="I'm a **model** value. <em>emphasize</em>")
    v_literal = param.String(default="I'm a **literal** value. <em>emphasize</em>")
    v_template = param.String(default="I'm a **template** value. <em>emphasize</em>")

    _child_config = {
        "v_model": "model",
        "v_literal": "literal",
        "v_template": "template",
    }

    _template = """
<div id="el_model">${v_model}</div>
<div id="el_literal">${v_literal}</div>
<div id="el_template">${v_template}</div>
"""


component = CustomComponent(width=500, height=200).servable()
component.v_model=component.v_model.replace("**", "*")
component.v_literal=component.v_model.replace("**", "*")
component.v_template=component.v_model.replace("**", "*")
print(component.v_model)

svg = """<svg style="stroke: #e62f63;" width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg" slot="collapsed-icon">
<path d="M15.2222 1H2.77778C1.79594 1 1 1.79594 1 2.77778V15.2222C1 16.2041 1.79594 17 2.77778 17H15.2222C16.2041 17 17 16.2041 17 15.2222V2.77778C17 1.79594 16.2041 1 15.2222 1Z" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="M9 5.44446V12.5556" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="M5.44446 9H12.5556" stroke-linecap="round" stroke-linejoin="round"></path></svg>"""
CustomComponent(v_literal=svg, v_template=svg, width=500, height=200).servable()
CustomComponent(v_model=svg, v_literal=svg, v_template=svg, width=500, height=200).servable()

image


Here is an illustrative example

```{pyodide}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example below is important. I spent hours finding some combination that works.

One problem is that ReactiveHTML has issues inserting {{ ... }} values that contains tilde

Another is that I could not find a working way to just loop param. Instead I had to use param.params().values() which is cumbersome.

Another is that I could not find an easy way to get the current value of a parameter in the loop. So I ended up having to do object.owner[object.name].

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One problem is that ReactiveHTML has issues inserting {{ ... }} values that contains tilde

Will look at fixing that.

Another is that I could not find a working way to just loop param. Instead I had to use param.params().values() which is cumbersome.

.param can be looped directly.

Another is that I could not find an easy way to get the current value of a parameter in the loop. So I ended up having to do object.owner[object.name].

This is a good point, .param.values()[name] seems awkward.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can loop .param directly. It gives you a name of a parameter. But .param[name] does not seem to work. Thus you cannot get the actual parameter to work with.

@@ -268,7 +272,7 @@ def resolve_stylesheet(cls, stylesheet: str, attribute: str | None = None):
The stylesheet definition
"""
stylesheet = str(stylesheet)
if not stylesheet.startswith('http') and attribute and (custom_path:= resolve_custom_path(cls, stylesheet)):
if not stylesheet.startswith('http') and attribute and _is_file_path(stylesheet) and (custom_path:= resolve_custom_path(cls, stylesheet)):
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried running the below example. But it raised an error because it thought the _stylesheets value was a path. This change fixes the issue.

import param
from panel.reactive import ReactiveHTML


class CounterWidget(ReactiveHTML):
    value = param.Integer(default=0)

    _template = """  
    <button id="button_el" class="styled-button" onclick="${script('click_handler')}"></button>  
    """

    _scripts = {
        "render": "self.value()",
        "click_handler": "data.value+=1",
        "value": "button_el.innerHTML = `count is ${data.value}`"
    }
    
    _stylesheets = ["""\
.styled-button {  
    display: inline-block;
    padding: 10px 20px;
    font-size: 16px;
    font-weight: bold;
    text-align: center;
    text-decoration: none;
    background-color: #4CAF50;
    color: white;
    border: none;
    border-radius: 5px;
    cursor: pointer;
    transition: background-color 0.3s;
}  

.styled-button:hover {
    background-color: #45a049;  
}  
"""]  

CounterWidget().servable()

@@ -2,6 +2,10 @@

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like this "big" topic of custom components and ReactiveHTML is very much hidden because its several clicks down

image

Also I believe that it would make navigation easier of the sections and cards where aligned as much as possible between how-to and explanation pages. That would make navigation easier as you don't have to remember seperately where to find ReactiveHTML components material for how-to guides and for explanation.

image

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would recommend restructering the how-to navigation by having a Components section and moving the following cards there

  • construct components
  • arrange components
  • style components
  • build custom components

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds reasonable to me.

@MarcSkovMadsen
Copy link
Collaborator Author

I've reviewed a version of the docs deployed to dev site and reviewed. Among other things I've fixed some broken navigation.

I've triggered the docs dev build again for a second review.


### React, Preact and Vue

`ReactiveHTML` can be used with [React](https://react.dev/), [Preact](https://preactjs.com/) and [Vue](https://vuejs.org/).
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm no longer sure ReactiveHTML works with React (and MaterialUI). In mui/material-ui#17473 they claim React and Material does not and do not want to support ShadowRoot.

There is a minimal example here that I tried to get working but could not https://discourse.holoviz.org/t/help-with-panel-react-materialui/5204/3?u=marc

@MarcSkovMadsen
Copy link
Collaborator Author

Can I do something to move this forward and be easier to review+accept @philippjfr ? I think it will be a big step forward for ReactiveHTML and custom components in Panel.

@philippjfr
Copy link
Member

I think you've really done a great job here. It's really on me to review and see if we can fix some of the issues you raised.

@MarcSkovMadsen
Copy link
Collaborator Author

MarcSkovMadsen commented Oct 31, 2023

We could postpone the "hard to solve" issues if there is not time for it in the forseable future, fix the "easy ones" and release it in a first step. Then take a second step to solve the harder issues and maybe improve ReactiveHTML too.

Copy link

@tomascsantos tomascsantos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR is a work of art. thanks so much @MarcSkovMadsen for putting it together, I had no idea ReactiveHTML was so POWERFUL!

One last open ended question that I'm wondering about...:
what is the difference between reactiveHTML and templates? In other words, why don't templates inherit from reactive HTML?

doc/explanation/components/reactive_html_components.md Outdated Show resolved Hide resolved

- `_child_config` (dict): This is a mapping that controls how children are rendered.
- `_ignored_refs` (tuple[str]): This is tuple of parameter names. Use this to render Panel components as Panel components and not their value or object.
- `_dom_events` (dict): This is a mapping of node IDs to DOM events to add event listeners to.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's a node ID here? maybe a link would be useful? Little hard to understand this, and I'm not sure what python objects I would put into this dictionary

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

having read through more of the document, I see there is a section below that goes into more detail, can we use: https://stackoverflow.com/a/16426829 ?
image

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Which section are you referring to?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recommend linking all the sections... I just reviewed this reading through github markdown, not sure if when pushed to the website this would add links automatically

doc/explanation/components/reactive_html_components.md Outdated Show resolved Hide resolved
You can also declare the following optional attributes:

- `_child_config` (dict): This is a mapping that controls how children are rendered.
- `_ignored_refs` (tuple[str]): This is tuple of parameter names. Use this to render Panel components as Panel components and not their value or object.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is this different from child_config?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I notice this one doesn't have a more in-depth section below and from the explanation of _child_config is seems there is some overlap?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think its because I don't understand the _ignored_refs. I could just see I had to use it to get some examples to work. Could you please elaborate @philippjfr . Thanks.


By combining Python callbacks and JavaScript callbacks, you can create dynamic and interactive components that respond to user interactions.

### Parameter callbacks

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This section was a little confusing because it seems to be a duplicate of the above?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its a subsection of the section above. The intention is to systematically go through the different kind of scripts. And there is some overlap with the above.

Any ideas to how I can make this clearer or better?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can take this with a grain of salt but,

If you feel you need this section, I might change the example code so that there is no overlap, and then include a callback to the previous example:

If the key in the _scripts dictionary matches one of the parameters declared on the class the callback will automatically fire whenever the synced parameter value changes. This is how counts changes in the example above. Here's another example let's say we have a class...

@MarcSkovMadsen
Copy link
Collaborator Author

MarcSkovMadsen commented Jan 14, 2024

This PR is a work of art. thanks so much @MarcSkovMadsen for putting it together, I had no idea ReactiveHTML was so POWERFUL!

One last open ended question that I'm wondering about...: what is the difference between reactiveHTML and templates? In other words, why don't templates inherit from reactive HTML?

Hi @tomascsantos

I think the answer is history.

  • Bokeh comes with support for jinja templates. These has been used to build the FastListTemplate and similar templates. The jinja templates can be used to layout Bokeh/ Panel components in a HTML template.
  • Bokeh comes with bokeh models to build advanced Bokeh/ Panel components using typecript and build tools
  • Later there was a need for more easily creating custom components with bi-directional communication and javascript without build tools. Then ReactiveHTML was introduced. In principle ReactiveHTML can replace the layout functionality of templates (I don't know about performance). I don't think it would ever replace the basic templating things like specifying the favicon etc. in the header of a template. But if I should start over creating the FastListTemplate etc. one day, then I would probably have done that by building up sub components in ReactiveHTML and combining them.
  • There is also the future. And ReactiveHTML does not support ESM modules which Javascript development is moving towards. And which Anywidget is supporting with great success. So some day this will be supported in Panel as well. See Add support for ESM bundle on ReactiveHTML #5593.

And thanks for the nice feedback. I'm a sucker for that :-)

@philippjfr
Copy link
Member

pre-commit.ci autofix

@philippjfr
Copy link
Member

philippjfr commented Jan 20, 2024

I will merge as is and if further fixes/cleanup are needed will address that as part of the ESM docs in #5593

@philippjfr philippjfr merged commit d357a8a into main Jan 20, 2024
3 of 14 checks passed
@philippjfr philippjfr deleted the enhancement/reactive-html-docs branch January 20, 2024 18:35
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.

None yet

4 participants