panel
panel copied to clipboard
Add support for ESM bundle on ReactiveHTML
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
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()
- [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
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.
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.
?
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.
Ah, got you. I was trying to copy the API of anywidget which does not separate
data
andmodel
. 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?
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
.
Please consider adding the requirements from https://github.com/holoviz/panel/issues/5550 to your todo list in the first post 😄
One open question is also if you can use code in _esm
to insert Panel objects? See https://github.com/holoviz/panel/issues/5551
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.
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.
Thanks. It was meant as a joke. But not perceived that way.
Progress
(Low frame rate is due to the GIF, not the example running slow.)
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}/>`
}
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.
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;
}
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}/>;
}
The global namespace also contains a React
object containing:
React: {
Component,
useCallback,
useContext,
useEffect,
useErrorBoundary,
useLayoutEffect,
useState,
useReducer,
createElement: h
}
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
.
... 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.
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.
Yes, ESMComponent seems like a good name for it. And also yes, I'm working on adding the necessary shims to get mui working.
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.
The downside of carving it out as a separate component is complexity. Will you explain both ReactiveHTML
and ...ESM...
? What should users choose?
The same is true if you overload ReactiveHTML
, suddenly you have this monstrosity which does 500 different things.
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?
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.
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?
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.
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.