Fundamentals

State and scope with v-data

Every interactive page has to remember something: whether the menu is open, what was typed, which items are on the list. In Voodoo that lives in a v-data, and the piece of HTML that declares it owns that state.

The basics

The value of v-data is an object. Each key becomes a variable available on that element and on everything inside it.

<div v-data="{ open: false, items: ['bread', 'milk'] }">
  <button @click="open = !open">Toggle menu</button>
  <p v-show="open">{ items.length } items on the list</p>
</div>

There is no declaration in another file, no setState, no registration step. The object in the attribute is the state.

Scope, and what one scope sees of another

Each v-data creates a scope. Scopes nest, and the inner one sees the outer one.

<div v-data="{ theme: 'dark' }">
  <div v-data="{ open: false }">
    <button @click="open = !open">Current theme: { theme }</button>
    <p v-show="open">The button sees theme and open at the same time.</p>
  </div>
</div>

Two rules head off practically every surprise here:

The classic mistake

Writing @click="opne = true" with a typo blows up nothing: the library creates the key opne in the local scope, and the v-show="open" stays put, with no message at all. If a value does not react, start by checking the name. Turning on data-devtools in the script tag turns these cases into a console warning.

What is reactive

Everything inside a v-data becomes an observed object, all the way down. Changing a value, pushing an item onto a list, deleting a key: any of those re-runs only the pieces of the page that read that value.

<div v-data="{ cart: [] }">
  <button @click="cart.push({ name: 'Mug', price: 39 })">Add</button>
  <button @click="cart.pop()">Remove</button>
  <p>{ cart.length } items, total { cart.reduce((s, i) => s + i.price, 0) }</p>
</div>

The paragraph updates because it read cart.length. Another paragraph that reads nothing from cart is never even visited — that is what the documentation means by granular updates.

Every array method works

push, pop, splice, sort, reverse, shift and unshift all trigger the update. Assigning by index (list[2] = 'x') and touching list.length do too. The classic limitation of having to replace the whole array does not exist here.

Where the functions live

The value of v-data is an expression, not a block of JavaScript: it takes objects, lists, numbers and strings, but it does not take methods written inside it. This does not work:

<!-- does not work: the expression parser does not accept a function body -->
<div v-data="{ items: [], total() { return this.items.length } }">

Logic longer than one line lives in one of the three places below, and the HTML calls it by name:

<div>
  <script>
    V.data({
      prices: { Mug: 39, Shirt: 89 },
      total(items) {
        return items.reduce((sum, name) => sum + this.prices[name], 0);
      }
    });
  </script>

  <div v-data="{ items: ['Mug'] }">
    <button @click="items.push('Shirt')">Add a shirt</button>
    <p>{ items.length } items, $ { total(items) }</p>
  </div>
</div>
The function is usedWrite it in
By a whole pageV.data({ ... }), the root scope
Alongside shared stateA store, where this is the store itself
Only by one reusable pieceThe methods of a component

Short expressions still fit in the attribute — @click="items.push('x')", { items.filter(i => i.paid).length }. The limit is the function body, not the length.

Running something the moment the scope is born

v-init runs an expression exactly once, right after the scope is created. It is there to prepare derived values, fire a first fetch or read something off the document.

<div>
  <script>
    V.config.globals.timeNow = () => new Date().toLocaleTimeString();
  </script>

  <div v-data="{ now: '' }" v-init="now = timeNow()">
    <p>This box opened at { now }.</p>
  </div>
</div>

The example goes through V.config.globals because an attribute expression does not accept new: it is a subset of JavaScript, and constructors are left out. Anything that needs new, function or more than one line lives in a script and enters the expression by name.

Where to keep each thing

The data isKeep it in
Used only by that block of HTMLv-data on the element that wraps the block
Used by distant parts of the pageA global store, with $store
Required to survive an F5v-persist, on the persistent state page
Coming from a serverHTTP directives, which already write into the scope
The same thing repeated in several placesA component, with the state inside it

Reading and writing the scope from JavaScript

Sometimes the value has to change from loose code — a callback from another library, a setInterval, an event from the browser itself. V.getScope(element) returns that node's scope, and .data is the reactive object: writing to it updates the screen exactly the way a directive does.

<div id="panel" v-data="{ online: true }">
  <p>{ online ? 'Connected' : 'No connection' }</p>
</div>

<script>
  const panel = V.getScope(document.getElementById('panel')).data;
  addEventListener('offline', () => { panel.online = false; });
  addEventListener('online',  () => { panel.online = true; });
</script>

V.getScope(node) returns the scope declared on that node, or undefined if it declared none. V.findScope(node) walks up the ancestors until it finds one, and falls back to the root scope when it does not. The root scope is V.scope, and V.data({ ... }) writes values into it that the whole page can see.

There is a ready-made path for this

For this specific case, the v-online and v-offline directives already do the work without JavaScript. The example stands as a way of reaching the scope from the outside, which is what matters here.