Getting started
The two modes: attributes and createApp
Voodoo.js has two ways of taking over a page, and both are valid. The first binds attributes to the HTML that is already there. The second describes the application in JavaScript and mounts it on a root, in the same shape as Vue. You choose per page, and you can use both on the same one.
Attribute mode, over the HTML you already have
<div v-data="{ n: 0 }">
<button @click="n++">Clicks: { n }</button>
</div>
This is the default mode: you point the script tag, you write attributes, and the library walks the document. It suits server-rendered pages well, where the HTML arrives finished and only the behaviour is missing.
Application mode, with createApp
<div id="counter"></div>
<script>
V.createApp({
data: () => ({ n: 0 }),
computed: { double() { return this.n * 2 } },
methods: { add() { this.n++ } },
template: `
<button @click="add()">Clicks: { n }</button>
<p>Double: { double }</p>
`
}).mount('#counter');
</script>
Here the whole application lives in JavaScript: state, computed values, methods, watchers, the lifecycle and the template HTML. The container starts empty and receives the result. Anyone coming from Vue will recognize all of it, names included.
The options
| Option | What it is |
|---|---|
data or state | A function returning the initial state |
computed | Derived values, cached |
methods | Functions, with this on the instance |
watch | Reacts to a key changing |
template | The root HTML. Accepts every directive |
components | Components visible only inside this application |
provide | Values handed to the whole tree |
inject | Values fetched from a provide above |
style | CSS injected once |
beforeMount, mounted, updated, beforeUnmount, unmounted | Lifecycle |
The app instance
| Member | What it does |
|---|---|
app.mount(target) | Mounts on a selector or element. Returns the root instance |
app.unmount() | Unmounts and gives the container back its original HTML |
app.component(name, def) | Registers a component for this application |
app.directive(name, def) | Registers a directive |
app.use(plugin, options) | Installs a plugin |
app.provide(key, value) | Hands a value to the tree |
app.config.globalProperties | Values exposed inside expressions |
app.instance, app.container, app.isMounted | Mount state |
app.whenMounted() | A promise resolved with the instance once it mounts |
Local components, provide and inject
<div id="panel"></div>
<script>
V.createApp({
provide: { user: 'Ann' },
components: {
'panel-card': {
props: ['title'],
template: '<article><b>{ title }</b> <slot></slot></article>'
},
'panel-footer': {
inject: ['user'],
template: '<small>Signed in as { user }</small>'
}
},
template: `
<panel-card title="Revenue">$128,400</panel-card>
<panel-footer></panel-footer>
`
}).mount('#panel');
</script>
Components declared in components leave the registry when the application is
unmounted, so two applications on the same page can each have a card of their own
without fighting over the name. provide and inject work as they do in
Vue: the value goes down the tree, and the component asks for it by name, with
default for when nobody provided it.
The scheduler of its own
This is the difference that pays off most day to day. Voodoo does not use
DOMContentLoaded or document.readyState to decide when to start: it has
a loop of its own, which asks whether what it needs already exists and has stopped changing.
In practice, mount accepts a target that is not on the page yet:
<script>
// The container does not even exist yet. The mount waits for it.
const app = V.createApp({ template: '<p>mounted when it could</p>' });
app.mount('#screen-2');
// ...another script, another moment, and the application mounts by itself.
</script>
| Situation | Vue and React | Voodoo.js |
|---|---|---|
Script in the <head>, without defer | Fails: the container does not exist | Mounts when the container shows up |
| Container created by another script | Fails, or demands orchestration | Mounts by itself |
| Page loaded a long time ago | Mounts right away | Mounts right away |
The scheduler's three functions are published: V.whenReady(fn), which waits for the
document to have a body and stop growing, V.whenElement(selector, fn), which waits
for an element, and V.ready(fn?), which also returns a promise.
V.whenElement('#chart', (el) => V.renderChart(el, { type: 'line', data: series }));
await V.ready();
Both modes on the same page
<div v-data="{ outside: 'attribute' }">
<p>This piece is { outside } mode.</p>
</div>
<div id="app-part"></div>
<script>
V.createApp({
data: () => ({ inside: 'createApp' }),
template: '<p>And this one is { inside } mode.</p>'
}).mount('#app-part');
</script>
The two share the same reactivity, the same stores and the same directives. A
$store created anywhere shows up on both sides, which makes adoption a choice per
screen rather than for the whole project.
Which one to use
| Situation | Mode |
|---|---|
| A page rendered by Laravel, Rails, Django or PHP | Attributes |
| One interactive stretch inside a content site | Attributes |
| A whole screen mounted on the client, with many components | createApp |
| You come from Vue and want the familiar shape | createApp |
| You need to unmount and remount the interface | createApp, through unmount |
Directives, expressions, components, stores, declarative HTTP, forms, masks, the UI kit,
animations and charts are the same. createApp changes where the HTML comes from
and who owns the root, and nothing else.