anywidget
anywidget copied to clipboard
[Suggestion] Using HTML template in an external file, just like it works for JS/CSS
This is a great project!
I was trying to build a controller for a custom chart viewer widget. While it is possible to manually create DOM elements via JS - it is very cumbersome. A potentially better functionality might be to allow users to create HTML templates (for structuing/layouting) and then have them work with the custom JS/CSS in the way that it currently does with external files.
For example, it would be great if the following kinda pseudo-code was supported:
class ExampleWidget(anywidget.AnyWidget):
...
# HTML template file
_html = pathlib.Path().resolve() / "index.html"
# JS
_esm = pathlib.Path().resolve() / "index.js"
# CSS
_css = pathlib.Path().resolve() / "index.css"
...
ew = ExampleWidget()
ew
I was wondering if this is even possible to implement in the anywidget framework. If so, is this something on the roadmap ?
This is a neat idea. You can get at least a preliminary basic solution working in user-land by implementing a subclass of AnyWidget:
import anywidget
import traitlets
class HtmlWidget(anywidget.AnyWidget):
_esm = """
async function render(ctx) {
ctx.el.innerHTML = ctx.model.get("_html");
const childEsm = ctx.model.get("_child_esm");
let cleanup;
try {
const childEsmUrl = URL.createObjectURL(new Blob([childEsm], { type: "text/javascript" }));
const childRender = (await import(childEsmUrl)).render;
URL.revokeObjectURL(childEsmUrl);
cleanup = childRender(ctx);
} catch(e) {
console.error(e);
}
return cleanup;
}
export { render }
"""
def __init__(self, *args, **kwargs):
html_value = getattr(self, "_html")
html_trait = traitlets.Unicode(str(html_value)).tag(sync=True)
child_esm_value = getattr(self, "_child_esm")
child_esm_trait = traitlets.Unicode(str(child_esm_value)).tag(sync=True)
self.add_traits(_html=html_trait, _child_esm=child_esm_trait)
super().__init__(*args, **kwargs)
Then, your widget can inherit from this subclass.
class MyWidget(HtmlWidget):
_html = """
<h1>Test</h1>
<div class="red-box"></div>
"""
_css = """
.red-box { width: 100px; height: 100px; background-color: red; }
.blue-border { border: 20px solid blue; }
"""
_child_esm = """
function render({ model, el }) {
setTimeout(() => {
el.querySelector(".red-box").classList.add("blue-border");
}, 1000);
}
export { render };
"""
w = MyWidget()
w
It would take some additional work to get the Path-based loading working and to get features like initialize() function support and hot-module-reloading which might make more sense to implement at a library level.
Not sure about integrating templating but that would probably depend a lot on the use case.
Thanks for taking the time out and sharing a starter code! Will try this out.
Thanks for your idea! Integrating HTML templates with anywidget for a nicer templating experience is interesting. However, it is not something that is currently on the roadmap for the "core" of anywidget.
Widgets fundamentally insert content into the DOM using JavaScript, a process commonly handled by front-end frameworks like Backbone, React, Vue, and Svelte (which provide nicer development experiences). anywidget's current strategy focuses on creating adapters for these frameworks to simple and minimal primitives, allowing each to manage updates in "user-land." This means anywidget does not need to endorse a particular framework and build upon more stable web standards.
Implementing an HTML template system in anywdget would parallel creating another such framework, significantly expanding the project's scope. I'm not totally opposed to such a system, however, I envision it would be something that lives outside of anywidget's core (like the rest of these frameworks). @keller-mark's idea seems like a great starting point, and I would be open to working on a community effort to standardize something like this on-top of anywidget's core.