Extending and debugging
Your own directives and plugins
Everything Voodoo.js does internally is available to you: registering directives, creating magic
variables, adding components, extending the V object and packing all of that into a
plugin.
The short form
A single function, which stands for mounted and updated at once. It is
enough for most cases.
<div>
<script>
V.directive('size', (el, binding) => {
el.style.fontSize = binding.value + 'px';
});
</script>
<div v-data="{ n: 18 }">
<input type="range" min="12" max="48" v-model.number="n">
<p v-size="n">This text obeys a directive you wrote yourself.</p>
</div>
</div>
The form with a lifecycle
V.directive('highlight', {
created(el, binding) {},
beforeMount(el, binding) {},
mounted(el, binding) { el.style.background = binding.value; },
updated(el, binding) { el.style.background = binding.value; },
beforeUnmount(el, binding) {},
unmounted(el, binding) {},
priority: 0,
raw: false
});
Field of binding | What it is |
|---|---|
el | The element |
value | The value, already evaluated |
oldValue | The previous value, in updated |
arg | The argument after the colon |
modifiers | The modifiers after the dots |
expression | The original text |
scope | The active scope |
instance | The nearest component instance, or null |
priority sets the order: higher runs first. raw: true hands you the
expression as text, without evaluating it, which suits directives that receive selectors or
names.
Integrating an outside library
This is where a directive of your own pays off most: wrap an external component once and use it as an attribute anywhere.
V.directive('datepicker', {
mounted(el, binding) {
const picker = new SomeDateLibrary(el, {
format: binding.arg || 'dd/mm/yyyy',
initial: binding.value,
onChange: (value) => {
el.value = value;
el.dispatchEvent(new Event('input', { bubbles: true }));
}
});
el.__picker = picker;
},
updated(el, binding) { el.__picker.setValue(binding.value); },
unmounted(el) { el.__picker.destroy(); }
});
<input v-datepicker:dd-mm-yyyy="form.birthday" v-model="form.birthday">
The dispatchEvent of an input is how an outside integration tells
v-model. Without it, the library writes into the field and the state never finds
out.
Fine control, with defineDirective
For cases that need effects of their own, explicit cleanup or control over the subtree:
import { defineDirective, PRIORITY } from 'voodoojs';
defineDirective(
'countdown',
({ el, evaluate, effect, cleanup }) => {
let timer = null;
effect(() => {
const target = new Date(evaluate());
clearInterval(timer);
timer = setInterval(() => {
const left = Math.max(0, target - Date.now());
el.textContent = Math.floor(left / 1000) + 's';
if (left === 0) clearInterval(timer);
}, 1000);
});
cleanup(() => clearInterval(timer));
},
{ priority: PRIORITY.DEFAULT }
);
| Context | What it is |
|---|---|
el | The element that declared the attribute |
scope | The active scope |
expression | The text of the attribute |
arg, modifiers | Argument and modifiers |
raw | The full attribute name, handy in error messages |
evaluate(expr?) | Evaluates the expression. Never throws |
effect(fn) | Creates a reactive effect already tied to the element's cleanup |
cleanup(fn) | Runs when the element leaves the DOM |
walk(node, scope) | Initializes HTML created by the directive |
The registration options are priority and terminal, which stops the
walker from descending into the children — it is what v-for and v-if
use. The priorities live in V.PRIORITY: IGNORE (100),
FOR (90), IF (80), DATA (70), COMPONENT (65),
REF (60), MODEL (40), BIND (30), DEFAULT (0),
INIT (-10) and TRANSITION (-20).
The registered name does not include the prefix: V.directive('toggle', ...)
answers to v-toggle — and overwrites the internal directive of the same name.
Before picking a name, check V.directives, the Map holding everything
already registered. A prefix of your own, such as v-acme-toggle, avoids the
collision for good.
Magic variables of your own
<div>
<script>
V.magic('$user', () => ({ name: 'Ann', plan: 'Pro' }));
V.magic('$height', (scope) => scope.el.offsetHeight);
</script>
<div v-data="{}">
<p>Hello, { $user.name } — { $user.plan } plan.</p>
<p>The height of this scope is { $height } pixels.</p>
</div>
</div>
The getter receives the active scope, so a magic variable can depend on where it was used. The
dollar sign is added for you when you leave it out. Magic variables are read-only, except for the
ones that expose a set method of their own.
Packing it into a plugin
const myPlugin = {
name: 'analytics',
install(V, options) {
V.track = (event, data) => send(options.key, event, data);
V.directive('track', (el, binding) => {
el.addEventListener('click', () => V.track(binding.value));
});
V.magic('$track', () => V.track);
}
};
V.use(myPlugin, { key: 'abc123' });
The short function form works too:
V.use((V, options) => {
V.config.globals.APP = options;
}, { version: '2.0' });
The same plugin installed twice is ignored the second time. Anything fits inside
install: components, validation rules with V.validator, masks with
V.registerMask, or new methods on V itself.
const brazilPlus = {
install(V) {
V.validator('voter-id', (value) => V.unmask(value).length === 12, 'Invalid voter ID.');
V.registerMask('voterId', '9999 9999 9999');
}
};
V.use(brazilPlus);
What you add to V is not visible inside expressions, which only see the scopes,
the magic variables and a closed list of globals. Publish what you need on
V.config.globals, or create a magic variable with V.magic.
A directive, a component or a magic variable?
| What you want to add | Use |
|---|---|
| A behaviour applied to elements that already exist | A directive |
| A reused structure of HTML | A component |
| A value or function available in any expression | A magic variable, or V.config.globals |
| A set of all three, shipped to other projects | A plugin |