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:
- the key becomes
voodoo:persist:example-editor; - with no value, the key is derived from the page path and the position of the element;
-
only the keys the current state declares are restored, so adding a new field to the
v-datadoes not break what was already saved; - functions, values starting with a dollar sign and circular structures are left out;
- the write is debounced by 120 ms;
- when the element is removed, the pending write is applied and the observer is shut down.
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 $history | What it is |
|---|---|
canUndo, canRedo | Reactive booleans |
size | How 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>
$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.