Quality and migration
Migrating from Vue
The template syntax was designed to be familiar to anyone coming from Vue: directives,
modifiers, v-model and the component model use the same names. The underlying
difference is that there is no compilation, Voodoo interprets the HTML that is already
on the page. That changes what is possible; it is worth reading the limits section before
deciding.
createApp
Mounting has the same shape, and the same root component options:
// Vue
createApp({ data: () => ({ n: 0 }), template: '<b>{{ n }}</b>' }).mount('#app');
// Voodoo.js
V.createApp({ data: () => ({ n: 0 }), template: '<b>{ n }</b>' }).mount('#app');
| Vue | Voodoo.js |
|---|---|
app.component(name, def) | same |
app.directive(name, def) | same |
app.use(plugin, options) | same |
app.provide(key, value) | same |
app.config.globalProperties | same |
app.mount(), app.unmount() | same. unmount restores the container's original HTML |
app.mixin() | does not exist |
the mount target must already exist | it may not exist yet: mounting waits for it |
The guide on the two modes explains when to use
createApp and when to stay with attributes.
Template
| Vue | Voodoo.js | Note |
|---|---|---|
{{ name }} | { name } | The double brace works too |
v-text, v-html, v-show | same | |
v-if, v-else-if, v-else | same | |
v-for, in every form | same | Includes (item, i) and (value, key) |
:key, v-bind:x, :x, v-bind="object" | same | |
.prop | .prop and the . shorthand | |
v-on:click, @click | same, plus the v-click shorthand | |
v-model, v-once, v-cloak, v-pre | same | |
v-memo | does not exist | |
v-slot | slot="name" attribute on the child | |
<Teleport> | v-teleport="#target" | |
<Transition> | v-transition="fade" | |
<KeepAlive> | does not exist | |
<Suspense> | does not exist | Use v-resource with loading |
Modifiers
.prevent, .stop, .self, .once,
.capture, .passive, the key modifiers and .ctrl,
.shift, .alt, .meta are the same. From
v-model, so are .lazy, .number and .trim.
These do not exist: .exact and the mouse button modifiers
(.left, .right, .middle). These exist only here:
.window, .document, .outside, .debounce and
.throttle.
Components
| Vue (Options API) | Voodoo.js |
|---|---|
props: { x: { type: Number, default: 0 } } | props: { x: { type: 'number', default: 0 } } |
props: ['a', 'b'] | same |
data() { return {} } | state(props) or data(props) |
computed, methods, watch, template | same |
emits: [] | does not exist |
this.$emit('x', d) | this.emit('x', d) or this.$emit |
this.$refs, $el, $props, $parent, $nextTick() | same |
this.$watch(source, cb) | this.$watch('expression', cb) |
provide and inject | same, on the component or on the app definition |
<slot>, <slot name="x"> | same |
mounted, updated, beforeUnmount, unmounted | same |
created, beforeCreate | only beforeMount |
<style scoped> | style: '...', with no scope isolation |
// Vue
app.component('user-card', { props: ['name'], template: '<p>{{ name }}</p>' });
// Voodoo.js
V.component('user-card', { props: ['name'], template: '<p>{ name }</p>' });
Both tag spellings work, as in Vue with compilation:
<user-card> and <UserCard>.
Reactivity
reactive, ref, shallowRef, computed (also
in the { get, set } form), watch, watchEffect,
effectScope, nextTick, toRaw, markRaw,
unref and isReactive exist under the same names, on V.
The watch options are the same: immediate, deep and
flush with pre, post and sync.
There is no readonly, shallowReactive, toRefs or
customRef. And there is no automatic ref unwrapping in the template:
inside the expression, use .value.
Global state
| Vue | Voodoo.js |
|---|---|
Pinia: defineStore('cart', { state, getters, actions }) | V.store('cart', { ...state, ...methods }) |
useCart() | V.store('cart') |
in the template: cart.items | $store.cart.items |
provide and inject | same, or a store for shared state |
Pinia getters become methods, because a get does not track changes:
V.store('cart', {
items: [],
total() { return this.items.reduce((s, i) => s + i.price, 0) }
});
Router
| Vue Router | Voodoo.js |
|---|---|
createRouter({ history, routes }) | V.router({ mode, routes }) |
path: '/x', component: X | '/x': { component: 'x' } |
<router-view> | <main v-router-view> |
<router-link to="/x"> | <a v-link href="/x"> |
router.push('/x') | V.router.push('/x') or V.navigate('/x') |
useRoute() | V.route, and $route in the HTML |
beforeEach, afterEach, beforeEnter | same |
| nested routes | do not exist |
| component lazy loading | a route with view, which fetches remote HTML |
Internationalization
createI18n({ locale, messages }) becomes V.i18n({ locale, messages });
$t('key') and pluralization with a vertical bar work the same way;
$n and $d exist, plus $c and $rt; and
locale.value = 'en' becomes V.setLocale('en').
Transitions
<!-- Vue -->
<Transition name="fade"><p v-if="visible">Hello</p></Transition>
<!-- Voodoo.js -->
<p v-if="visible" v-transition="fade">Hello</p>
The classes follow the same pattern: v-fade-enter-from,
v-fade-enter-active, v-fade-enter-to, and the three matching leave
classes.
What does not exist, and why
Voodoo interprets the page's HTML. Without compilation, some things from Vue have no way to exist:
- single-file components (
.vue), which depend on a compiler; - JSX and render functions;
setup(), the Composition API and<script setup>;- server-side rendering with hydration;
- scoped slots, which pass data from the child to the parent's content;
<KeepAlive>,<Suspense>andv-memo;- automatic
refunwrapping in the template; - type checking in the template.
Expressions also accept only a subset of JavaScript: no function,
class, new, delete, await, loops or
destructuring. The expression reference lists what is
allowed.
What you gain
- No build step. No bundler, no configuration and no
node_modulesto put a page online. - A single file, which you download and serve from your own domain.
- A lot built in: declarative HTTP, forms with validation and masks, UI, drag and drop, storage, notifications, and, in the full build, charts, animations, router and languages.
- Works under a strict CSP, without
unsafe-eval. - Lives alongside server-generated HTML, without having to take over the whole page.
When not to switch
If your project has dozens of screens, a large team, component tests and strict typing in the templates, Vue is still the right choice. Voodoo was made for the other side of the scale: server-generated pages, admin panels, prototypes and small teams.
Coexisting
The two can run on the same page, in different areas. Mark the Vue region with
v-ignore so that Voodoo does not touch it:
<div id="app-vue" v-ignore></div>
<div v-data="{ n: 0 }">
<button @click="n++">{ n }</button>
</div>