npm.io
0.0.33 • Published 2 weeks ago

be-persistent

Licence
MIT
Version
0.0.33
Deps
6
Size
87 kB
Vulns
0
Weekly
0

be-persistent ()

Where are my cookies?

How big is this package in your project? NPM version Playwright Tests

be-persistent is a behavior/decorator/enhancement alternative to purr-sist.

Default settings

<input be-persistent>

What this does:

Stores input's value in session storage (key is based on location within the DOM), but can also be specified.

The syntax above is short-hand for:

Explicit Default Settings

<input be-persistent="of value@input via sessionStorage://{autoGenId}.">

The fragment "of value" is assumed if not provided.

Also, the event "input" is assumed if not provided.

The end of the statement above: "sessionStorage://{autoGenId}" is a Uniform Storage Locator, resolved by the fifteenth package. {autoGenId} is expanded at runtime to a location-independent DOM path so each element gets its own stable key.

We can apply multiple statements within the be-persistent attribute, separated by the "period". Each sentence can start with "of" or "Of".

Specifying the event name

The DOM event that triggers a save can be optionally specified using either of the standard namings that are used by other enhancements in this framework.

  1. Attach it to the property with @, the way be-switched does:
<input be-persistent="of value@change via sessionStorage://{autoGenId}.">
  1. ...or trail an on <event> clause, the way do-inc does:
<input be-persistent="of value via sessionStorage://{autoGenId} on change.">

Both forms are equivalent, and on my-custom-event / @my-custom-event (hyphens allowed) work for any event name. The old :: separator is no longer supported.

Emoji alternative

<input 💾="of value@input via sessionStorage://{autoGenId}.">

Programmatic attachment

Attributes are the easy path for server-rendered progressive enhancement, but they're clunky from a framework that already has the values in hand as properties -- stringifying into a DSL just to have it parsed straight back out is wasted work, and awkward to express in JSX/templating syntaxes that don't love setting attributes. The attribute syntax further up this page is parsed into an internal persistenceRules prop that exists only to be transferred into store -- the property hydrate actually reads. No attribute needed: store (and nudge, below) are ordinary properties you can assign directly.

Every pattern below starts by registering the enhancement's config once, via the same helper the tests and demo/Programmatic/ pages use:

// def.js -- ships alongside be-persistent.js in the package
import 'assign-gingerly/object-extension.js';

export async function defBePersistent(ref){
    const {default: emc} = await import('./emc.json', {with: {type: 'json'}});
    return await push(ref, emc);
}

async function push(ref, emc){
    const {BePersistent} = await import('./be-persistent.js');
    const {enhConfig} = emc;
    enhConfig.spawn = BePersistent;
    enhConfig.customData = emc.customData;
    const registry = (ref?.customElementRegistry ?? customElements).enhancementRegistry;
    registry.push(enhConfig);
    return enhConfig;
}

(ref is any element whose customElementRegistry you want to register against -- pass document.body for the page's default, global registry, or a shadow root's host for a scoped one.)

Declarative -- via the enh.set proxy

Reads like setting properties on any other object. Only the first access per enhKey needs to go through .set -- it's what triggers the spawn; later reads/writes can go straight through .enh.bePersistent:

import { defBePersistent } from 'be-persistent/def.js';

await defBePersistent(document.body);
oInput.enh.set.bePersistent.nudge = true;
oInput.enh.bePersistent.store = 'sessionStorage://{autoGenId}';

This works regardless of order -- properties can be set before defBePersistent registers the config too:

import { defBePersistent } from 'be-persistent/def.js';

// Nothing's registered yet -- `.set` creates a placeholder and defers the
// real spawn until the config shows up.
oInput.enh.set.bePersistent.nudge = true;
oInput.enh.bePersistent.store = 'sessionStorage://{autoGenId}';

await defBePersistent(document.body);
Imperative -- via enh.get()

For code that wants a direct reference to the instance rather than working through the .set proxy:

import { defBePersistent } from 'be-persistent/def.js';

const emc = await defBePersistent(document.body);
const persistenceEnhancement = oInput.enh.get(emc);
Object.assign(persistenceEnhancement, {
    nudge: true,
    store: 'sessionStorage://{autoGenId}',
});
store's three shapes

store accepts three equivalent shapes — a bare USL string is shorthand for a single rule with localProp/localEvent at their defaults:

// Shorthand: a bare USL string
persistenceEnhancement.store = 'sessionStorage://{autoGenId}';

// Equivalent single rule
persistenceEnhancement.store = {
    localProp: 'value',  // default
    localEvent: 'input',  // default
    usl: 'sessionStorage://{autoGenId}'
};

// Equivalent one-rule array -- multiple rules (one per statement in the
// attribute form) are just a longer array
persistenceEnhancement.store = [
    {
        localProp: 'value',
        localEvent: 'input',
        usl: 'sessionStorage://{autoGenId}'
    }
];

Reassigning store at any point (not just at spawn time) re-hydrates: prior listeners are torn down and new ones wired up against the new rule(s).

See demo/Programmatic/ for complete, runnable pages, and tests/Programmatic*.spec.mjs for the automated coverage of all three patterns above.

Store to IDB

<input 💾="via indexedDB://myDB/myStore/{autoGenId}.">
<input 💾="via cookie://{autoGenId}.">

Store to location.hash

<input 💾="via locationHash://{autoGenId}.">

Store to a GitHub Gist

fifteenth's gist:// protocol stores a JSON document as one file inside a git-versioned GitHub Gist. be-persistent speaks it for free — Binder hands every USL straight to fifteenth's get / set, which consult the protocol registry for any scheme beyond the built-in browser stores.

gist:// is opt-in, so the page has to switch it on once, before the first save, by calling configureGist (writes need a token with the gist scope):

<script type="module">
    import { configureGist } from 'fifteenth/gist.js';
    configureGist({ getToken: () => localStorage.getItem('ghGistToken') });
</script>
<input 💾="of value via gist://my-prefs/greeting.json on change.">

gist://<alias>[/<file>] — <alias> is a local name (the real gist id is kept in an id-store, the URL hash by default), <file> defaults to data.json. The first save to an unmapped alias POSTs a new secret gist; later saves PATCH. Prefer a real DOM event like change over the default input so a network round-trip isn't fired on every keystroke. See demo/StoreToGist.html for a runnable page with a token panel.

Store to GitHub Pages

fifteenth's githubpages:// protocol stores a file at a real path inside the repo backing a GitHub Pages site, via the GitHub Contents API. be-persistent speaks it for free, the same way it speaks gist:// — Binder hands every USL straight to fifteenth's get / set, which consult the protocol registry for any scheme beyond the built-in browser stores. No be-persistent code change was needed to support it; see Chats/StoreUnsafeHTMLWithGitPages.md for how that was confirmed.

githubpages:// is opt-in, so the page has to switch it on once, before the first save, by calling configureGitHubPages (writes need a token with Contents: Read and write on that one repo):

<script type="module">
    import { configureGitHubPages } from 'fifteenth/githubpages.js';
    configureGitHubPages({ getToken: (addr) => localStorage.getItem('ghPagesToken') });
</script>
<input 💾="of value via githubpages://bahrus/be-persistent-demo/greeting.json on change.">

githubpages://<owner>[.github.io]/[repo:<repo>/]<path> — unlike gist://, there's no alias/id-store: the path is the address, defaulting to the repo <owner>.github.io (a leading repo:<name> segment picks a different one), so persisted fields land at real, nested, folder-organized paths instead of flat files crammed into one gist. Reads default to readVia: 'api' — the only one of fifteenth's three read paths that reliably reflects a write it just made ('raw' and 'pages' both sit behind a CDN cache a write never invalidates — see the fifteenth chat linked above for how that was verified). See demo/StoreToGitHubPages.html for a runnable page with a token + repo panel.

Persist unsafe innerHTML

There are certain, limited circumstances, where we want to throw security to the dogs, and provide a convenient way of creating "virtual web pages embedded in the url". Here's how we do this:

<div 💾="of unsanitizedInnerHTML via locationHash://{autoGenId}"
    onsecuritypolicyviolation="event.anythingGoes = true">
    <span conteneditable></span>
</div>

On refreshing the browser, the inner content's edits are retained.

of unsanitizedInnerHTML is the opt-in for round-tripping raw markup: the element must acknowledge it by handling securitypolicyviolation and setting event.anythingGoes = true, after which the rule behaves as plain innerHTML. It is storage-agnostic — swap locationHash:// for any other USL. With gist:// the markup lives in a secret gist and only the #gistID:… pointer stays in the URL; prefer a real event like focusout over the default input so a network write isn't fired on every keystroke:

<div 💾="of unsanitizedInnerHTML via gist://my-page/markup.html on focusout."
    onsecuritypolicyviolation="event.anythingGoes = true">
    <span contenteditable></span>
</div>

See demo/StoreUnsafeHTMLWithGist.html.

With githubpages:// the same markup lands at a real, browsable path in a repo instead of one file inside an opaque gist:

<div 💾="of unsanitizedInnerHTML via githubpages://bahrus/my-page/markup.html on focusout."
    onsecuritypolicyviolation="event.anythingGoes = true">
    <span contenteditable></span>
</div>

See demo/StoreUnsafeHTMLWithGitPages.html.

Persist safe inner HTML

We make use of trusted types [TODO]

Nudging disabled elements after hydrating

Server-rendered markup will sometimes ship an input as disabled so the visitor can't type into it before its persisted value has been restored. Add the boolean be-persistent-nudge attribute (💾-nudge for the emoji form) and be-persistent will re-enable the element once every rule has finished rehydrating:

<input disabled be-persistent="via locationHash://{autoGenId}." be-persistent-nudge value="hello">

It's strictly opt-in — without the attribute a disabled element stays disabled. Under the hood this calls assign-gingerly's nudge handler, which decrements a disabled "counter" (removing the attribute when it hits zero), so it composes with other enhancements that disable the same element.

Viewing Locally

Any web server that serves static files with server-side includes will do but...

  1. Install git
  2. Fork/clone this repo
  3. Install node.js
  4. Open command window to folder where you cloned this repo
  5. git submodule add https://github.com/bahrus/types.git types

  6. git submodule update --init --recursive

  7. npm install

  8. npm run build

  9. npm run serve

  10. Open http://localhost:8000/demo/ in a modern browser (Chrome 146+ — JSON module imports with type assertion are required)

Running Tests

> npm run test

Keywords