ghost
Sign In

Developing WebUI addons: Being a good citizen

Developing WebUI addons: Being a good citizen

I've recently started getting into stable diffusion more seriously, learning about the ins and outs, and both developing and experimenting with a lot of addons for the A1111 WebUI.

In doing so I have noticed a number of bad practices across the eco-system, as well as a lack of guidance on developing for this environment. That's why I'm posting this quick article to establish a baseline of behaviour and hopefully encourage some good add-on design.

To some of you this advice will seem beyond obvious, but when you're a new developer or just want to "move fast and break stuff" it's easy to get caught up in making something work Well Enough™, ship it, and move on. Hopefully this guide will encourage these developers to take just a little bit of extra time and effort to be a good citizen.

1: Keep the public space clean (Polluting the global namespace)

When developing Javascript add-ons it is important to keep functions and variables out of the global namespace. Not only does this keep things nice and organized, it also prevents conflicts.

For example, imagine the following JS code:

function injectUI(){
    const button = createButton("Awesome button", () => alert("You're awesome!");
    gradioApp().querySelector('#quickSettings').appendChild(button);
}

function createButton(label){ ... }

onUiLoaded(injectUI);

Any future devs who also want to inject their UI or ship a helper function for creating buttons will now have a conflict with your globally-scoped functions. In addition, it's just annoying having to browse through "random" functions like createButton when exploring what functionality is available in A1111, and not knowing where this function came from or how it works exactly.

I've seen some extensions prefix their function names, so instead of just injectUI() it's called injectAwesomeAddonUI(), which is definitely more considerate, but we can do better!

A common design pattern in JS to encapsulate code is to wrap it in an anonymous function that is then immediately executed. This is a great way to prevent exposing all of your shenanigans to the outside work. Here's what that looks like:

(function(){
    function injectUI(){
        const button = createButton("Awesome button", () => alert("You're awesome!");
        gradioApp().querySelector('#quickSettings').appendChild(button);
    }

    function createButton(label){ ... }

    onUiLoaded(injectUI);
)();

That was easy! And all it took was two sets of parentheses and a function wrapper. Sweet! But... what if you want to expose some of your code to the outside? Perhaps some of your addon's functionality would work great through code, or you want to expose an API so other developers that automate some of your addon's tasks.

In that case, you can still encapsulate your code like above, but attach all public members to a window object. You then pass this object when calling the anonymous function. I recommend naming this object by your full addon name to minimize the chances of a naming conflict.

(function(am){
    // Regular functions and variables are still just locally scoped
    fileExtensions = ['png', 'jpg', 'webp'];

    function injectUI(){
        const button = createButton("Awesome button", () => alert("You're awesome!");
        gradioApp().querySelector('#quickSettings').appendChild(button);
    }

    function createButton(label){ ... }

    // But we can attach them to the am object as well
    am.version = '1.0';

    am.doAwesomeThing = function(){
        alert("You're awesome!");
    }

    onUiLoaded(injectUI);
)(window.awesomeAPI = window.awesomeAPI || {});

This way, a user can just open up their console and type awesomeAPI.version for example.

The way we're passing in the object might seem a bit esoteric, but all it does is just ensure that awesomeAPI is either what was there already, or a new, empty object.

2: Introduce yourself to strangers (give your elements clear names)

Honestly, I wish A1111 itself took this advice to heart. It's common for addons to add elements to the ui, from buttons to previews, to entire control panels.

It is important to remember however that other addons may interact with your content. A first-hand example of this is when I developed my state manager addon. Unfortunately, since most UI controls lack a dedicated name or id, I've had to rely on an element's associated label (which may be a sibling, or several levels removed in either direction) and the text it contains... Which is even more troubling when you consider some people may be using a different translation, or that labels can change over time.

Some good conventions can include a simple id like #awesomeUI-parameter1, or even a data attribute like <div data-awesome-control="param1">

3: Use your inside voice (scope your CSS)

When styling your UI elements, it's easy to accidentally target an unrelated element instead. There are two main ways to mitigate this.

First, be specific with your class names. Rather than just calling your panel .sidebar, prepend the name of your addon instead, making it .awesomeui-sidebar e.g.

Second, limit your CSS selectors to only target elements you know are yours. So instead of just setting the background-color on button, set it on .awesomeui-sidebar button instead.

4: You can build on your land, but don't divert the river (try to limit your impact to just your stuff)

Ok, these analogies are getting a bit trite. But seriously, sometimes you face a problem and it's easier to just target :root or perform some crazy dark ritual on the existing data/ layout. And sometimes there really is no way around it, but in general, try and avoid this at all costs!

The last thing you want is multiple addons fighting for control over a panel, size, or how it impacts image generation. There are already a few addons that have disclaimers that they will not work if "addon X" is also installed, and that's a problem.


That's all I have for now. I might amend this later, but these issues stood out to me soon after I started developing.

If there's any interest in more development articles or guides, please do let me know! I'm looking to get back into making tutorials again, and if there's an interest here I'd be happy to include this in my list of topics to cover.

Happy dev'ing everyone!

0

Comments