SJ (Scott Jehl)

An HTML Templating Language That Keeps Its Secrets

photo of scott jehl profile headshot

By Scott Jehl

Most of the websites I work on use some kind of templating language to generate their HTML using data. If you build websites too, you're probably familiar with these languages... for example, Mustache, Handlebars, Nunjucks, Liquid, and even JavaScript template literals (which I've come to appreciate a lot as I've been working with the elegant Enhance.dev framework). Technically, any server-side language can be used to output dynamic HTML in this way too, but the template syntaxes that can run on a server and in the browser are particularly handy for sites that use dynamic data in both of those environments. So those are the kind I'm focusing on here.

These languages come in a variety of flavors but they tend to share one similar pattern: their output no longer contains the details of how it was populated. For example, a template might have a variable in it like "{userName}", and once parsed, that variable will be changed to something like "harryfankhauser". Typically, that's a nice thing, unless you aren't Harry I suppose. It's kind of the point of the tool.

Lately however, I have been thinking that I'd like to see a templating language that doesn't do that, and that's because it's harder to update the same string of HTML (for example, after delivery in the browser) after its templating information has been removed. That's not to say we don't have tools that can do it, but typically, those tools will need access to the original template all over again in order to update that markup in a reliable way. More concretely, that means we need to deliver HTML templates to the browser in addition to the actual HTML we generated the first time.

That overhead feels like a shame, and the obfuscation may even tether us to proprietary implementations we'll one day want to escape.

A Template that Keeps Its Secrets

I would like to explore a templating language that keeps its secrets instead, allowing the output of a template to serve as a template all over again for a future update. If we had such a thing, it's possible that we could not only avoid delivering templates in addition to our generated HTML, but we could also smartly annotate our markup to mark only the portions that need to receive later updates, which is often a small portion of an overall webpage, maybe allowing us to save delivery weight. And maybe, if it was generic enough, we could even land on something more like a markup convention than a specific code library, so that the HTML might be served by one language and later updated and enhanced by another. Like a microformat for data binding...

Ideally, this convention will clearly specify how HTML relates to the data that populates it, enabling transparent data binding and overall, a simpler means of applying progressive enhancement! The idea intrigues me enought that I have started to draft a specification of how it might look...

Introducing "PE": A Progressive Enhancement templating language

PE (a working title for now) is a Declarative templating and data binding pattern for HTML, which could (one day) run in Node.js and in the browser. The goal of PE is to offer a simple way to generate usable and meaningful HTML from data that retains its data binding references so it can serve as a template for future updates as well, using simple one-way data binding.

I've created a project on Github called PE where I'm planning and experimenting with this idea. Most of the project is currently in the form of a documentation Readme, which I'll be referencing here.

How It Works: Leaning Hard on Data Attributes

PE uses a templating syntax built on standard HTML data attributes that are meant to stay in the HTML after it is rendered, rather than being removed, allowing for simple declarative progressive enhancement in the browser. These attributes reference relationships between HTML elements and JavaScript variables, arrays, objects, and properties for one-way data binding that can populate and update the element's content and attributes. Some client-side JavaScript can ensure that any updates to data will cause any HTML with relationships to that data to update as well, which could mean changes to its text content or its attributes, for example.

Here's a basic example of pe syntax in play, using data-pe-text.

Server HTML Template::

<h1 data-pe-text="data.page.title"></h1>

Data Source:

const data = { page: { title: "This is the article title" } }

HTML Output (after parsing by pe.js):

<h1 data-pe-text="data.page.title">This is the article title</h1>

Above, a source template containing an h1 element starts with a data-pe-text attribute to communicate a relationship to a property in the data: data.page.title. The data-pe portion of the attribute can be considered a namespace for our instructions. In this example, the -text suffix in the attribute name says that the referenced property should populate the text content for the element.

To tie such a thing together, pe.js, an imaginary JavaScript library that can run on the server in Node and in the browser, would populate the text of the element while leaving the data-pe-text attribute in place, retaining its relationship to the property for later updates, which may replace the text once again.

Setting Attribute Values

In addition to binding an element's text content to JavaScript variables, pe can bind the values of an element's attributes as well. You do this via the data-pe-attr- attribute prefix, which can be combined with any attribute you'd like to control on the element. For example, here's a link with an href attribute bound to a property:

Server HTML:

<a data-pe-attr-href="callToActionURL">Buy my book!</a>

Data Source:

const callToActionURL = "https://mybookwebsite.com"

HTML Output:

<a data-pe-attr-href="callToActionURL" href="https://mybookwebsite.com">Buy my book!</a>

Again, should that callToActionURL update at a later time, the link's href should update to match it.

Reusing Bindings to Arrays and Objects with $

In addition to simple string variables, you can bind elements to objects and arrays too. Like this.

Data Source:

const data = { page: { title: "This is the article title" } }

Server HTML Template::

<h1 data-pe="data.page"></h1>

That object binding alone will not do anything yet, but once an element is bound to an object or array, additional pe attributes on that element or on that element's child elements can reference that array or object with shorthand of $.

The following example is functionally equivalent to the first H1 example above, using shorthand syntax:

Data Source:

const data = { page: { title: "This is the article title" } }

Server HTML Template::

<h1 data-pe="data.page" data-pe-text="$.title"></h1>

HTML Output:

<h1 data-pe="data.page" data-pe-text="$.title">This is the article title</h1>

Here's an example using the array shorthand to reference the second item in an array:

Data Source:

const greetings = ["hi", "hello"]
<h1 data-pe="greetings" data-pe-text="$[1]"></h1>

HTML Output:

<h1 data-pe="greetings" data-pe-text="$[1]">hello</h1>

This shorthand syntax is convenient when specifying many attributes from the same object. You can also use the shorthand for child elements of any depth, and the same context will be used until a child element binds to an object or array itself.

Data Source:

const user = {name: "Scott", id: "12345"}

Server

<h1 data-pe="data.user">Hey there, <span data-pe-text="$.name"></span></h1>

HTML Output:

<h1 data-pe="data.user">Hey there, <span data-pe-text="$.name">Scott</span></h1>

Looping Through Arrays

For looping through arrays to say, generate an HTML list, pe offers a special attribute: data-pe-each.

This attribute is unique because pe will treat the first element that has an data-pe-each attribute as a template for rendering every item in the array as siblings of that first element, replacing its content and relevant bound attributes to reflect each array item's data. When pe is parsing an each element (such as when HTML is initially generated, or when the array changes state later on), if it encounters additional element siblings that also have that attribute, it will remove them from the HTML so that the items match the items in the array.

Unlike ordinary shorthand, in a data-pe-each loop the $ references an item of the array, rather than the parent array.

Here's an example:

Data Source:

const data = {
  page: {
    navigation: [
      {title: "Home", url: "/"},
      {title: "Contact", url: "/contact"}
    ]
  }
}

Server HTML Template:

<ul>
  <li data-pe-each="data.page.navigation">
    <a data-pe-attr-href="$.url" data-pe-text="$.title"></a>
  </li>
</ul>

HTML Output:

<ul>
  <li data-pe-each="data.page.navigation">
    <a data-pe-attr-href="$.url" href="/" data-pe-text="$.title">Home</a>
  </li>
  <li data-pe-each="data.page.navigation">
    <a data-pe-attr-href="$.url" href="/contact" data-pe-text="$.title">Contact</a>
  </li>
</ul>

And once again, if you were to re-run that output, the first list item would be considered the source template for generating the other items in the array.

Quick/Shallow Implementation of PE's One-Way Binding

Just for fun, I've built a simple demo page that performs the most basic of data binding tasks, which you can find here

In the demo, the HTML in the page was generated on the server-side from the same data structure that is also included at the bottom of the page, and a fancy little JavaScript proxy is there to update the markup to reflect basic changes to the data.

As the demo page notes, you can use your console to update a property or two, though it doesn't do a whole lot more than that.

Broad Considerations

As you can see, there's no serious implementation for PE yet, so I'm very much working in abstract here. But even without much code to play with, some considerations and caveats already come to mind.

One thought came from some kind feedback from Jeremy Keith, who had given my readme a look recently and wrote on his blog,

This is an interesting idea from Scott—a templating language that doesn’t just replace variables with values, but keeps the original variable names in there too. Not sure how I feel about using data- attributes for this though; as far as I know, they’re intended to be site-specific, not for cross-site solutions like this.

While I'm unsure whether I agree on the specifics of data attribute usage in userland that Jeremy mentions, I do think the idea of configurable data attribute namespacing could resolve this potential issue and would be great for avoiding potential collisions as well. For example, perhaps your site would set its configuration to use data-cnet or data-bind and the templating library could work within that namespace instead.

Looking Ahead / Forks Appreciated!

While the examples above don't cover all of the scenarios you'd commonly find in a templating language, I hope they convey the idea and benefits of using and preserving data attributes as a sole convention for the template's instructions.

I'm not sure exactly where to take this idea from here. If you're interested and able, I'd love to see somebody fork the project and run with it, or send me a pull request to look at, or just file an issue in the tracker to think about. I'd appreciate any of that!

On my end, I'll likely keep noodling on this idea further as time allows, but I'm mostly hoping the idea influences someone to build something like it.

Here's that link! PE on Github

Thanks for reading! You can find me on Mastodon if you want to discuss further.

By the way... I am currently exploring work opportunities for 2024! If you think I might make a good addition to your team, whether full-time or as an independent contractor, please reach out.