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.