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.