panel icon indicating copy to clipboard operation
panel copied to clipboard

Add support for ESM bundle on ReactiveHTML

Open philippjfr opened this issue 8 months ago • 28 comments

class Counter(pn.reactive.ReactiveHTML):
    _esm = """
    export function render({ model, el }) {
      let button = document.createElement("button");
      button.classList.add("counter-button");
      button.innerHTML = `count is ${model.count}`;
      button.addEventListener("click", () => {
        model.count += 1
      });
      model.properties.count.change.connect(() => {
        button.innerHTML = `count is ${model.count}`;
      });
      el.appendChild(button);
    }
    """
    _stylesheets=["""
    .counter-button { background-color: #ea580c; }
    .counter-button:hover { background-color: #9a3412; }
    """]
    count = param.Integer(default=0)

counter = Counter()
counter.count = 42
counter

counter

class ConfettiWidget(pn.reactive.ReactiveHTML):
    _esm = """
    import confetti from "https://esm.sh/[email protected]";

    export function render({model, el}) {
      let btn = document.createElement("button");
      btn.classList.add("confetti-button");
      btn.innerHTML = "click me!";
      btn.addEventListener("click", () => {
        confetti();
      });
      el.appendChild(btn);
    }
    """
    _stylesheets = ["""
    .confetti-button { background-color: #ea580c; }
    .confetti-button:hover { background-color: #9a3412; }
    """]
    
ConfettiWidget()

confetti

  • [x] Add nicer API for listening to model events
  • [x] Add ability to specify Path and then dynamically watch the path for changes
  • [x] Give users the ability to write Preact components
  • [ ] Add docs
  • [ ] Add tests

philippjfr avatar Oct 07 '23 10:10 philippjfr

Codecov Report

Attention: Patch coverage is 56.36943% with 137 lines in your changes are missing coverage. Please review.

Project coverage is 40.36%. Comparing base (4322499) to head (b3b261d). Report is 1 commits behind head on main.

:exclamation: Current head b3b261d differs from pull request most recent head f129ebf

Please upload reports for the commit f129ebf to get more accurate results.

Files Patch % Lines
panel/esm.py 33.58% 89 Missing :warning:
panel/reactive.py 40.90% 39 Missing :warning:
panel/io/datamodel.py 86.66% 2 Missing :warning:
panel/io/resources.py 77.77% 2 Missing :warning:
panel/links.py 33.33% 2 Missing :warning:
panel/util/checks.py 50.00% 2 Missing :warning:
panel/tests/ui/test_esm.py 98.55% 1 Missing :warning:
Additional details and impacted files
@@             Coverage Diff             @@
##             main    #5593       +/-   ##
===========================================
- Coverage   81.52%   40.36%   -41.17%     
===========================================
  Files         318      316        -2     
  Lines       46776    46706       -70     
===========================================
- Hits        38136    18851    -19285     
- Misses       8640    27855    +19215     
Flag Coverage Δ
ui-tests 40.36% <56.36%> (?)

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

:umbrella: View full report in Codecov by Sentry.
:loudspeaker: Have feedback on the report? Share it here.

codecov[bot] avatar Oct 07 '23 11:10 codecov[bot]

WTF! Where did data go? Are parameter values now on model? And What about state etc? And remember that after layout and delete life cycle hooks are still needed.

MarcSkovMadsen avatar Oct 07 '23 11:10 MarcSkovMadsen

?

philippjfr avatar Oct 07 '23 11:10 philippjfr

Ah, got you. I was trying to copy the API of anywidget which does not separate data and model. For consistency that's probably not a great idea, so will probably rename back to data.

philippjfr avatar Oct 07 '23 11:10 philippjfr

Ah, got you. I was trying to copy the API of anywidget which does not separate data and model. For consistency that's probably not a great idea, so will probably rename back to data.

If it was just possible to merge data and model?

MarcSkovMadsen avatar Oct 07 '23 11:10 MarcSkovMadsen

Regarding: "Give users the ability to write Preact components".

Remember the purpose is not to enable preact. The purpose is to make it easy for users to create react components in a simple way. Almost like copy pasting existing examples. And those examples very often use JSX or even typescript/ TSX.

With Preact its complicated to replicate React examples. Skilled React developers can do this via clever configuration of package.json file. But with ReactiveHTML its complicated. Furthermore Preact points to htm for JSX like syntax.

In https://github.com/holoviz/panel/issues/5550 and inspired by IpyReact you can see how React, JSX and typescript/ TSX more easily can be supported using Sucrase. I think we should follow this path.

We should either document how to use React + Sucrase with ReactiveHTML + _esm. Or even better create a ready to use ReactBaseComponent.

MarcSkovMadsen avatar Oct 07 '23 11:10 MarcSkovMadsen

Please consider adding the requirements from https://github.com/holoviz/panel/issues/5550 to your todo list in the first post 😄

MarcSkovMadsen avatar Oct 07 '23 11:10 MarcSkovMadsen

One open question is also if you can use code in _esm to insert Panel objects? See https://github.com/holoviz/panel/issues/5551

MarcSkovMadsen avatar Oct 07 '23 11:10 MarcSkovMadsen

If it was just possible to merge data and model?

It's a bad idea because a user may define properties that clash with properties that are automatically inherited.

Remember the purpose is not to enable preact. The purpose is to make it easy for users to create react components in a simple way. Almost like copy pasting existing examples. And those examples very often use JSX or even typescript/ TSX.

The idea there is that we already use and export Preact so they can start using it at zero extra cost. This does not preclude actual React based workflows but those will come at the cost of loading React on top of everything else.

One open question is also if you can use code in _esm to insert Panel objects? See #5551

Will have to think about that.

philippjfr avatar Oct 07 '23 11:10 philippjfr

WTF! Where did data go? Are parameter values now on model? And What about state etc? And remember that after layout and delete life cycle hooks are still needed.

I think we have to be a little more careful with our language. We've heard complaints that we weren't always very welcoming.

maximlt avatar Oct 08 '23 13:10 maximlt

Thanks. It was meant as a joke. But not perceived that way.

MarcSkovMadsen avatar Oct 08 '23 16:10 MarcSkovMadsen

Progress

threejs

(Low frame rate is due to the GIF, not the example running slow.)

philippjfr avatar Nov 19 '23 14:11 philippjfr

With the latest version you can now leverage Preact from within the ESM bundle:

import pathlib
import param
import panel as pn

from panel.reactive import ReactiveHTML

class Todo(ReactiveHTML):

    tasks = param.List()

    _esm = pathlib.Path(__file__).parent / 'preact.js'

preact = Todo(width=800, height=600)
    
preact.servable()
function App(props) {
  const [newTask, setNewTask] = useState('');
  const [tasks, setTasks] = useState(props.tasks);

  const addTodo = () => {
    const task = { name: newTask, done: false }
    setTasks([...tasks, task]);
    props.data.tasks = [...tasks, task]
    setNewTask('');
  }

  const onInput = (event) => {
    const { value } = event.target;
    setNewTask(value)
  }

  const onKeyUp = (event) => {
    const { key } = event;
    if (key === 'Enter') {
      addTodo();
    }
  }

  return html`
    <div class="task-container">
      <div class="task-input">
        <input
          placeholder="Add new item..."
          type="text"
          class="task-new"
          value=${newTask}
          onInput=${onInput}
          onKeyUp=${onKeyUp}
        />
        <button class="task-add" onClick=${addTodo}>Add</button>
      </div>
      <ul class="task-list">
        ${tasks.map((task, index) => html`
          <li key=${index} class="task-item" v-for="task in state.tasks">
            <label class="task-item-container">
              <div class="task-checkbox">
                <input type="checkbox" class="opacity-0 absolute" value=${task.done} />
                <svg class="task-checkbox-icon" width=20 height=20 viewBox="0 0 20 20">
                  <path d="M0 11l2-2 5 5L18 3l2 2L7 18z" />
                </svg>
              </div>
              <div class="ml-2">${task.name}</div>
            </label>
          </li>
        `)}
      </ul>
    </div>
  `;
}

export function render({ data, model, el }) {
  return html`<${App} tasks=${data.tasks} data=${data}/>`
}

todo

philippjfr avatar Nov 20 '23 16:11 philippjfr

I've got sucrase working now so you can write React code without htm. At only 5kb increase in bundle size that seems worth it.

philippjfr avatar Nov 20 '23 18:11 philippjfr

So the example above now works with:

interface Task {
  name: string;
  done: boolean
};

function App() {
  const [newTask, setNewTask] = useState('');
  const [tasks, setTasks] = useState([] as Task[]);

  const addTodo = () => {
    setTasks([...tasks, { name: newTask, done: false }]);
    setNewTask('');
  }

  const onInput = (event: Event) => {
    const { value } = event.target as HTMLInputElement;
    setNewTask(value)
  }

  const onKeyUp = (event: KeyboardEvent) => {
    const { key } = event;
    if (key === 'Enter') {
      addTodo();
    }
  }

  return (
    <div class="task-container">
      <div class="task-input">
        <input
          placeholder="Add new item..."
          type="text"
          class="task-new"
          value={newTask}
          onInput={onInput}
          onKeyUp={onKeyUp}
        />
        <button class="task-add" onClick={addTodo}>Add</button>
      </div>
      <ul class="task-list">
        {tasks.map((task, index) => (
          <li key={index} class="task-item" v-for="task in state.tasks">
            <label class="task-item-container">
              <div class="task-checkbox">
                <input type="checkbox" class="opacity-0 absolute" value={task.done} />
                <svg class="task-checkbox-icon" viewBox="0 0 20 20">
                  <path d="M0 11l2-2 5 5L18 3l2 2L7 18z" />
                </svg>
              </div>
              <div class="ml-2">{task.name}</div>
            </label>
          </li>
        ))}
      </ul>
    </div>
  );
}

export function render({ data, model, el }) {
  const app = <App tasks={data.tasks}/>;
  return app;
}

philippjfr avatar Nov 20 '23 18:11 philippjfr

Okay I think this example demonstrates the React API fairly well:

Python

import pathlib
import param
import panel as pn

from panel.reactive import ReactiveHTML

class Example(ReactiveHTML):

    color = param.Color()

    text = param.String()
    
    _esm = pathlib.Path(__file__).parent / 'react_demo.js'

example = Example(text='Hello World!')

pn.Row(pn.Param(example.param, parameters=['color', 'text']), example).servable()

JSX

function App(props) {
  const [color, setColor] = props.state.color
  const [text, setText ] = props.state.text
  const style = {color: color}
  return (
    <div>
      <h1 style={style}>{text}</h1>
      <input
        value={text}
        onChange={e => setText(e.target.value)} 
      />
    </div>
  );
}

export function render({ state }) {
  return <App state={state}/>;
}

react_demo

The global namespace also contains a React object containing:

React: {
      Component,
      useCallback,
      useContext,
      useEffect,
      useErrorBoundary,
      useLayoutEffect,
      useState,
      useReducer,
      createElement: h
    }

philippjfr avatar Nov 20 '23 22:11 philippjfr

I think it's a pretty clean API now. Here's another demo:

https://github.com/holoviz/panel/assets/1550771/1f3abc79-2beb-457b-844e-a89809f4bc93

This demonstrates:

  • Bi-directional syncing of state parameters and React state variables
  • ESM module importing
  • Automatic creation of state hooks with the state argument to the render function
  • Availability of the React namespace
  • Automatic transpilation of JSX / TypeScript code

That said, I'm now considering whether we should split ReactiveHTML and ReactiveESM.

philippjfr avatar Nov 20 '23 23:11 philippjfr

... and also considering whether ReactiveESM is the right name?

I don't think AnyWidget name would be right for us because a widget is an input component in our terminology.

But would AnyComponent, BaseComponent, ESMComponent, BaseESMComponent or something similar be a better name? How can we communicate its an equivalent of AnyWidget in our ecosystem supporting any type of component?

The one I like the most is BaseComponent. I would choose AnyComponent if I had to explain it was similar to AnyWidget but supporting more than widgets.

MarcSkovMadsen avatar Nov 21 '23 11:11 MarcSkovMadsen

I think building a MaterialUI component using the ESM and (P)React functionality would be a really good test if you have done this right.

MarcSkovMadsen avatar Nov 21 '23 11:11 MarcSkovMadsen

Yes, ESMComponent seems like a good name for it. And also yes, I'm working on adding the necessary shims to get mui working.

philippjfr avatar Nov 21 '23 11:11 philippjfr

I would think twice about putting ESM in the name :-) Focus on what the component can do for the developer, not what technology its build on. Its AnyWidget not ESMWidget because its much easier to explain.

MarcSkovMadsen avatar Nov 21 '23 11:11 MarcSkovMadsen

The downside of carving it out as a separate component is complexity. Will you explain both ReactiveHTML and ...ESM...? What should users choose?

MarcSkovMadsen avatar Nov 21 '23 14:11 MarcSkovMadsen

The same is true if you overload ReactiveHTML, suddenly you have this monstrosity which does 500 different things.

philippjfr avatar Nov 21 '23 14:11 philippjfr

Would it be an idea to give ...ESM.. a nice name and focus on that one? Maybe deprecate ReactiveHTML over a long period of time?

MarcSkovMadsen avatar Nov 21 '23 14:11 MarcSkovMadsen

Yes, in principle. The nice thing about ReactiveHTML is that thanks to the templating you can write a useful component entirely without knowing any Javascript, so I'd be hesitant to deprecate it, but certainly it could be de-emphasized in the docs.

philippjfr avatar Nov 21 '23 14:11 philippjfr

OOOhhhhh this is cool!

The downside of carving it out as a separate component is complexity. Will you explain both ReactiveHTML and ...ESM...? What should users choose?

As a potential user... I have this question ^ could there be a feature comparison breakdown available somewhere?

tomascsantos avatar Jan 15 '24 00:01 tomascsantos

I've tried to start understanding and documenting. It lead to a lot of questions that I've put in. I will stop now and wait for answers. There are too many things that don't work for me or that I don't understand to proceed.

MarcSkovMadsen avatar May 05 '24 05:05 MarcSkovMadsen

This is because this PR was absolutely not in a state that should be reviewed yet. Most of the items you highlighted were on my to do list but it's good to have them recorded anyway.

philippjfr avatar May 05 '24 06:05 philippjfr