Cobblestone

Proxy

The library converts the data object to a JavaScript Proxy. It has to walk down the object for any nested objects and arrays, and proxify them too, one by one, because a Proxy is shallow. The Proxy will intercept any changes to the underlying object and hand them off to our library for rerendering.

Generator

The library looks for any script tags whose Content-Type is text/x-cob. For each, it takes the string content (your template) and converts it into a JavaScript function — specifically a Generator, so that it can build up the HTML with the yield statements, rather than concatenating HTML strings to a variable named something that hopefully no one will will ever use in their templates. It then runs this function, given the starting values of the data. This generates a document fragment. But it is not yet inserted into the page. It's just floating in space.

So this template:

{if next}
    <a href={url} class={classes}>{label}</a>
{/if}

might become:

<a href="//www.example.com/p/12" class="nav next">Page 12</a>

So far, this works like any server-side template engine, and if there was no need to be dynamic, then the story would end here. But if you're going to be reactive, then you need to know which piece of the template is responsible for each piece of the HTML.

Comments

The Generator actually also surrounds the rendered HTML with specially formatted comments, to remember where they came from.

<!--{if next
    // A copy of the block's template, for scanning into the index
    {if next}<a href={url} class={classes}>{label}</a>{/if}
}-->
    <!--{attr href=url class=classes}--><a href="//www.example.com/p/12" class="nav next">
        <!--{text label}-->Page 12<!--{/text}--></a>
<!--{/if}-->

In their wisdom, the originators of JavaScript made it so that HTML comments become nodes in the DOM, like elements, and therefore are traversible.

Another advantage of HTML comments is that the first pass of the template could be rendered server side. This, along with JSON of the data, could be sent to the client to add reactivity to. This “progressive hydration” is yet unimplemented.

Index

We traverse the fragment and save each comment-outlined section to an index. The keys are serialized strings of the path to the governing variable in the data object. The values are arrays of metadata for each section, so that we can rerender them whenever that variable changes.

{
    "data.url": [
        {
            "type": "attr",
            "attrs": [ "href" ],
            "firstNode": {…}
        }
    ],
    "data.classes": [
        {
            "type": "attr",
            "attrs": [ "class" ],
            "firstNode": {…}
        }
    ],
    "data.label": [
        {
            "type": "text",
            "firstNode": {…}
        }
    ],
    "data.next": [
        {
            "type": "if",
            "template": "{if next}<a href={url} class={classes}>{label}</a>{/if}",
            "firstNode": {…}
            "lastNode": {…}
        }
    ]
}

Now that the HTML has been indexed, it is reactive. So it is ready to go into the DOM. We append it just after the template's original script tag.

Whenever you change a property of the data object, the Proxy will generate the stringified version of the key, in the same format as the index. It sends it to the index, which rerenders that section of the page. It's very easy to rerender just that section, because it is outlined with comments.

Handlers

Updates to the view should be done with mere reassignments of variables. The easiest way to do that is with inline handlers, because the library augments them.

Inline functions normally have access just to global variables and two private ones, this and event. But with this library, ones in a template have access to all of the top-level properties of the data object (as unqualified variables: x instead of having to type data.x), as well as any item aliases from surrounding each-loops. So, with {each fruits as f}, f becomes a variable that any function under it can act upon, and the right item in the array will be changed.

The mechanism for all of this can be made clear by seeing an example of what the library does to a function. The function's source code is a string. So the library just adds more code to it.

For example, in this template:

<script>
    var data = {
        fruits: [ 'apple', 'banana', 'orange' ],
        open: true
    };
</script>
<script type="text/x-cob" data-key="data">
    {if open}
        <p>Come in and buy some fruit!</p>
        <ul>
        {each fruits as f}
            <li onclick="
                f = 'You bought an ' + f;
            ">{f}</li>
        {/each}
        </ul>
    {else}
        <p>Sorry, come back tomorrow.</p>
    {/if}
</script>

the onclick handler becomes this:

<li onclick="
    /* * * begin function augmentation * * */

    "use strict";

    (async () => {
        let fruits = data.fruits,
            open = data.open,
            f = fruits[0];

        /* * * end function augmentation * * */


        f = 'You bought an ' + f;


        /* * * begin function augmentation * * */

        fruits[0] = f;
        data.open = open;
        data.fruits = fruits;
    })();

    /* * * end function augmentation * * */
">{f}</li>

So you can see how, first of all, the function becomes strict and async. Secondly, variables are fanned out at the beginning, and collected back up at the end, through a series of extra assignment statements. So any changes you make to one of those variables ultimately finds its way to the back to data object, which triggers its Proxy, which rerenders any dependencies.