Components and state

State that persists, syncs and undoes

Three behaviours that normally cost a fair amount of code: surviving a reload, moving in step with the other open tabs, and having undo. Each one is an attribute.

v-persist

Keeps the scope of a v-data in localStorage and restores it on the next load:

<div v-data="{ theme: 'dark', draft: '' }" v-persist="example-editor">
  <textarea v-model="draft" placeholder="Write something and reload the page"></textarea>
  <p>{ draft.length } characters saved</p>
</div>

The details that matter:

What not to keep there

localStorage is readable by any script of the same origin and survives indefinitely. A text draft, a list filter and a theme preference are good candidates. A token, card data and personal information are not.

v-sync

Keeps the scope in sync with the other open tabs, live, using BroadcastChannel:

<div v-data="{ counter: 0 }" v-sync="panel">
  <button @click="counter++">{ counter }</button>
</div>

Open the page in two tabs and click: both change together. With no value, the channel name is derived from the position of the element. In browsers with no BroadcastChannel, the directive simply does nothing — the page keeps working, it just does not sync.

For syncing between tabs and surviving a reload, combine the two:

<div v-data="{ filter: '' }" v-persist="list" v-sync="list"></div>

v-history, v-undo and v-redo

Undo and redo for the whole scope, not for one field:

<div v-data="{ text: '', colour: '#5b2ee5' }" v-history="50">
  <div class="linha">
    <input v-model="text" placeholder="Write something">
    <input type="color" v-model="colour">
  </div>
  <div class="linha">
    <button v-undo :disabled="!$history.canUndo">Undo</button>
    <button v-redo :disabled="!$history.canRedo">Redo</button>
    <small>{ $history.size } states kept</small>
  </div>
  <div :style="{ background: colour, color: '#fff', padding: '.5rem', borderRadius: '6px' }">
    { text || 'nothing yet' }
  </div>
</div>

The value of v-history is the snapshot limit, defaulting to 50. A snapshot is taken 300 ms after the last change — which is why typing a whole word becomes one step, rather than one letter per step. Writing after undoing throws the future away, as in any editor.

Field of $historyWhat it is
canUndo, canRedoReactive booleans
sizeHow many states are kept
undo(), redo()Moves through the history
clear()Wipes it and starts again from the current state

v-undo and v-redo tie the click to the nearest controller in the tree, so the buttons can sit anywhere inside the scope that has the history.

v-storage, for a single field

For when there is no scope at all and you only want that field not to lose what was written:

<input v-storage="comment-draft" placeholder="Write something and reload">

The value is written on every keystroke into the key voodoo:field:<name> and restored on load.

Storage from JavaScript

V.storage.set('user', { id: 1, name: 'Ann' });   // localStorage with JSON
V.storage.get('user', {});                       // with a default value
V.storage.remove('user');
V.storage.has('user');
V.storage.keys();
V.storage.clear();

V.session.set('step', 2);                        // sessionStorage, same API

V.cookie.set('token', 'abc', { expires: 7, sameSite: 'Lax', secure: true });
V.cookie.get('token');
V.cookie.remove('token');

V.cache.set('products', list, 60000);            // in memory, with an expiry
await V.cache.remember('cep:01001000', 3600000, () => V.http.get('/api/cep/01001000'));

V.url.get('page');                               // query string
V.url.set('page', 2);                            // without reloading
V.url.merge({ sort: 'name', page: 1 });
V.url.all();

Every read and write is safe: in private mode, with the quota full or outside a browser, the calls do not throw — they return the default value and carry on.

Inside the HTML, each one has a matching magic variable: $storage, $session, $cookie, $cache and $url.

<div v-data="{ seen: false }" v-init="seen = $storage.get('tip-seen', false)">
  <p v-show="!seen">
    An important tip that shows up only once.
    <button @click="seen = true; $storage.set('tip-seen', true)">
      Do not show again
    </button>
  </p>
  <button @click="seen = false; $storage.remove('tip-seen')">Show it again</button>
</div>
Reading storage is not reactive

$storage.get(...) reads the value at the moment the expression runs, and nothing tells the screen when it changes later. A v-show="!$storage.get('tip-seen')" would stay visible even after the write. That is why the example keeps the answer in a scope variable, which is reactive, and uses storage only to remember between one visit and the next. When the value has to be reactive and persistent, the shortest road is a store with persist, or the v-persist from the start of this page.