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

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

Closed
MarcSkovMadsen opened this issue Sep 28, 2023 · 1 comment
Labels
reactivehtml type: enhancement Minor feature or improvement to an existing feature
Milestone

Comments

@MarcSkovMadsen
Copy link
Collaborator

MarcSkovMadsen commented Sep 28, 2023

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

  • esm modules
    • import statements
    • import maps
  • async/ await syntax
  • jsx/ tsx
  • typescript

while ReactiveHTML does not or does not easily.

The consequences are

  • There are lots of javascript examples that do not easily translate into a ReactiveHTML component
  • Its not easy to translate Anywidget or Ipyreact examples or code into ReactiveHTML
  • There are lots of use cases that are really hard to support.

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.

  • esm modules: Run in new <script type="module"></script> tag
    • import statements: import(...).then syntax.
    • import maps: no workaround yet
  • async/ await syntax: .then syntax
  • jsx/ tsx: sucrase transpiler
  • typescript: sucrase transpiler
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 MarcSkovMadsen added type: enhancement Minor feature or improvement to an existing feature reactivehtml labels Sep 28, 2023
@MarcSkovMadsen MarcSkovMadsen added this to the Wishlist milestone Sep 28, 2023
@MarcSkovMadsen MarcSkovMadsen changed the title Add support for esm modules, async/ await, import maps, jsx/ tsx and typescript Add support for esm modules, async/ await, import maps, jsx/ tsx and typescript to ReactiveHTML Sep 28, 2023
@MarcSkovMadsen MarcSkovMadsen changed the title Add support for esm modules, async/ await, import maps, jsx/ tsx and typescript to ReactiveHTML ReactiveHTML: Add support for esm modules, async/ await, import maps, jsx/ tsx and typescript Sep 28, 2023
@MarcSkovMadsen
Copy link
Collaborator Author

Solved by JSComponent, ReactComponent and AnyWidgetComponent.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
reactivehtml type: enhancement Minor feature or improvement to an existing feature
Projects
None yet
Development

No branches or pull requests

1 participant