← Back


Manage binding between UI elements and history.state




Published on


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.


The syntax for binding a UI element is shown below.

<input data-st="draft.key">

<bi-st level="global"><script nomodule>
        onPopState: ({bist, el}) =>{
            el.value = bist.pullFromPath(, 'Volt ikh mit dir gefloygn vu du vilst');
            input: ({event, bist}) => bist.merge(,, 'push');
<p-d on="sync-history" to="{histEvent:detail.value}"></p-d>
<purr-sist write></purr-sist>

The demo shown here does the following:

  1. Allows the user to enter arbitary key value pairs into an object.
  2. The data the user enters is put into history.state.
  3. 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">

    <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>

    <div style="display:flex;flex-direction: column">

        <!-- Parse the address bar -->
        <xtal-state-parse disabled="3" parse="location.href" level="global" 

        <!-- 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.value);
                on: {
                    input: (event, bist ) => bist.merge(,, 'push'),
                    click: (event, bist) => bist.merge('submitted',, 'push'),
        <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 };
        <!-- 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=""></script>
        <script type="module" src=""></script>
        <script type="module" src=""></script>
        <script type="module" src=""></script>
        <script type="module" src=""></script>
        <script type="module" src=""></script>
        <script type="module" src=""></script>
        <script type="module" src=""></script>


(Loading compatibility data...)

Was this helpful? Need more help?
Leave a comment or a question below. You can also join the chat on Discord or ask questions on StackOverflow.



  • xtal-state#0.0.85
  • xtal-element#0.0.59
  • trans-render#0.0.111
MIT License


Browser Independent