holoviz / panel

Panel: The powerful data exploration & web app framework for Python
https://panel.holoviz.org
BSD 3-Clause "New" or "Revised" License
4.83k stars 519 forks source link

ReactiveHTML: Add support for esm modules, async/ await, import maps, jsx/ tsx and typescript #5550

Closed MarcSkovMadsen closed 1 week ago

MarcSkovMadsen commented 1 year ago

I'm working on the ReactiveHTML docs. When comparing to AnyWidget and Ipyreact I can see that they support modern browser features like

while ReactiveHTML does not or does not easily.

The consequences are

Please either add these features to ReactiveHTML or document how to add them.

Workaround

In the below example I show how to work around some of the missing features. This example is based on the IpyReact getting started example.

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

pn.extension()

# https://esm.sh/#docs
# https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import

_ESM_ID=-1

def _get_esm_id():
    global _ESM_ID
    _ESM_ID += 1
    return _ESM_ID

def _esm(render: str, container: str="container"):
    _id = _get_esm_id()
    render = render.replace(
        "export default function({value, set_value, debug}) {",
        f"window.__component_{ _id }__ = ({{value, set_value, debug}}) => {{"
    )
    return {
        "render": f"""
state.root = ReactDOM.createRoot({container});
window.__render_{ _id }__=self.value

import("https://cdn.jsdelivr.net/npm/sucrase@3.34.0/+esm").then((sucrase) => {{
    var code = `{render}`;
    if (data.debug || false){{ console.log("source code", code) }}
    code = sucrase.transform(code, {{transforms: ["jsx", "typescript"], filePath: "test.jsx"}}).code
    if (data.debug || false){{ console.log("transpiled", code) }}

    code = code + ";window.__render_{ _id }__()"

    const el = document.createElement('script');
    el.type = 'module';
    el.textContent = code;
    {container}.appendChild(el);
}});
""",
    "value": f"""
const props = {{ value: data.value, set_value: (value)=>{{data.value=value}}, debug: data.debug || false }}
const component = window.__component_{ _id }__(props)
state.root.render(component)
"""
    }

class ReactComponent(ReactiveHTML):
    value = param.Number()
    debug = param.Boolean(False)

    _template = """
<div id="container"></div>
"""

    _scripts = _esm("""
import confetti from 'https://unpkg.com/canvas-confetti@1.4.0/dist/confetti.module.mjs'

export default function({value, set_value, debug}) {
    return <button onClick={() => confetti() && set_value(value + 1)}>
        {value || 0} times confetti
    </button>
};
""")

    __javascript__=[
        "https://unpkg.com/react@latest/umd/react.development.js",
        "https://unpkg.com/react-dom@latest/umd/react-dom.development.js",
    ]
    __javascript__modules__=["https://cdn.jsdelivr.net/npm/sucrase@3.34.0/dist/index.min.js"]

component = ReactComponent(width=500, height=200).servable()
print(component._scripts)

Discussion

You can see from the workaround above that it could be possible to allow users to take IpyReact component code and almost copy paste it into a ReactiveHTML component with a utility function like _esm.

One question I have is whether we want to support the component signature

export default function({value, set_value, debug}) 

or

export default function({data, debug}) 

And somehow the code needs to add functionality to rerender when data parameter values changes. Should that user specify which parameters trigger a rerender? Or can that be automatically determined?

Another question I have is how to inject Panel components from _scripts instead of via template variables ${...} in the _template. See #5551

MarcSkovMadsen commented 1 week ago

Solved by JSComponent, ReactComponent and AnyWidgetComponent.