# Write Plugins

While bits comes with a lot of handy features already built in to its core, you can probably imagine cases where you want to extend the basic functionality with your own. Beginning with version 1.10 you can create plugins to extend the library to your hearts desire.

# Use plugins

There are some plugins you can install through npm and register them in your bits application. There are already some pre-made plugins for everyday problems:

Simply install the plugin through npm and add it to your app configuration, for example:

npm install @labor-digital/bits-translator
import {BitApp} from '@labor-digital/bits';
import TranslatorPlugin from '@labor-digital/bits-translator';

new BitApp({
    /* ... */
    plugins: [
        new TranslatorPlugin()
    ]
});

TIP

A plugin is only active in the application you have registered it for. Should you have multiple bit apps, you can have completely different plugins and/or configurations.

# Create a plugin

To create a new plugin, start with a new typescript file that will contain the main class. In our case we call it DemoPlugin.ts that has the following content:

import {IBitPlugin} from '@labor-digital/bits';

export class DemoPlugin implements IBitPlugin
{

}

That is the basic implementation required to make a plugin, now you have to register it in your app in the plugins section of the app config.

import {BitApp} from '@labor-digital/bits';
import DemoPlugin from './DemoPlugin';

new BitApp({
    /* ... */
    plugins: [
        new DemoPlugin()
    ]
});

So, while we created a plugin, we don't have much benefit yet. So let's start to extend our plugin class with the hook methods. The plugin has three lifecycle hooks to inject itself into the application.

# initialized

Executed right after the plugin instance was initialized. This means, the plugin loader noticed the registered plugin instance for the first time. This is normally done right before the "created" lifecycle hook

TIP

This hook is the best place to register custom services into the service container.

import {BitApp, IBitPlugin} from '@labor-digital/bits';
import {PluginService} from './PluginService';

export class DemoPlugin implements IBitPlugin
{
    public initialized(app: BitApp): void
    {
        // Either as instance that must be available at all times
        app.di.set('pluginService', new PluginService());
        // Or as factory to be created only when required
        app.di.setFactory('pluginService', () => new PluginService());
    }
}

# created

Executed when the bits app reaches the "created" lifecycle hook, before the DOM gets mounted.

# mounted

Executed when the bits app reaches the "mounted" lifecycle hook, after the bits have been mounted to the DOM

# Extending the bits

Your plugin can extend extending bits by injecting methods, or a getters to all bit instances of the app, using the extendBits() method. The class extension is not done through the prototype, which allows for different extensions in different apps.

extendBits() receives the inject() function as property to register extensions with. If you ever worked with nuxt (opens new window) you will probably know what's coming next 😉.

import {IBitPluginExtensionInjector,AbstractBit, IBitPlugin} from '@labor-digital/bits';

export class DemoPlugin implements IBitPlugin
{
    public extendBits(inject: IBitPluginExtensionInjector): void
    {
        // The first parameter defines the name of the method
        // to be added to the bit classes, the second parameter
        // defines the function to be executed when the method is called.
        // As you can see, the methods this always points to the bit it is 
        // executed in.
        
        // We use the plugin service we injected in the "initialized" lifecycle hook
        // example further up on this page.
        inject('changePluginMessage', function (this: AbstractBit, message: string) {
            this.$di.pluginService.message = message;
        });
        
        // Instead of a function, you can also provide an object literal containing
        // additional options as second parameter. With this we tell inject, that it 
        // should handle our callback like a getter.
        inject('pluginService', {
            callback: function (this: AbstractBit) {
                return this.$di.pluginService;
            },
            getter: true
        });
    }
}

TIP

When you use inject, make sure to start your function parameters with (this: AbstractBit). This tells typescript, that the function is actually part of the bit class, and you can access protected properties without issues.

After you registered the extensions, you can now use them in your bit. You can see how it works in the bit we registered from our plugin.

import {AbstractBit, Hot, Listener} from '@labor-digital/bits';

@Hot(module)
export class PluginBit extends AbstractBit
{
    @Listener('click')
    protected onClick(): void
    {
        this.$changePluginMessage('I was clicked!');
    }
    
    public get message(): string
    {
        return this.$pluginService.message;
    }
}

TIP

The $ prefix is a declaration I inherited from vue to prefix everything not part of the current bit class. It is automatically prepended to every extension registered through inject()

Plugin Service

This is the source code of our plugin service, we registered in the "initialized" lifecycle hook example further up on this page. Nothing much to talk about, other than the usage of makeObservable() to create a reactive service property.

import {makeObservable, observable} from 'mobx';

export class PluginService
{
    protected _message: string = 'Hello world! I am a demo plugin :)';
    
    constructor()
    {
        makeObservable(this, {_message: observable} as any);
    }
    
    public set message(message)
    {
        this._message = message;
    }
    
    public get message(): string
    {
        return this._message;
    }
}

# Typescript and Autocompletion

While technically already working, typescript and your autocompletion will likely complain about not knowing our extension being part of the bit class.

TS2339: Property '$changePluginMessage' does not exist on type 'PluginBit'.
TS2339: Property '$pluginService' does not exist on type 'PluginBit'.

To fix this issue we use module augmentation to tell typescript about our extension. At the top of the plugin class we will add the following:

declare module '@labor-digital/bits/dist/Core/AbstractBit'
{
    interface AbstractBit
    {
        $changePluginMessage(message: string): void;
        readonly $pluginService: PluginService;
    }
}

declare module '@labor-digital/bits/dist/Core/Di/DiContainer'
{
    interface DiContainer
    {
        readonly pluginService: PluginService
    }
}

And there you have it, now typescript compiles the code without an issue.

# Providing bits

Your plugin can provide bit classes to the application, which is really helpful to encapsulate reusable code, for sliders, modals and similar elements. The provideBits method in your plugin class allows you to return a bit definition you already know from the app options.

import {BitApp, IBitNs, IBitPlugin} from '@labor-digital/bits';
import {PluginBit} from './PluginBit';

export class DemoPlugin implements IBitPlugin
{
    /* ... */
    
    public provideBits(app: BitApp): IBitNs
    {
        return {
            plugin: {
                demo: PluginBit
            }
        };
    }
    
}

Now you can add this markup to your page and see how the bit provided by the plugin is automatically registered for usage:

<b-mount type="plugin/demo"><span data-bind="message"></span></b-mount>

TIP

Like in the app bit definition you can nest the definition using object literals, the resulting type will be "plugin/demo". I would strongly suggest to use a common "namespace" for all your provided bits, to avoid naming collisions.

PRO-TIP

Plugin bits are registered before those of the app, this allows you to override plugin bits on a per-app basis.

# Providing directives

Similar to providing whole bits, your plugin can bring its own directives and register them in the application.

import {BitApp, IBitNs, IBitPlugin} from '@labor-digital/bits';
import {AlertDirective} from './AlertDirective';

export class DemoPlugin implements IBitPlugin
{
    /* ... */
    
    public provideDirectives(app: BitApp): PlainObject<IDirectiveCtor>
    {
        return {
            alert: AlertDirective
        };
    }
    
}

PRO-TIP

Directives provided by plugins MAY override default directives like data-bind, data-if, however can, themselves be overwritten by the directive configuration on a per-app basis!