-
Notifications
You must be signed in to change notification settings - Fork 6
Aurelia and Web Components (HTML Templates and Custom elements)
In this series we explore the Web Component APIs: HTML Imports, Custom elements, HTML Templates, and the Shadow DOM, seeing how each of these APIs can be used within the context of your Aurelia applications. In this installment, we'll look at the features Aurelia provides around HTML templates and custom elements.
Regardless of which SPA frameworks you use, one of the fundamental requirements involves taking a snippet of HTML (often called a template), injecting variable values, and then rendering it to the DOM. The method that I used to use for this back in the BackboneJS days was to create a JavaScript template:
<script type="text/template" id="greeting-template">
hello <%= name %>
</script>
This template would then be rendered by implementing a Backbone view, which looked the template up by ID using jQuery, and rendered it using the Underscore.js templating engine. This method (termed Overloading script) was pioneered by John Resig in his Micro Templating Utility back in 2008. This approach is actually great for the most part. Because script
is set to display:none
by default, nothing is rendered when the template is loaded. It doesn't actually get rendered until we choose to do this with JavaScript - the browser doesn't parse this script as content. Further, the template is inert - the browser doesn't attempt to process the text as JavaScript because the type is set to something other than text/javascript
. However, this approach does come with a downside. If you take user input and then use then directly set the .innerHTML
using the rendered template combined with this input it can lead to XSS vulnerabilities. So long as you're aware of these vulnerabilities it's not a problem, but ideally, there would be a standard approach that circumvents it all together. You can find out more about the pros and cons of this template method on the HTML 5 Rocks - Template Tutorial.
HTML templates provide a standard way of creating snippets of markup that are not rendered when the page is loaded but are instead loaded at run time with JavaScript. These work in much the same way as the Overloaded JavaScript approach but do not have the drawback of increasing the likelihood of developers introducing XSS vulnerabilities into your application. In the below example HTML page I show an <info-card>
HTML snippet. An important thing to note here is that image in the snippet isn't rendered when the page is loaded. Instead, we need to explicitly create an element for it and load it into the DOM. Using templates is as simple as declaring a new <template>
element with some content:
<template id="card">
<div class="info-card">
<p>Basic info card.</p>
</div>
</template>
After declaring the template element, we need to query it from the DOM document.querySelector('#info-card')
, clone the node, creating a new copy of the DOM fragment from the template so that it can be included in the current document document.importNode(infoCard.content, true)
, and then append cloned node to the current document bodyElement.appendChild(infoCardInstance)
:
let infoCard = document.querySelector('#info-card');
let bodyElement = document.querySelector('body');
let infoCardInstance = document.importNode(infoCard.content, true);
bodyElement.appendChild(infoCardInstance);
Aurelia views are created using HTML templates. When the Aurelia framework loads it parses the template and creates a view instance which it then initializes with data-binding and so on. You can find out more about the Aurelia view initialization process in this great post by Jeremy Danyow on the Aurelia Hub. Aurelia adds templating features such as repeaters, one-way and two-way data-binding, binding behaviors, input validation and so on primarily through the use of custom HTML attributes. This keeps the view syntax as close to standard HTML as possible. The equivalent info card view template in Aurelia might look something like this:
<template>
<div class="info-card">
<p>${message}</p> <!-- One way string interpolation binding used to render the text content -->
</div>
</template>
The striking thing about the template syntax in the above example is how close it is to the vanilla web component template we saw earlier.
I was first introduced to the idea of custom elements with Angular 1 directives, which provided a way of declaring an HTML snippet (view) and linking it to a backing controller. Since then just about every SPA framework has implemented custom elements in some way shape or form. Custom elements extend the HTMLElement
class and should be prefixed with an X. To refer back to the info-card
template from earlier, if we wanted to implement this as a vanilla custom element we'd first need to create an XInfoCard
class which derived from HTMLElement
. It's also possible to attributes on a custom element using the observedAttributes()
method, and respond to changes by implementing the attributeChangedCallback
in the custom attribute class:
class XInfoCard extends HTMLElement{
// Monitor the 'message' attribute for changes.
static get observedAttributes() {return ['message']; }
// Respond to attribute changes.
attributeChangedCallback(attr, oldValue, newValue) {
if (attr == 'message') {
this.textContent = message;
}
}
}
You could then use the custom element as follows:
<x-info-card message="basic info card content"></x-info-card>
For various reasons (primarily performance), the Aurelia team chose to create a framework specific implementation of custom elements, rather than using the vanilla web components option. Aurelia custom elements are the bedrock of Aurelia's component system. You create them by first implementing a view-model (for example info-card.js
), which is analogous to the XInfoCard
class in the vanilla web components example, and then creating the corresponding HTML view template. Instead of needing to implement custom callbacks to watch for attributes changing, Aurelia handles this by convention, as you'll see in the below example:
info-card.js - the view-model
import {bindable} from 'aurelia-framework'; //import bindable decorator to allow message to be passed down from parent component
export class InfoCard{
@bindable message;
}
You then need to create the corresponding view file (which as we saw earlier is just a standard HTML template):
<template>
<div class="info-card">
<p>${message}</p> <!-- One way string interpolation binding used to render the text content -->
</div>
</template>
You can use the custom element as follows:
info-card.html - the view
<info-card message.bind="myMessage"></info-card>
This passes the message from a parent component down to the info-card component using one-way data-binding.
Whilst Aurelia doesn't leverage vanilla custom elements under the hood, I think the trade-off is worth it, for the performance gains and elegant development model it allows.
This concludes the overview of how you can use Templates and Custom Elements in Aurelia. In the next post, we'll delve into how to use arguably the most important, and my favourite web component API, the Shadow DOM.