Custom Element Partials in BackflipHTML

When I first wrote about BackflipHTML last week I mentioned my next challenge was to support Web Components. The first step in that journey is to support using custom elements as partials.

BackflipHTML Partials

In BackflipHTML partials are a unit of a template. It’s one bit of reusable HTML. Here is how you define a regular partial in Backflip:

<section b-name="cat-alert">
    <h1>Cat: {{ cat }}</h1>
    <p b-slot></p>
</section>

And here is how you would use it:

<article>
    <p>We're keeping an eye on the cat.</p>
    <div b-part="cat-alert" b-data:cat="'Mama'">
        The cat is on the kid's bed again.
    </div>
</article>

And it renders this:

<article>
    <p>We're keeping an eye on the cat.</p>
    <div>
        <section>
            <h1>Cat: Mama</h1>
            <p>The cat is on the kid's bed again.</p>
        </section>
    </div>
</article>

You’ll notice a few things here:

  • Both the <div> and the <section> elements appear in the output. We could replace the div with <b-unwrap> which is a special Backflip element that disappears in the output.
  • the b-data:cat=... is not in the output.

Custom Elements

Custom Elements are the starting point of the ensemble of web technologies that make up Web Components. Giving them first class support in Backflip is natural. But as you can see from how things work with regular old partials, custom element partials will need some adjustments.

Ideally you should be able to define a partial such that the custom element is the wrapper element. Just change <section> to <my-cat-alert> and you’re good to go. Backflip already supports this.

<my-cat-alert b-name="cat-alert">
    <h1>Cat: {{ cat }}</h1>
    <p b-slot></p>
</my-cat-alert>

But now b-name is redundant. The custom element’s tag name is its natural name. So with custom element partials we ditch the b-name:

<my-cat-alert>
    <h1>Cat: {{ cat }}</h1>
    <p b-slot></p>
</my-cat-alert>

Nice. To call it, <b-unwrap b-name="my-cat-alert"> seems inelegant. Let’s just write the custom element to call it:

<article>
    <p>We're keeping an eye on the cat.</p>
    <my-cat-alert b-data:cat="'Mama'">
        The cat is on the kid's bed again.
    </my-cat-alert>
</article>

When Backflip sees a custom element at the top level of a template file, it’s a partial definition. When it’s the child of any element, it’s a call site.

Now the tricky bit: at render time, it renders one instance of the custom element <my-cat-alert>:

<article>
    <p>We're keeping an eye on the cat.</p>
    <my-cat-alert>
        <h1>Cat: Mama</h1>
        <p>The cat is on the kid's bed again.</p>
    </my-cat-alert>
</article>

There is one gotcha: there could be attributes like class and others on either the definition or the caller element. These should be merged. For now, that’s an error.

Custom Element Attributes

Web Components can use attributes of the custom element to determine state. In Backflip we pass state to partials using b-data:someVar="someValue" but all that is removed from the generated output. This leaves the web component with no knowledge of what was used to render the current DOM.

To solve this we’ll use b-attr on the definition side to specify the names of these attributes. The definition now looks like this:

<my-cat-alert b-attr:cat>
    <h1>Cat: {{ cat }}</h1>
    <p b-slot></p>
</my-cat-alert>

And we call it like this: (note we’re using :cat, which is equivalent to b-bind:cat which is how we set regular ol' attributes using template variables).

<article>
    <p>We're keeping an eye on the cat.</p>
    <my-cat-alert :cat="'Mama'">
        The cat is on the kid's bed again.
    </my-cat-alert>
</article>

In this case “Mama” is not dynamic, so we can plunk the attribute down like a regular static HTML attribute:

<article>
    <p>We're keeping an eye on the cat.</p>
    <my-cat-alert cat="Mama">
        The cat is on the kid's bed again.
    </my-cat-alert>
</article>

Either way the rendered output is:

<article>
    <p>We're keeping an eye on the cat.</p>
    <my-cat-alert cat="Mama">
        <h1>Cat: Mama</h1>
        <p>The cat is on the kid's bed again.</p>
    </my-cat-alert>
</article>

Neat. Now we can write a custom element class that listens for changes on the attribute:

class MyCatAlert extends HTMLElement {
  static observedAttributes = ["cat"];

  constructor() {
    super();
  }

  attributeChangedCallback(name, oldValue, newValue) {
    // TODO: re-render with new data 😬
  }
}

customElements.define("my-cat-alert", MyCatAlert);

Note: I’m using cat as the attribute name. It should be data-cat of course. I don’t yet know if Backflip should automatically prepend the name with data- or if that’s the author’s responsibility. Another option is for Backflip to interpret data-cat as cat in the template, which is how these attributes are referenced in JS.

BackflipHTML Custom Element Partials

That’s what I have so far for custom element partials in BackflipHTML. The docs are here.

There are a few things I didn’t cover above, like the need to b-export a partial to make it usable from a different template file, and uniqueness rules which I’m not sure about.

And there are some limitations I need to address, like not supporting b-if and b-for at the custom element partial call site.

What’s Next?

That TODO comment in the JS snippet above is a hint: I want to use the templates to generate functions that allow patching or recreating the DOM in the browser. But there are challenges, to say the least.

Another thing is looming: we’re passing very simple values in attributes (strings essentially) but sometimes you need to pass something much more complex, like a big object. How to handle that? Going further, sometimes you just want to pass an ID to a component, and have it load and manage its own data, can this pattern be supported? (Probably yes, but lots of work before I get there.)

What about shadow DOM? I have ideas for this but I’m focused on light DOM for now.

I welcome any constructive thoughts and comments.

Aerospace Engineer turned sofware developer and bootstrappin' entrepreneur.