Retro Web App Zen: iframes, tag functions & template literals.

A long time I wrote my own WebComponents framework.

This was back in 2013, when WebComponents, Shadow DOM, HTML imports had just landed in Chrome. This technology felt alive with possibility. Soon, thought I, the whole web would adopt WebComponents as “the right way”.

But it never happened.

Others have told this story [ the broken promise of web components ], but in my quest to build a WebComponents framework, I came across some limitations in the standard ( which I feel the new specification of Shadow DOM v1 and slots still does not address ), namely:

  • templating, and the interface of HTML view, data model and controller, is a whole, weird, DSL-like shibboleh any day of the week, but in WebComponents things got worse and weird. We have select and content and now slot . But the fundamental problem, of providing a legible syntax to mingle data and view, likely including conditionals, was never solved by WCs.
  • the focus on declarative, “HTML-as-single-source-of-truth” was a noble and idealistic attempt to revert to the simpler days of yore, where documents really where an appropriate abstraction for multifaceted interaction flows. I embraced this philosophy fully in my own WC framework, adding Angular-like “directives” that started innocently enough specifying what events a components would respond to and what would handle those, and proceeded down a rabbit hole that ended up with an instruction-pointer attribute, and mov attributes, to move the values of selectors around, basically implementing a small, limited, assembler-like virtual machine in the DOM. It had the noble property that at any point in time the “document” was an exact representation of the state of the virtual machine, but the result was a contraption of Rube Goldberg character, an implementation that was a satire of its own principles. I learned my lesson — I had taken things to the natural, if extreme, conclusion, and observed fatal flaws in the approach.

Despite the quirks and limitations that I discovered, there remained a lot to be admired about WebComponents: style encapsulation, modularity, composability. But for the time being, back then in 2013, I was disillusioned with the entire machinery we had been given to build our works of great renown and glory. I was sucked in by the idealistic lure of WebComponents, and when I embarked on a humble project to build a simple tool to help me build apps a little faster, I descended into a rabbit hole of unintended complexity and declarative madness, ending up disillusioned. Never again would I trust a “new fad” or “framework”.

So, of course, I decided, eventually, to build my own.

What I hope to achieve is, by staying as simple as possible, and using technologies in every modern browser, I end up with something that uses the web stack both in the way it was intended and in a way that is a very efficient method to build apps. For me, the investment of time into building a “new way” is nothing, if the payoff is a faster, clearer way to do the thing we spend the bulk of our time doing, building apps.

Principles

The main principles of my design are:

  • No JavaScript on the client, except in rare cases, JavaScript for cosmetic purposes. The advantage of this is that the resulting views can run anywhere, and by inlining styles, even made to load in emails. Write once run anywhere is an attractive payoff.
  • Use native input controls, to avoid quirks and reinventing the wheel just to get “design parity”.
  • Spend most of your time writing HTML pages, styling them with CSS, and deciding the connections ( links, and so on ) between them that create interface flows.

Solutions

The solutions I came up with for this design are:

  • Use iframes to replace components. An iframe can load a particular resource, encapsulate its style, perform a full HTTP roundtrip without navigating its embedding context and, when named, act as a navigation target for any link or form that targets it.
  • Perform templating on the server, in my case, using ES6 template literals, and a tag function to define components, that can accept parameters such as a file to read the markup from, and whether the component output should be wrapped in an iframe explicitly, or if that is the responsibility of the embedding page.

The tag function I’m talking about is called def. This function defines a component function and saves it to a global object. The component function is an async function that when called with a data object, performs any async actions that are requested in the template, and prints and returns the results. It’s simple, short but very effective and powerful, because it means we can put all sorts of functions, including async functions, into our templates, that do things based on the data they get. The template can even make network requests to fulfil data, or these can be fulfilled beforehand and passed in on the data object the component function is invoked with.

Here’s what using def looks like when used in a JavaScript file:

def`widget_name ${spec_object} 
<widget-inline-markup>
${T.path.to.data}
</widget-inline-markup>`;

And here’s what defining a component looks like when we factor the markup out to another file:

def`external_widget ${{file:'widget.html'}}`;

widget.html:

<article class=widget>
<h1>${T.title}</h1>
<p>
<span class=author>${T.author}</span>
${ T.paragraphs.map( p => `<p>${p}</p>` ) }
</article>

And then we can use the component, like so:

app.get('/article', async (req,res,next) => {
res.type('html');
const html = await I.widget({
title: 'How to prepare pizza dough',
author: 'Spanish Pizza Kitchen',
paragraphs: [
"Step 1. Mix flour, water, yeast in 3:2:0.1 ratio",
"Step 2. Let dough rise until 2x as large.",
"Step 3. Kneed until sticky",
"Step 4. Form into balls and roll out to 'dishtowel' thickness",
"Step 5. Apply tomato base, bake until bubbles form.",
"Step 6. Add remaining toppings and cheese. Bake until exposed dough browns darkly."
]
);
res.end(html);
});

In this sense, it “inverts” the pattern of React, where components are “JavaScript” files that contain HTML. Components here are HTML files that contain “JavaScript”. And unlike React, instead of requiring a babel processor for JSX, we use the already built in support for ES6 templates to make our syntax valid JavaScript, and valid HTML.

The T global object is a special Proxy object that essentially functions as a “Future Type”. The Proxy stores the chain of property names, and any arguments (such as functions), and “resolves” them later when the data is available. In that sense, it is simply a convenient short hand for writing this:

${ d => d.paragraphs.map( p => `<p>${p}</p>` ) }

It’s unnecessary to use the T object, but it saves a few bytes when you need something like, <span class=author>${ d => d.author }</span>.

Resolving Data

A template tag function like def receives two arguments when called: an array of template strings, and a spread array of values interspersed between those strings. So, for example, in the following:

def`name ${spec} <markup>${d => d.data}</markup>`

The def function is invoked as if it was called with the following arguments:

def( ['name ', ' <markup>', '</markup>'], spec, d => d.data );

The syntactic sugar of template tag functions and template literals allows us to write beautiful and simple templates that can contain many times of values and have those values “resolve” to their correct string results when it is time to “print” a template with data for display.

The function I use to resolve the values is:

async function resolveVal( data, val ) {
if ( typeOf( val ) == "Promise" ) {
val = await val;
} else if ( typeOf( val ) == "FutureType" ) {
val = await execute( data, val );
}
if ( !! val && typeOf( val.$$future ) == "FutureType" ) {
val = await val( await execute( data, val.$$future ), true );
}
if ( typeOf( val ) == "AsyncFunction" ) {
val = await val( data );
}
if ( typeOf( val ) == "Function" ) {
val = val( data );
}
if ( typeOf( val ) == "Array" ) {
val = val.join(' \n');
}
return val;
}

Where val is one of the values given to the template and data is the data object the component function is called with, as in:

const val = [
d => d.name,
async d => {
const resp = await fetch(d.project_url);
const json = await resp.json();
return json.project_name;
}
];
def`widget ${spec} hi ${val[0]}! how is your ${val[1]}?`;
/* ... */
const data = { name: 'Max', project: 'essay' };
const html = I.widget(data);
/* html: hi Max! how is your essay? */

If this still doesn’t make sense and you’d like to know more, take a look at the code and ask me a question.

Putting it all together

There are many ways to template on the server. I happen to like this one because it makes it easy to write templates that use JavaScript logic and async functions, and it plays nicely with express. But the question that this post was aiming to answer was, “How do you write web apps quickly and simply?”

And I still haven’t said that much about the other object in the title: <iframe>.

Using iframes well

To illustrate this, I’d like to have a simple search component. I’ll take the example from a current project using this framework.

We have two endpoints, one serving the search form, the other serving the embedded search results:

def`searchmaps ${{file:'markup/searchmaps.html'}}`;
def`mapsearchresult ${{file:'markup/mapsearchresult.html'}}`;

And we have the two views for the form, searchform.html:

<form role=search method=POST 
action=mapsearchresult target=mapsearchresult>
<h1 class=sticky id=searchmaps>Map search</h1>
<a target=journey href=journey#journey>Back to journey</a>
<p>
<input maxlength=140 name=query.map mapholder=search size=15>
<button>Search</button>
<p>
<a target=map href="map?map=_new#map">Create new map</a>
<p>
<iframe name=mapsearchresult width=95% height=90 frameborder=0 src="mapsearchresult"k></iframe>
</form>

And the result page, searchresult.html:

<form method=POST action=journey target=journey>
<ul>
${ d => d.maps.map( m => `
<li>
<span class="fixedwidth">
<a target=map href="map?map=${m.name}#map">${m.name}</a>
</span>
<button formtarget=journey
formaction=journey
name=journey.addstep
value="${JSON.stringify(m).replace(/"/g,'&quot;')}"
>Add step</button>` )}
</ul>
</form>

Now actions that occur within the result page can target forms anywhere else in the document tree. And when the search button is clicked it only has to reload the embedded iframe that lists the results and not the search form itself. This principle of nested iframes, using targeted forms, and server-side templating can be extended arbitrarily to create interfaces with arbitrarily complex nested components, all possessing style encapsulation, and all using nothing more than traditional HTML form GET and POST requests, and zero JavaScript on the client.

Conclusion

For me, after the disillusionment of seeing the “things you gave your life to, broken”, in other words, ineffectiveness of current tools either through their bloated excess code, in the case of React, their incorrect focus on declarative, in the case of Angular and WebComponents, or simply not taking full advantage of ES6 features, in the case of hyperapp requiring babel JSX to use cleanly instead of simply using template literals, having a from-the-ground-up standards compliant, back-to-basics HTML and server-side JavaScript framework to build apps is a lifesaver. Cold mountain air. Knowing there are no client-side JavaScript bugs, related to “information flow” or anything like that to resolve. Knowing that you can map out the dependency graph of your components using the tried-and-tested technology of targeted forms and named iframes, knowing that you are returning to a “simpler time” from the origin of the Web. All these things are powerful motivators that make me want to use this approach.

Your situation might be different, and for adopting a technology you didn’t create yourself and wasn’t created by some enormous corporation, you probably can’t afford to take the risk. I understand that. I don’t mind. I’m not trying to get people to adopt it. In fact, if I’m right, and this way of coding web apps really is more effective, I would prefer that no one else use it, because I want to preserve the advantage that my team and I shall have by using it.

But all those questions aside, a big motivation for me is aesthetic sensibility. I find a kind of elegance and beauty in getting back to basics, and doing something extraordinary with something simple: in this case, getting templating, and complex multifaceted interfaces and flows, with the smallest number of parts possible. Using just what we have to get just what we want. To do that, to get away from the world of massive toolchains or corporate created bloatware, I feel is a kind of freedom that allows clarity of thought and purity of spirit, and those things, in doing creative work, are very valuable. Also, the limitations of trying to do more with less, without being hacky nor contrived, was a valuable discipline and constraint.

If you want to know more about the “framework” I am discussing here, check out the code repository.

Like what you read? Give WCR a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.