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

OptionWhat it is
data or stateA function returning the initial state
computedDerived values, cached
methodsFunctions, with this on the instance
watchReacts to a key changing
templateThe root HTML. Accepts every directive
componentsComponents visible only inside this application
provideValues handed to the whole tree
injectValues fetched from a provide above
styleCSS injected once
beforeMount, mounted, updated, beforeUnmount, unmountedLifecycle

The app instance

MemberWhat 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.globalPropertiesValues exposed inside expressions
app.instance, app.container, app.isMountedMount 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>
SituationVue and ReactVoodoo.js
Script in the <head>, without deferFails: the container does not existMounts when the container shows up
Container created by another scriptFails, or demands orchestrationMounts by itself
Page loaded a long time agoMounts right awayMounts 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

SituationMode
A page rendered by Laravel, Rails, Django or PHPAttributes
One interactive stretch inside a content siteAttributes
A whole screen mounted on the client, with many componentscreateApp
You come from Vue and want the familiar shapecreateApp
You need to unmount and remount the interfacecreateApp, through unmount
What does not change between them

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.