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:
- Add a
@DomModule
decorator to the class, - 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 kebabCase 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>