DOM modules

Very simple utility that allow you to attach a TypeScript class to a DOM element. You can find this module in the @banquette/dom-modules package.

The principle

The idea is to remove the "glue" code between a reusable piece of logic and the DOM. To illustrate, imagine you have the following class:

class ToggleClassModule {
    public constructor(element: HTMLElement, className: string) {
        element.addEventListener('click', () => {
            if (this.element.classList.contains(className)) {
                this.element.classList.remove(className);
            } else {
                this.element.classList.add(className);
            }
        });
    }
}

This simple class takes an HTMLElement and a CSS class name as input and adds/removes it on click on the target element. Of course, there are far better ways to do this, the event is never de-registered, and so on, but this is just an example.

If you want to associate this piece of logic to a <div>, you'll have to do something like this:

<div id="target">Content</div>
new ToggleClassModule(document.getElementById('target'), 'active');

It's unpractical.


If you create a dom module for this class, you can do something like:

<div dom-class-toggle="active">Content</div>

And the result will be the same.

Create a module

To convert the previous class into a dom module, you must do two things:

  1. Add a @DomModule decorator to the class,
  2. Implement DomModuleInterface.

@DomModule

@DomModule is a class decorator that adds the class to the Injector with the appropriate tag. It expects the name of the module as parameter:

@DomModule('my-module')
class MyModule {
    
}

TIP

The name of your model is converted into kebabCaseopen in new window and prefixed with dom-. So in the example above, you'll have to do:

<div dom-my-module></div>

to use your module.


DomModuleInterface

You module must also implement DomModuleInterface, which contains the following:

interface DomModuleInterface {
    /**
     * Sets the HTML element associated with the module.
     */
    setElement(element: HTMLElement): void;

    /**
     * Initialize the module.
     */
    initialize(options?: Pojo): void;

    /**
     * Get the name of the option to use when a scalar value is passed
     * to the html attribute, like: dom-my-module="2".
     */
    getDefaultOptionName(): string|null;
}

TIP

You can also inherit from AbstractDomModule if you prefer. It adds some useful methods like getOption, onReady, and so on.

In practice

Let's make our ToggleClass module a dom module:

import { DomModule, DomModuleInterface } from "@banquette/dom-modules";
import { Pojo } from "@banquette/utils-type";

@DomModule('class-toggle')
class ToggleClassModule implements DomModuleInterface {
    private element!: HTMLElement;

    public initialize(options: Pojo): void {
        const className = options.className as string;
        this.element.addEventListener('click', () => {
            if (this.element.classList.contains(className)) {
                this.element.classList.remove(className);
            } else {
                this.element.classList.add(className);
            }
        });
    }

    public getDefaultOptionName(): string|null {
        return 'className';
    }

    public setElement(element: HTMLElement): void {
        this.element = element;
    }
}

Here is a live demo:

    Scanner

    The scanner is a service that search for dom-* attributes in the DOM and create the appropriate DOM modules for them. That's the glue we talked about at the beginning, but this time it's generalized.

    To DOM modules to work, you need to inject it and call the scan() method:

    import { Injector } from "@banquette/dependency-injection";
    import { DomModulesScannerService } from "@banquette/dom-modules";
    
    Injector.Get(DomModulesScannerService).scan();
    

    You can do it anywhere, anytime. When a module is attached to a DOM element, the attribute is removed. So it can't be set twice.

    Built-in modules

    This package is not the most advanced of Banquette. There are very few built-in modules. It was originally developed to be able to create a Vue app directly from the HTML, that's why there are so few built-in modules.

    You're welcome to propose 😉.


    Vue

    Create a Vue app and mount it on the target element.

    <div dom-vue>
        <!-- Here we can use Vue -->
        <bt-button>Hey!</bt-button>
    </div>
    
    

    Internally, the module uses the VueBuilder from VueTypescript. It can take a group as parameter:

    <!-- Same as above, the default value is already "default". -->
    <div dom-vue="default">
        <bt-button></bt-button>
    </div>
    
    <!-- This app will only contain components that have been -->
    <!-- registered under the "forms" group name. -->
    <div dom-vue="forms">
        
    </div>
    

    NOTE

    If you're not familiar with what a group or the VueBuilder is, please check the VueTypescript documentation.

    WARNING

    For this to work you need to import the module in your app (anywhere):

    import '@banquette/vue-dom-module';
    

    Watcher

    Create a MutationObserver on the target element that do a scan() each time a change is detected. This can be very useful if you want to use dom modules in a already dynamic part of your app (like a Vue app).

    Example:

    <!-- Everything inside that div is watched by a MutationObserver -->
    <div dom-watcher>
        <!-- We create a Vue app -->
        <div dom-vue>
            <!-- dom-my-module will work as expected -->
            <div v-for="item in items" dom-my-module>
                ...
            </div>
        </div>
    </div>