support for React, Vue, Svelte, Solid, Angular, etc, by making Custom Elements
Hello! This lib is awesome! I've been wanting something like this, but in the form of Custom Elements, so that it can be supported no matter what project I'm working in, while staying in declarative-reactive paradigms of the current app.
Would you be interested in Tweakpan elements?
For example, based on the Number example, It could look like the following in an HTML file:
<tweak-pane id="pane">
<!-- This one has only the number input (not a range) -->
<tweak-number name="speed" value="0.5"></tweak-number>
<!-- This one also has the draggable slider, because it is a range. -->
<tweak-number name="count" value="10" min="0" max="100"></tweak-number>
</tweak-pane>
<script>
const pane = document.getElementById('pane')
// pane.params could be the objects that contains all the values (besides also being able to read the DOM element values directly)
console.log(pane.params)
// provide a custom params object (random idea, we could chat about how we want it to work):
pane.params = {...}
</script>
For simplicity, someone might want to integrate into HTML (or React/Vue/Solid/etc) by using only the root element, but letting it create the pane without specifying the child elements:
HTML:
<tweak-pane id="pane"></tweak-pane>
<script>
pane.params = {...}
pane.autoBind = true // creates the UI automatically
</script>
Solid.js (and similar, f.e. JSX, React, Preact, etc):
function MyApp() {
const [params, setParams] = createSignal({...})
return <tweak-pane params={params()} autoBind={true}></tweak-pane>
}
Lit (with html template tag):
class MyEl extends LitElement {
params = {...}
render() {
return html`<tweak-pane .params=${this.params} autobind></tweak-pane>`
}
}
etc, etc, etc.
By making a set of Custom Elements, we can automatically support all declarative-reactive frameworks.
TweakPane is already written as plain JS components, so what we would do is wrap the API in custom elements.
For example, here's what it would start to look like using Lume Element (my lib for defining Custom Elements more simply), but you can also imagine using another Custom Element lib like Lit:
import {element, Element, attribute} from '@lume/element'
import {createEffect, onCleanup} from 'solid-js' // (Lume Element is built over reactivity and templating from Solid.js)
@element('tweak-pane')
class TweakPane extends Element {
#params = {}
@attribute
get params() {
return this.#params
}
set params(val) {
if (typeof val === 'string') this.#params = JSON.parse(val) // string values can come from HTML attributes
else this.#params = val
}
@booleanAttribute autoBind = false // f.e. `<tweak-pane auto-bind />`
#pane = new Pane()
connectedCallback() {
super.connectedCallback()
createEffect(() => {
// Any time `this.params` or `this.autoBind` change (f.e. because the user set a new values via JS property or HTML attribute) this "effect" will re-run because Lume Element attribute properties are reactive (Solid signals).
if (!this.autoBind) return
for (const key of Object.keys(this.params)) {
this.#pane.addBinding(this.params, key)
}
// onCleanup runs any time the effect will re-run, to clean anything up from the last run.
// This is the simplest/naive implementation, any time `this.params` changes, just make a whole new Pane with new params.
// In a more advanced implementation, maybe we diff params, change only the parts of the pane that are added/removed.
onCleanup(() => {
this.#pane.dispose()
this.#pane = new Pane()
})
})
// ... etc ...
}
// ... etc ...
}
Furthermore, we can provide TypeScript types for JSX type checking in Solid, React, etc. For example, here's how I hook Lume 3D elements into JSX and DOM type defs:
https://github.com/lume/lume/blob/a5ea06101174ed3f8fc165b82a76d1521ae7f5ce/src/cameras/CameraRig.ts#L425-L437
and React users can import the React type:
https://github.com/lume/lume/blob/a5ea06101174ed3f8fc165b82a76d1521ae7f5ce/src/cameras/CameraRig.react-jsx.d.ts
We can similarly hook up type defs for other systems that also rely on JSX (Svelte and Vue use JSX type defs for their templating even though the syntax is not JSX).
Basically we can write components once, run within any declarative-reactive framework, and the only framework-specific thing we need to do is provide the type hookup for TS users.
Thank you for using Tweakpane!
Interesting, but currently I don't plan to use custom elements because it will prevent quick hacks like this: https://github.com/cocopon/tweakpane/issues/508
User can easily customize the pane with CSS and JS, and this would be one of the nice things of Tweakpane.
@cocopon Hello! Custom Elements are only a way to organize code, adding a declarative interface, but it would not prevent that from being possible. We would just need to think about how we would want to expose the customization.
quick hacks like this: https://github.com/cocopon/tweakpane/issues/508
If you have any more samples, paste here for reference, so we can think about them.
It can purposefully be designed to be extendable. For example:
<tweak-pane>
<tweak-number name="count" value="10" min="0" max="100">
<button class="my-reset-button" slot="suffix"></button>
</tweak-number>
</tweak-pane>
Or similar. That's equivalent of the above hack
Plus, we can leave all ShadowRoots open, so full control by JS is still possible as before.
const myButton = document.createElement('button')
// DOM is fully accessible.
tweakpaneElement.shadowRoot
.querySelector('.some-element')
.append(myButton)
And for example, we can also make sure the JS API is fully exposed:
<tweak-pane id="one"></tweak-pane>
<script type="module">
import {Pane} from 'tweakpane'
const tweakElement = document.querySelector('#one')
const tweakpane = tweakElement.pane
console.log(tweakpane instanceof Pane) // true
</script>
etc
User can easily customize the pane with CSS and JS
There are various ways to allow CSS overrides for full control:
- Global API that accepts CSSStyleSheet, CSS string, or URL to stylesheet, and it will apply to all tweakpan instances
import {addStyle} from 'tweakpane' addStyle(`.foo {color: red}`) const sheet = new CSSStyleSheet() sheet.replaceSync(`.bar {color: blue}`) addStyle(sheet) addStyle(new URL('./path/to/foo.css', import.meta.url)) - CSS variables
- elements can have well-design theme variables, f.e.
tweak-pane { --folder-header-color: pink; }
- elements can have well-design theme variables, f.e.
- CSS parts (this is an API specifically for Custom Elements)
- elements inside the custom elements can be exposed for external styling, f.e. inside a tweakpane element there could be
<div part="header">...</div>and then user can style:<tweak-pane id="myPane"></tweak-pane> <style> #myPane::part(header) { color: cyan } </style>
- elements inside the custom elements can be exposed for external styling, f.e. inside a tweakpane element there could be
- All elements could accept a
stylesheetattribute/property that accepts CSS string, CSSStyleSheet, or URL to a stylesheet- example of this one here: https://github.com/lume/code-mirror-el/?tab=readme-ov-file#usage-example (see
stylesheetdocumentation further below). - this is nice because it allows a variety of ways to style the whole tweakpane instance. Not just in plain HTML, but via JS f.e.
The stylesheet can apply to all elements used within a tweak-pane element (i.e. global for that instance). So it would be possible to have two panes with different themes, for example:const sheet = new CSSStyleSheet(); sheet.replaceSync(`...`) // plain JS varieties: tweakpaneElement.stylesheet = `.foo {color: deeppink}` tweakpaneElement.stylesheet = document.querySelector('link#tweakpane-style') // <link> element tweakpaneElement.stylesheet = document.querySelector('style#tweakpane-style') // <style> element tweakpaneElement.stylesheet = sheet // Or in any declarative framework such as React JSX, Vue, Svelte, etc: return <tweak-pane stylesheet={`.lorem {background: purple}`}>...</tweak-pane> return <tweak-pane stylesheet={linkElement}>...</tweak-pane> return <tweak-pane stylesheet={styleElement}>...</tweak-pane> return <tweak-pane stylesheet={sheet}>...</tweak-pane><tweak-pane id="paneOne">...</tweak-pane> <tweak-pane id="paneTwo">...</tweak-pane> <script> paneOne.stylesheet = new URL('./path/to/tweakpane-theme-1.css', location.href) paneTwo.stylesheet = new URL('./path/to/tweakpane-theme-2.css', location.href) </script>
- example of this one here: https://github.com/lume/code-mirror-el/?tab=readme-ov-file#usage-example (see
Basically we can totally do it, without blocking style, without blocking JS hacks, etc, but making it be fully compatible with web dev's favorite frameworks, allowing them to control tweakpane easily with reactive-declarative paradigms.
A popular example in the wild is Shoelace, a component library written as Custom Elements (very recently raised $700k on kickstarter!). But Shoelace is not as specialized as Tweak Pane for making parameter-heavy panels.