localStorageView: Non-invasive local persistence
Lets make it simple to add local storage to a UI control (e.g. @observablehq/inputs)
We exploit back-writability and input binding to avoid having to mess with existing UI control code.
localStorageView(key) creates a read/write view of a safe-local-storage. Because it's a view it can be synchronized to any control we want to provide persistence for.
We avoid having to write any setItem/getItem imperative wiring.
If you want all users to share a networked value, consider shareview.
This works with an view that follows design guidelines for views. A similar notebook for URL query fields is the urlQueryFieldView.
~~~js
import {localStorageView} from '@tomlarkworthy/local-storage-view'
~~~
Change log
- 2021-11-21: Added json option which is true uses JSON.stringify/parse
- 2021-10-09: Added defaultValue option
Demo
So starting with an ordinary control:
//const example_base = Inputs.range()
//const example1 = view(Inputs.range())
const example1Element = Inputs.range()
const example1 = view(example1Element)
const example1ElementVal = Generators.input(example1Element)
example1
example1ElementVal
//const example1 = Generators.input(example_base);
//const example1 = Generators.input(example1Element)
We will use the excellent @mbostock/safe-local-storage which very nicely abstracts over enhanced privacy controls with an in memory fallback.
//import { localStorage } from '@mbostock/safe-local-storage'
import { localStorage } from '/components/safe-local-storage.js';
display(localStorage)
However, we don't want to have to mess around with our original control to add local persistence. Instead we create a writable view of a local storage key
//viewof example1storage = localStorageView("example1");
//const example1storage = view(localStorageView("example1"));
const example1storageElement = localStorageView("example1");
const example1storage = Generators.input(example1storageElement)
example1storage
display(example1storage)
function localStorageView(key, { bindTo, defaultValue = null, json = false } = {}) {
const id = DOM.uid().id;
const readRaw = () => localStorage.getItem(key);
const readValue = () => {
const raw = readRaw();
if (raw == null) return defaultValue;
if (!json) return raw;
try { return JSON.parse(raw); } catch { return defaultValue; }
};
const ui = htl.html`<div class="observablehq--inspect" style="display:flex; gap:.5rem;">
<code>localStorageView(<span class="observablehq--string">"${key}"</span>):</code>
<span id="${id}"></span>
</div>`;
const holder = ui.querySelector(`#${id}`);
holder.textContent = String(readValue());
Object.defineProperty(ui, "value", {
get: readValue,
set: (value) => {
const toStore = json ? JSON.stringify(value) : value;
localStorage.setItem(key, toStore);
holder.textContent = String(readValue());
},
enumerable: true
});
if (bindTo) Inputs.bind(bindTo, ui);
return ui;
}
display(localStorageView)
localStorageView
localStorageView.value
And we bind our original control to the key view
// Note you need to get these the right way round to have the page load work correctly
// CHECK TO DETERMINE THAT THIS BINDING PARAMETER IS CORRECT FOR FRAMEWORK
//Inputs.bind(example1, example1storage)
Inputs.bind(display(Inputs.bind(Inputs.range(), example1Element)), example1storageElement)
Tada! that control will now persist its state across page refreshes.
JSON support
Set json to true to serde.
const jsonViewElement = localStorageView("json", {
json: true
})
jsonView
jsonViewElement
// THIS NEED TO BE VERIFIED AGAINST FRAMEWORK
jsonView.value
jsonViewElement.value
Writing
// THIS NEED TO BE VERIFIED AGAINST FRAMEWORK
{
// jsonView.value = {
jsonViewElement.value = {
rnd: Math.random()
};
jsonViewElement.dispatchEvent(new Event("input", { bubbles: true }));
}
In two cells
It is quite likely we often just want to create the view and bind it to a ui control so just pass the viewof in as the bindTo option in the 2nd argument
const example2Element = Inputs.textarea()
const example2 = Generators.input(example2Element)
example2Element
// CHECK FOR FRAMEWORK COMPATIBILITY
localStorageView("example2", {
bindTo: example2Element
})
In a single cell!
You can even declare a UI control, wrap it with local storage and return in a single cell! (thanks @mbostock!)
const example3Element = view(Inputs.bind(Inputs.textarea(), localStorageView("example3")))
const example3 = Generators.input(example3Element)
example3Element
example3Element
//added DOM control
import {DOM} from "/components/DOM.js";
display(DOM)
//import { footer } from "@endpointservices/endpoint-services-footer"
//footer