# Directives
Directives are basically, "mini-bits" that are designed to encapsulate code executed close to the DOM.
The library ships with built-in directives like: data-bind
, data-model
or data-if
, but both
plugins and your application add custom directives.
# Getting started (focused)
To create a new directive, add a new file at src/Directives/FocusDirective.ts
and paste in the following content:
import {AbstractDirective} from '@labor-digital/bits';
export class FocusDirective extends AbstractDirective
{
public mounted()
{
this.$el.focus();
}
}
this.$el
The directive instance knows on which DOM element it is currently bound. You can use this.$el to access the reference.
Now, you need to register the directive in your application by adding it to the configuration:
import {BitApp} from '@labor-digital/bits';
import {FocusDirective} from 'src/Directives/FocusDirective.ts';
new BitApp({
/* ... */
directives: {
focus: FocusDirective
}
});
Finally, start using the directive in the HTML markup of your bit:
<b-mount type="advanced/directives">
<input type="text" data-focus placeholder="I'm focused">
</b-mount>
TIP
The key you provide (e.g. "focus") defines how the data attribute looks like. If you use
camelBacked values like myDirective
, you end up with an attribute like: data-my-directive
# Lifecycle
As you have seen in the simple "focus" example above, a directive also has lifecycle methods, similar to a bit. Note, however, that directives are not directly coupled to a bit lifecycle! Directives are recreated every time the "domChange" event is triggered on the bit or its children.
# bind(value: any): Promise<void>
Async lifecycle hook, executed when the bindable is bound/mounted to the DOM node.
The value
is either null, or if the value of the bound bit property. (See "Data binding" for more information).
NOTE This is a Low-Level hook, you MUST call super.bind(value)
!
# mounted(value: any): void
Lifecycle hook, executed ONCE, when the directive was mounted to the DOM. Can be used to register event listeners or perform DOM manipulation.
The value
is either null, or if the value of the bound bit property. (See "Data binding" for more information).
# update(value: any)
Lifecycle hook, executed every time the linked, reactive value got updated. Will ONLY be executed, if a reactive value was bound in the HTML markup
# unmount()
Lifecycle hook, executed ONCE when the directive gets removed from the DOM. Should be used to remove all event listeners to avoid memory leaks.
# Data binding
A directive can be linked to reactive properties of bits. To bind a property, the HTML must look like: data-my-directive="propertyName"
.
The property, called propertyName
MUST exist in the parent bit of the directive in order to work correctly. The data will be bound
reactively, meaning every time the value of propertyName
changes, the update
lifecycle hook will be executed in the directive.
To make it less theoretical, imagine a simple bit:
import {AbstractBit, Data, Hot} from '@labor-digital/bits';
@Hot(module)
export class Directives extends AbstractBit
{
@Data()
protected message: string = 'Hello World';
}
With an equally simple markup:
<b-mount type="advanced/directives">
<input type="text" data-model="message" value="Hello there!">
<button data-alert="message">Show alert</button>
</b-mount>
The magic part in the HTML is data-alert="message"
which A.) binds the "AlertDirective" and B.) defines that "message" is the reactive property to bind to.
This means that all livecycle hooks of "AlertDirective" receive "Hello World" as value (Because it is the default value in the bit).
Now, lets implement the directive:
import {AbstractDirective} from '@labor-digital/bits';
export class AlertDirective extends AbstractDirective
{
protected message!: string;
public mounted(value: any)
{
this.message = value;
this.$el.addEventListener('click', this.onClick.bind(this));
}
public update(value: any)
{
this.message = value;
}
public unmount()
{
this.$el.removeEventListener('click', this.onClick.bind(this));
}
protected onClick(e: MouseEvent)
{
e.preventDefault();
alert(this.message);
}
}
mounted
is called first, receiving the initial value, which we store as "message". It also
registers the click event listener to the element it is bound to. unmount
does the opposite,
pulling down the event listener again.
As you see in the HTML, we also bind message
using data-model
on the input field,
so every time the user changes the value, the update
method in the directive is called,
receiving the new value and storing it again as message.
When the element gets clicked an alert will pop up with the stored message.
Enforcing data binding
By default directives, don't need to be bound to a property in the bit.
To enforce a property you can use the requireValue
field in the directive class.
import {AbstractDirective} from '@labor-digital/bits';
export class AlertDirective extends AbstractDirective
{
// If this directive is bound without a
// matching data binding an error will be thrown
requireValue = true;
// ...
}
# A word on "Bindables"
When you start working with directives, you will probably soon read something about "Bindables". AbstractBindable
is the base class from which the AbstractDirective
inherits. It is even more low-level than a directive and
has just a subset of lifecycle methods. data-bind
, data-model
or data-bind-attr
all are bindables,
and not directives to improve the performance by dropping unused functionality.
In your daily work I would strongly suggest using directives instead of bindables unless you have a good reason and know what you want to achieve.