cocopon / tweakpane

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

Having a hard time with plugin development in vanilla js #575

Closed bumbeishvili closed 9 months ago

bumbeishvili commented 9 months ago

Hi,

I wanted to create a simple text editor plugin. I faced some issues and I'll list them here, might be helpful for others who are just starting out, or for you to simplify workflow & improve docs.

First, I just wanted to write simple javascript and not run any backend & typescript project and publish the result to npm. This was the first hassle.

So, I just followed your tutorial - https://cocopon.github.io/tweakpane/plugins/dev/

The second hassle was understanding where createPlugin comes from and what would it do, turns out it just adds a version number. Not sure if this approach is good, since it might invalidate a perfectly working plugin.

Although it was unclear if I needed to register CounterPluginBundle or CounterInputPlugin.

The third hassle came when I registered CounterInputPlugin and there were no errors, but also it did not have any influence.

Then I followed the code and after digging into it, turns out I needed to register CounterPluginBundle.

After registering it, I got an incompatibility error, I think it's because the version is hard-coded in the source code

image

So, I had to manually set the version

Here is a working vanilla js code if it helps anyone

import * as PaneLibrary from 'https://cdn.jsdelivr.net/npm/tweakpane@4.0.0/dist/tweakpane.js';

let params ={count:20};
let Pane = PaneLibrary.Pane;

const pane = new Pane({
        container: document.querySelector('.control-container'),
        title: 'Advanced settings',
        expanded: true
});

class CounterView {
        constructor(doc, config) {
                console.log('counter view constructor')
                // Create view elements
                this.element = doc.createElement('div');
                this.element.classList.add('tp-counter');

                // Apply value changes to the preview element
                const previewElem = doc.createElement('div');
                const value = config.value;
                value.emitter.on('change', () => {
                        previewElem.textContent = String(value.rawValue);
                });
                previewElem.textContent = String(value.rawValue);
                this.element.appendChild(previewElem);

                // Create a button element for user interaction
                const buttonElem = doc.createElement('button');
                buttonElem.textContent = '+';
                this.element.appendChild(buttonElem);
                this.buttonElement = buttonElem;

                console.log({ el: this })
        }
}

class CounterController {
        constructor(doc, config) {
                console.log('counter controller constructor')
                // Models
                this.value = config.value;
                this.viewProps = config.viewProps;

                // Create a view
                this.view = new CounterView(doc, {
                        value: config.value,
                        viewProps: this.viewProps,
                });

                // Handle user interaction
                this.view.buttonElement.addEventListener('click', () => {
                        // Update a model
                        this.value.rawValue += 1;
                });
        }
}

const CounterInputPlugin = {
        core: { major: 2 },
        id: 'counter',
        type: 'input',
        accept: (value, params) => {
                console.log('counter plugin accept')
                if (typeof value !== 'number') {
                        return null;
                }
                if (params.view !== 'counter') {
                        return null;
                }
                return {
                        initialValue: value,
                        params: params,
                };
        },
        binding: {
                reader: () => (value) => Number(value),
                writer: () => (target, value) => {
                        target.write(value);
                },
        },
        controller: (args) => {
                console.log('counter plugin controller')
                return new CounterController(args.document, {
                        value: args.value,
                        viewProps: args.viewProps,
                });
        },
};

const CounterPluginBundle = {
        // Identifier of the plugin bundle
        id: 'counter',
        // Plugins that should be registered
        plugins: [
                CounterInputPlugin,
        ],
        // Additional CSS for this bundle
        css: `
                         .tp-counter {align-items: center; display: flex;}
                         .tp-counter div {color: #00ffd680; flex: 1;}
                         .tp-counter button {background-color: #00ffd6c0; border-radius: 2px; color: black; height: 20px; width: 20px;}
                       `,
};

// Register plugin to the pane
pane.registerPlugin(CounterPluginBundle);

pane.addBinding(params, 'count', {
        view: 'counter',
        // label: null,
});
cocopon commented 9 months ago

Thank you for your detailed feedback.

I just wanted to write simple javascript and not run any backend & typescript project

Indeed, working within the current structure can be challenging if you are not using a package bundler. At least for now, it's primarily designed for TypeScript and relies on the package bundlers for plugin development.

createPlugin come from and what would it do

createPlugin is a utility function. It just adds a version as you mentioned, but it's needed to ensure compatibility with the pane that is used by plugin user. It can be useful if plugin developers follow the expected way (forking @tweakpane/plugin-template and extend it) since it includes the core (@tweakpane/core) version automatically.

Although it was unclear if I needed to register CounterPluginBundle or CounterInputPlugin.

You raise a valid point. While typings can assist you if you uses TypeScript, but anyway registerPlugin requires TpPluginBundle and it seems strange a bit. Their names (registerPlugin, TpPluginBundle, TpPlugin) can be improved.

cocopon commented 9 months ago

P.S. Was there a specific reason you chose not to use @tweakpane/plugin-template and decided to develop a plugin from scratch? (just curious)

bumbeishvili commented 9 months ago

P.S. Was there a specific reason you chose not to use @tweakpane/plugin-template and decided to develop a plugin from scratch? (just curious)

Yes, it seemed excessive from my point of view. Did not want to run another project within my project (or outside it) to make use of a feature of the tweakpane library, first I wanted to do it quickly when failed to do so, I just had an interest in how would I achieve it.

In the end, the plugin is just 3 classes and 1 object, which can be written in vanilla JS easily (as long as the structure matches)

bumbeishvili commented 9 months ago

Not optimal, but still got what I wanted in the end with vanilla js (code editor integrated into pane).

Thank you for this library. It's great

https://github.com/cocopon/tweakpane/assets/6873202/9f57b5e1-098f-477b-bbbd-d2f97f25d74e

I am going to close this issue, I hope it helps someone