cocopon / tweakpane

:control_knobs: Compact GUI for fine-tuning parameters and monitoring value changes
https://tweakpane.github.io/docs/
MIT License
3.68k stars 92 forks source link

support for React, Vue, Svelte, Solid, Angular, etc, by making Custom Elements #604

Open trusktr opened 8 months ago

trusktr commented 8 months ago

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.

cocopon commented 4 months ago

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.

trusktr commented 4 months ago

@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:

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.