bi-st
v0.0.7
Published
Provide declarative binding of DOM/Custom Elements to history.state
Downloads
7
Readme
bi-st
bi-st binds elements to the window.history.state object. Its purpose is to take the long, drawn-out 99% unambiguously unidirectional markup seen here, and roll it up into a nice compact sushi roll. To help visualize this analogy, see Figure 1 below.
Figure 1
What we get is more compact and more flexible, which is good, but less clearly unidirectional, and a little less declarative, which, in my book, is not so good.
Syntax:
The syntax for binding a UI element is shown below.
<input data-st="draft.key">
<bi-st level="global"><script nomodule>
({
'[data-st]':{
onPopState: ({bist, el}) =>{
el.value = bist.pullFromPath(el.dataset.st, 'Volt ikh mit dir gefloygn vu du vilst');
},
on:{
input: ({event, bist}) => bist.merge(event.target.dataset.st, event.target.value, 'push');
}
}
})
</script></bi-st>
<p-d on="sync-history" to="{histEvent:detail.value}"></p-d>
<purr-sist write></purr-sist>
The demo shown here does the following:
- Allows the user to enter arbitary key value pairs into an object.
- The data the user enters is put into history.state.
- History.state is persisted to a remote datastore.
The total number of lines of markup is only slightly fewer than the example shown here (view source), that does the same thing without the benefit of this component. In particular, the number of lines drops from 129 lines to 110 lines.
However, as a page increases in complexity, with large numbers of UI elements, this component should help significantly reduce the amount of boilerplate. Markup shown below.
What's so great about history.state?
One of the trappings of a component library, like Vueactulitymber, is that each such library ships with a state manager / render binding. But what if you want to combine components together using different libraries? A tempting choice is to say "use Redux, or MobX, or RxJs, or CycleJS" to unify the state management. But this tends to enforce a "library / framework" choice of its own. For small applications / teams this may be fine, but what if you are working with a large application, that spans multiple generations of component libraries, involving loosely coupled teams?
Why not use the platform, and utilize something that will forevermore (?) ship with every browser? That supports time travel, and routing?
It should be noted that AMP components (like amp-bind) seem to ba based on the same principle.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Example 1</title>
</head>
<body>
<div style="display:flex;flex-direction: column">
<!-- Parse the address bar -->
<xtal-state-parse disabled="3" parse="location.href" level="global"
with-url-pattern="id=(?<storeId>[a-z0-9-]*)">
</xtal-state-parse>
<!-- If no id found in address bar, "pass-down (p-d)" message to purr-sist-myjson writer
and xtal-state-update history initializer to create a new record ("session")
in both history and remote store -->
<p-d on="no-match-found" to="purr-sist-myjson[write],xtal-state-update[init]" prop="new" val="target.noMatch" m="2" skip-init></p-d>
<!-- If id found in address bar, pass it to
the persistence reader if history is null -->
<p-d on="match-found" if="[data-history-was-null]" to="purr-sist-myjson[read]" prop="storeId" val="target.value.storeId" m="1" skip-init></p-d>
<!-- If id found in address bar, pass it to the
persistence writer whether or not history is null -->
<p-d on="match-found" to="purr-sist-myjson[write]" prop="storeId"
val="target.value.storeId" m="1" skip-init></p-d>
<!-- Read stored history.state from remote database if
id found in address bar and history starts out null -->
<purr-sist-myjson read disabled></purr-sist-myjson>
<!-- If persisted history.state found, repopulate history.state-->
<p-d on="value-changed" to="history" prop="target.value"></p-d>
<xtal-state-update init rewrite level="global"></xtal-state-update>
<!-- ========================== UI Input Fields ===================================-->
<!-- Manage binding between global history.state and UI elements -->
<bi-st disabled id="bist" level="global" url-search="(?<store>(.*?))" replace-url-value="?id=$<store>"><script nomodule >({
'[data-st]': {
onPopState: (bist, el) => {
el.value = bist.pullFromPath(el.dataset.st, el.value);
},
on: {
input: (event, bist ) => bist.merge(event.target.dataset.st, event.target.value, 'push'),
}
},
'[insert]':{
on:{
click: (event, bist) => bist.merge('submitted', event.target.obj, 'push'),
}
}
})</script></bi-st>
<p-d on="history-changed" to="purr-sist-myjson" prop="newVal" val="target.value" m="1"></p-d>
<!-- Add a new key (or replace existing one) -->
<input type="text" disabled placeholder="key" data-st="draft.key">
<!-- Pass key to aggregator that creates key / value object -->
<p-d on="input" to="aggregator-fn" prop="key" val="target.value" m="1"></p-d>
<!-- Edit (JSON) value -->
<textarea disabled placeholder="value (JSON optional)" data-st="draft.value"></textarea>
<!-- Pass (JSON) value to key / value aggregator -->
<p-d on="input" prop="val" val="target.value"></p-d>
<!-- ============================ End UI Input fields =============================== -->
<!-- Combine key / value fields into one object -->
<aggregator-fn><script nomodule>
fn = ({ key, val }) => {
if (key === undefined || val === undefined) return null;
try {
return { [key]: JSON.parse(val) };
} catch (e) {
return { [key]: val };
}
}
</script></aggregator-fn>
<!-- Pass Aggregated Object to button's "obj" property -->
<p-d on="value-changed" to="button" prop="obj" val="target.value" m="1"></p-d>
<button insert>Insert Key/Value pair</button>
<!-- Persist history.state to remote store-->
<purr-sist-myjson write></purr-sist-myjson>
<!-- Pass store ID up one element so xtal-state-update knows how to update the address bar -->
<p-u on="new-store-id" to="bist" prop="url"></p-u>
<!-- Pass persisted object to JSON viewer -->
<p-d on="value-changed" prop="input"></p-d>
<xtal-json-editor options="{}" height="300px"></xtal-json-editor>
<!-- Reload window to see if changes persist -->
<button onclick="window.location.reload()">Reload Window</button>
<script src="https://cdn.jsdelivr.net/npm/@webcomponents/webcomponentsjs/webcomponents-loader.js"></script>
<script type="module" src="https://cdn.jsdelivr.net/npm/[email protected]/dist/purr-sist-myjson.iife.js"></script>
<script type="module" src="https://cdn.jsdelivr.net/npm/[email protected]/dist/p-all.iife.js"></script>
<script type="module" src="https://cdn.jsdelivr.net/npm/[email protected]/xtal-json-editor.js"></script>
<script type="module" src="https://cdn.jsdelivr.net/npm/[email protected]/dist/aggregator-fn.iife.js"></script>
<script type="module" src="https://cdn.jsdelivr.net/npm/[email protected]/dist/xtal-state-update.iife.js"></script>
<script type="module" src="https://cdn.jsdelivr.net/npm/[email protected]/dist/xtal-state-parse.iife.js"></script>
<script type="module" src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bi-st.iife.js"></script>
</div>
</body>
</html>