In this article we explore the exciting new possibilities of web components for web app developers and how Mozilla's Brick and X-Tag libraries can facilitate their use. First we'll use Brick to rapidly prototype a simple application. Then, we'll build a custom web component using X-Tag.
The web components problem
There is a problem with the Web as a platform for applications: HTML, the language that makes it easy to mark up documents and give them meaning doesn't have enough elements to build applications. There are quite a few new elements in the HTML5 spec, but their support is sketchy across browsers and there are still a lot of widgets missing that other platforms like Flex or iOS give developers out-of-the-box. As a result, developers build their own "widgets" like menu bars, slider controls and calendars using non-semantic HTML (mostly <div>
elements) and make them interactive using JavaScript and theme-able using CSS.
This is a great workaround but the issue is that we add on top of the functionality of browsers instead of extending the way they already function. In other words, a browser needs to display HTML and does a great job doing that at least 60 frames per second. We then add our own widget functionality on top of that and animate and change the display without notifying the browser. We constantly juggle the performance of the browser and our own code on top of it. This leads to laggy interfaces, battery drain and flickering.
The Technology
First we'll provide an overview of the technologies involved.
Brick: Curated Web Components
Mozilla Brick is a set of modular, reusable UI components. The components are designed for adaptive, responsive applications and are a great choice for going mobile first, which is by and large how web-based applications should be developed. This philosophy and its design patterns accomodate a wide range of devices. Brick components aren't just for mobile apps, they are for modern apps.
Brick is kind of like a library, but really you should think of it as a curated collection of web components.
Brick's collection of components are used declaratively in your HTML <like-this>
and can be styled with CSS like regular non-custom elements. Brick components have their own micro-APIs for interacting with them. These components are building blocks. Do you like Lego? Good. You will like Brick.
Brick web components are made using the X-Tag custom elements polyfill.
What is X-Tag?
X-Tag is a library that polyfills several (and soon all) features that enable web components in the browser. In particular, X-Tag is focused on polyfilling the creation of Custom Elements so that you can extend the DOM using your own declarative syntax and element-specific API.
When you are using components from Brick, you are using web components made using the X-Tag library. Brick includes X-Tag core, so if you include Brick and then decide to make your own custom elements you do not need to include X-Tag to do so — all the features of X-Tag are already available for you.
Working demos
Download the demo project files. First we'll use the material in the simple-app-with-bricks folder.
Using Bricks in an app
Here we will build a simple skeleton app using <x-appbar>
, <x-deck>
, and <x-card>
. x-appbar provides a tidy header bar for our application, and x-cards placed as children of an x-deck give us multiple views with transitions.{"innertube.build.label":"youtube_20170628_0_RC4","e":"11202606,9415293,9422596,9428595,9429003,9431012,9431867,9434289,9435797,9444108,9444635,9446054,9446364,9449243,9453897,9455457,9456213,9456940,9457141,9458050,9463594,9464088,9466793,9466795,9466797,9467217,9468797,9468799,9468805,9469072,9469176,9471972,9477080,9477614,9479694,9480475,9480495,9481947","timestamp":"2017-06-29T16:19:43.949Z","cos":"Android","bat":"0.420:0","conn":6,"innertube.build.timestamp":"1498694843","innertube.build.changelist":"160466596","cpn":"PNmdU3kPMkXtqv0P","df":"0/503","csdk":"23","innertube.build.experiments.source_version":"160417092","glrenderingmode":"RECTANGULAR_2D","videoid":"rgbytuCzEEc","cbrand":"samsung","logged_in":"1","cplatform":"mobile","cmodel":"SM-A520F","cver":"12.23.60","cosver":"6.0.1","bh":124141,"fmt":"133","c":"android","cbrver":"12.23.60","afmt":"139","bwe":113312,"innertube.build.variants.checksum":"b74583be0047d4a546940c7e5ab09d37","cbr":"com.google.android.youtube"}
First, we start with a barebones HTML document and then include Brick's CSS and JS, along with our own application-specific code (app.css
and app.js
respectively in the examples that follow).
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width"> <link rel="stylesheet" type="text/css" href="css/brick.min.css"> <link rel="stylesheet" type="text/css" href="css/app.css"> <title>Simple - Brick Demo</title> </head> <body> <!--- Some Brick components will go here --> <script type="text/javascript" src="js/brick.min.js"></script> <script type="text/javascript" src="js/app.js"></script> </body> </html>
Now we'll add some Brick elements:
<x-appbar id="bar"> <header>Simple Brick App</header> <button id="view-prev">Previous View</button> <button id="view-next">Next View</button> </x-appbar>
Finally, below the x-appbar
we'll create an x-deck
with some x-card
s as children. You can give the x-card
s any content you like.
<!-- Place your x-deck directly after your x-appbar --> <x-deck id="views"> <x-card> <h1>View 1</h1> <p>Hello, world!</p> </x-card> <x-card> <h1>Pick a Date</h1> <p><x-datepicker>s are a polyfill for <input type="date"></p> <x-datepicker></x-datepicker> <p>Just here to show you another tag in action!</p> </x-card> <x-card> <h1>A Random Cat To Spice Things Up</h1> <!-- Fetches from the Lorem Pixel placeholder content service --> <img src="http://lorempixel.com/300/300/cats"> </x-card> </x-deck>
We have already almost completed the structure for a simple application. All we need now is a little bit of CSS and JavaScript to tie it together. First, some simple JavaScript:
document.addEventListener('DOMComponentsLoaded', function() { // Run any code here that depends on Brick components being loaded first // Very similar to jQuery's document.ready() // Grab the x-deck and the two buttons found in our x-appbar and assign to local vars var deck = document.getElementById("views"), nextButton = document.getElementById("view-next"), prevButton = document.getElementById("view-prev"); // Add event listeners so that when we click the buttons, our views transition between one another prevButton.addEventListener("click", function(){ deck.previousCard(); }); nextButton.addEventListener("click", function(){ deck.nextCard(); }); });
Some simple CSS to style our fledgling application:
html, body { margin: 0; padding: 0; font-family: sans-serif; height: 100%; } h1 { font-size: 100%; } x-deck > x-card { background: #eee; padding: 0.6em }
Ka-bam! With a little bit of declarative markup and a few tweaks, we have a skeleton that anyone can use to make a multi-view app within a single HTML document. If you check out the markup in the developer tools, you'll see the Brick custom elements living happily alongside vanilla HTML elements - you can use the developer tools to inspect and manipulate them in the same way as regular old HTML.
Now let's learn how to make our own custom element using X-Tag.
Your own bricks: Creating custom elements using X-Tag
Let's say we have a mobile application in which the user takes an action that results in a blocking task. Maybe the application is waiting for an external service. The program's next instruction to complete the user-initiated task depends on the data from the server, so unfortunately we have to wait. For the sake of our purposes here, let's pretend we can't modify our program too much and assume an entrenched architecture — maybe we can't do much else other than communicate to the user until we find a way to deal with the blocking better. We have to do the best with what we have.
We will create a custom modal spinner that will tell the user they need to wait for a little while. It's important to give your users feedback on what's happening in your app when they don't get to complete their task in a timely manner: a frustrated or confused user might give up on using your app.
If you are following along with the example, you will want to switch to the x-status-hud folder inside of the demo materials now.
Registering Your Custom Element
X-Tag relies on several different events to detect and upgrade elements to custom elements. X-Tag will work whether the element was present in the original source document, added by setting the innerHTML
property, or created dynamically via document.createElement
. You should take a look at the Helpers section of the X-Tag documentation as it covers various functions that will allow you to work with your custom elements just like vanilla ones.
The first thing that we need to do is register our custom element with X-Tag so that X-Tag knows what to do if and when it encounters our custom element. We do that by calling xtag.register
:
xtag.register('x-status-hud', { // We will give our tag custom behavior here for our status indicating spinner });
Important: All custom element names must contain a hyphen. Why is this? The idea here is that there are no standard HTML elements with a hyphen in them, so by keeping to this rule we can ensure that we don't trample existing namespaces and cause collisions. You do not have to prefix with x-
: this is just a convention used for components created with X-Tag in the Brick ecosystem. Once upon a time in the early days of the W3C specification for custom elements, it was speculated that all custom elements would have an x-
prefix; this restriction was relaxed in later versions of the specification, meaning that <bacon-eggs>
and <adorable-kitten>
are both perfectly valid names. Choose a name that describes what your element is or does.
If we wanted to, we could choose to set what HTML element is being used as our base element before upgrading. We can also set a specific prototype for our element if we want to involve functionality from a different element. You can declare these as follows:
xtag.register('x-superinput', { extends: 'input', prototype: Object.create(HTMLInputElement.prototype) });
The element we are building doesn't require these properties to be set explicitly, but they are worth mentioning because they will be useful to you when you write more advanced components and want a specific level of control over them.
The Element Lifecycle
Custom elements have events that fire at certain times during their lifetime. Events are fired when an element is created, inserted into the DOM, removed from the DOM, and when attributes are set. You can take advantage of none or all of these events.
lifecycle:{ created: function(){ // fired once at the time a component // is initially created or parsed }, inserted: function(){ // fired each time a component // is inserted into the DOM }, removed: function(){ // fired each time an element // is removed from DOM }, attributeChanged: function(){ // fired when attributes are set }
Our element is going to use the created
event. When this event fires, our code will add some child elements.
xtag.register('x-status-hud', { lifecycle: { created: function(){ this.xtag.textEl = document.createElement('strong'); this.xtag.spinnerContainer = document.createElement('div'); this.xtag.spinner = document.createElement('div'); this.xtag.spinnerContainer.className = 'spinner'; this.xtag.spinnerContainer.appendChild(this.xtag.spinner); this.appendChild(this.xtag.spinnerContainer); this.appendChild(this.xtag.textEl); } } // More configuration of our element will follow here });
Adding Custom Methods
We need to have control over when we show or hide our status HUD. To do that, we need to add some methods to our component. A simple toggle()
may suffice for some use cases, but let's throw in individual hide()
and show()
functions too:
xtag.register('x-status-hud', { lifecycle: { created: function(){ this.xtag.textEl = document.createElement('strong'); this.xtag.spinnerContainer = document.createElement('div'); this.xtag.spinner = document.createElement('div'); this.xtag.spinnerContainer.className = 'spinner'; this.xtag.spinnerContainer.appendChild(this.xtag.spinner); this.appendChild(this.xtag.spinnerContainer); this.appendChild(this.xtag.textEl); } }, methods: { toggle: function(){ this.visible = this.visible ? false : true; }, show: function (){ this.visible = true; }, hide: function (){ this.visible = false; } }
Adding Custom Accessors
Properties on custom elements don't have to map to an attribute. This is by design because some setters could be very complex and not have a sensible attribute equivalent. If you would like an attribute and property to be linked, you have to pass in an empty object literal to the attribute
. In the example below, this has been done for the label
attribute:
xtag.register('x-status-hud', { lifecycle: { created: function(){ this.xtag.textEl = document.createElement('strong'); this.xtag.spinnerContainer = document.createElement('div'); this.xtag.spinner = document.createElement('div'); this.xtag.spinnerContainer.className = 'spinner'; this.xtag.spinnerContainer.appendChild(this.xtag.spinner); this.appendChild(this.xtag.spinnerContainer); this.appendChild(this.xtag.textEl); } }, methods: { toggle: function(){ this.visible = this.visible ? false : true; }, show: function (){ this.visible = true; }, hide: function (){ this.visible = false; } }, accessors: { visible: { attribute: { boolean: true } }, label: { attribute: {}, set: function(text) { this.xtag.textEl.innerHTML = text; } } } }); // End tag declaration
If the difference between attributes and properties is unclear to you, Stack Overflow provides a good answer. Although the question being asked is about something else entirely (jQuery), the top answer has a great explanation that will help you understand the relationship between attributes and properties.
The Finished Component
When we write code that depends on custom elements having been loaded already, we add an event listener that fires when the components have finished loading. This is sort of like jQuery's document.ready
.
<script type="text/javascript"> document.addEventListener('DOMComponentsLoaded', function(){ // Any HUD customizations should be done here. // We just pop up the HUD here to show you it works! var testHUD = document.getElementById("test"); testHUD.label = "Please Wait..."; testHUD.show(); }, false); </script>
And there you have it. We've created a simple modular, reusable widget for our client-side code.
Improvement ideas
Our widget is a good starting point, but is it really finished? There are a number of ways in which we can improve this element:
- Have the element recalculate its size when the
attributeChanged
event is fired and have the component resize to fit the label as it is updated rather than truncate the label with an ellipsis. - Let the developer set an image, such as an animated GIF, in place of the CSS spinner to customize the user experience further.
- Have a progress bar instead of a spinner to give the user some additional information about task progress.
Use your creativity to come up with a small set of practical features and improvements beyond these as an exercise on your own.