Universal Web Components

Jordan Last

Applications built from web components can be easy to understand, grow, and refactor, because they are easy to break up into modular and declarative pieces. They work well in the browser, but I believe there is far greater potential as they begin to spill out of the browser, into the desktop, server, and even embedded applications. Let us explore.

If you would like to stay in touch as the project progresses, join the email list.

Declarative Code

Let's talk about the power of the declarative. Declarative code has powerful advantages, which all boil down to hiding complexity and exposing it simply. With declarative code, we tell the computer what to do, not how to do it. That makes declarative code easier to understand and therefore to create. Anyone can tell someone what to do. Telling someone how to do it can be much harder. What's more, describing programs in chunks of what is not only easy, it takes less code. You don't believe me? Here is the HTML code for an input element, which I would describe as declarative:

<input type="text">

Here is some JavaScript code for the same input element, which I would describe as imperative:

const input = document.createElement('input');
input.type = 'text';
document.body.appendChild(input);

As you can see, the declarative HTML is much shorter and easier to understand (1 line and 19 characters versus 3 lines and 101 characters). Whenever we want an input to appear on the screen, we just have to tell the computer what we want by almost literally writing the word input. Whenever we are trying to understand a program, seeing <input type="text"> is a lot easier to understand than going through the step-by-step process of creating an input and adding it to the DOM as shown in the JavaScript code.

Having short code that is easy to understand can bring powerful benefits. According to Steven Pemberton in his presentation The Power of the Declarative, there was a company that needed to create very demanding user interfaces, traditionally needing 5 years and 30 people to complete. With XForms, a declarative XML format for building web applications, similar in capability to what we have today with HTML and web components, a certain user interface took only 1 year with 10 people. Assuming that story is accurate, then declarative code saved that company a lot of time and money.

There are also more altruistic benefits.

Declarative code opens up the world of programming to more people, and helps those people do more while expending less time and effort.

Think about how much more productive software developers have become over time as newer and better abstractions have been created. It takes much less code, time, and knowledge now to do things that only computer science experts used to be able to do. Imagine if we had to write web apps in Assembly or C. Each time these better levels of abstraction are introduced, many more people are able to join in the development process. I don't have the numbers, but I wouldn't be surprised if orders of magnitude more programmers joined the workforce at each major abstraction jump, from machine code to Assembly, from Assembly to C/C++ and similar languages, and from there to most of our garbage-collected higher-level languages.

History shows that abstraction is important, and good declarative code is well-abstracted code.

HTML

Based on the amount of currently used HTML code in the world (pretty much every website), HTML is one of the most successful declarative languages ever created. It's instructive to think about why it is has been so successful. HTML breaks up GUI complexity into well-defined, semantic, and somewhat composable pieces. Because of tag names that describe themselves simply, and the ability to visually create hierarchy in the source code, HTML can make it easy to visualize the structure and functionality of your application at a glance. It is easy to see parent, child, and sibling relationships between different pieces of GUI. All of this has made HTML relatively easy to work with, and has lowered the barrier to entry for creating web applications.

All kinds of people who would not be considered programmers can and do work with HTML code.

Think about it, there are over 1 billion total websites in the world today, and probably a couple hundred million active websites built with HTML. That is well more than all of the apps on all of the major app stores combined. That is powerful.

Web Components

Web components inherit all of the benefits of HTML, while adding more.

Web components give us the power to augment HTML, allowing us to create the syntax that we need, at the level of abstraction that we deem necessary.

We get to create our own HTML elements, custom elements, and we can assign any behavior we want to those elements. This is powerful. Frameworks like React, Angular, and Ember have for years been providing a similar capability in the form of custom composable components. Web components will bring all of this as a standard to the web platform. When programming with these components, building programs becomes more like playing with Legos. Complex functionality can be composed from smaller declarative pieces, and it is easy to see at a glance the high-level structure and functionality of your application.

Web components work well in the browser, but I believe there is far greater potential if they can begin to spill out of the browser, into the desktop, server, and even embedded applications. Why not?

There is nothing inherent to HTML and custom elements to stop them from being used in non-GUI applications.

In fact, people keep talking about how great web components are for creating UI components and applications. UI stands for user interface, and there are many kinds of user interfaces, graphical user interfaces (GUIs) being just one type. There are also hardware user interfaces (buttons, sensors, motors, etc) and network user interfaces (HTTP/TCP endpoints). Web components can help us build them all.

At the most basic level, you can think of HTML as a way of representing structure and its associated functionality in a declarative fashion. Up until this point in time, HTML has mostly been used to structure visual functionality, with a sprinkling of non-visual. Just look at these HTML elements: <img>, <input>, <textarea>, <canvas>, <h1>, <p>, <header>, <footer>, etc. All of these are visual, and many of the custom elements available now are visual, but not all.

For example, take a look at these: <script>, <audio>, <base>, <head>, <link>, <meta>. These aren't visual, but they are extremely useful. Developers have been creating their own custom elements with non-visual functionality: <iron-ajax>, <app-route>, <firebase-app>, <firebase-auth>, <firebase-document>, <firebase-query>, <redux-store>, <docker-create>, <docker-commit>, <docker-ps>.

We have discussed both visual and non-visual elements, but so far all of the elements we have discussed (besides <redux-store> have one thing in common: they are built with the assumption of a browser-based application.

What if we moved web components out of the browser? What if we built other applications besides web apps?

Express Web Components

Imagine elements like the following: <express-app>, <express-middleware>, and <express-router>? Could we build an Express server application with these? Absolutely. We'll use the Express Web Components project.

The first thing we need is to create an Express application, running on the port of our choice:

<express-app port="5000"></express-app>

There we go. Now let's prepare to serve up an index.html page on the main route. We'll hook up a callback now but define it later:

<express-app port="5000">
    <express-middleware method="get" path="/" callback="[[indexHandler]]"></express-middleware>
</express-app>

Excellent! Now when anyone goes to our main route we'll call the indexHandler function, which we'll define later. Our application is going to do some API calls, so it would be nice to group all of those under the same router:

<express-app port="5000">
    <express-middleware method="get" path="/" callback="[[indexHandler]]"></express-middleware>
    <express-router path="/api">
    </express-router>
</express-app>

Now let's put some more middleware inside of our router. They'll all automatically be served under the /api route:

<express-app port="5000">
    <express-middleware method="get" path="/" callback="[[indexHandler]]"></express-middleware>
    <express-router path="/api">
        <express-middleware method="get" path="/running-since" callback="[[runningSinceHandler]]"></express-middleware>
        <express-middleware method="get" path="/source-code" callback="[[sourceCodeHandler]]"></express-middleware>
        <express-middleware method="get" path="/cat-photo" callback="[[catHandler]]"></express-middleware>
    </express-router>
</express-app>

When we hit /api/running-since, we're going to return the amount of time this server application has been running. When we hit /api/source-code, we're going to return the server's main source code, and when we hit /api/cat-photo, we'll return a nice photo of a cat. Now to hook up the handlers, we'll be using Polymer 2, but any JavaScript library or framework that allows full DOM interaction should work. Also note that the JavaScript could easily be moved to a separate file and included with a <script> tag:

<express-app port="5000">
    <express-middleware method="get" path="/" callback="[[indexHandler]]"></express-middleware>
    <express-router path="/api">
        <express-middleware method="get" path="/running-since" callback="[[runningSinceHandler]]"></express-middleware>
        <express-middleware method="get" path="/source-code" callback="[[sourceCodeHandler]]"></express-middleware>
        <express-middleware method="get" path="/cat-photo" callback="[[catHandler]]"></express-middleware>
    </express-router>
</express-app>

<script>
    const path = require('path');
    const runningSince = new Date();

    class ExampleApp extends Polymer.Element {
        static get is() { return 'example-app'; }
        constructor() { super(); }

        indexHandler(req, res) {
            // send up the client app to interact with the API
            res.sendFile(path.join(__dirname, '/../client/index.html'));
        }

        runningSinceHandler(req, res) {
            res.send(runningSince);
        }

        sourceCodeHandler(req, res) {
            res.sendFile(path.join(__dirname, '/../server/components/app/app.component.html'));
        }

        catHandler(req, res) {
            res.sendFile(path.join(__dirname, '/../server/cat-hunting.jpg'));
        }
    }   
</script>

And there you have it. We just built a server with web components. I've left out some boilerplate, but this is based off of a real example that actually runs, and you can clone it and run it yourself or see it working here.

We've declared the structure of our Express application. We can visualize the hierarchy of routes. Think of what is now available to us! We could package up all of the middleware under our /api router into its own custom element, and then use those routes as a child element elsewhere. We could also put this entire application into its own custom element. It's also easy to manage multiple Express applications running on different ports by just including them together in the same HTML file.

Well that's all fine and good, we’ve moved out of the browser and into the server. Can we move farther? What about smaller computers, and interacting with hardware? Could we potentially build robots with web components? I think we could.

JFive Web Components

What if we had elements like <jfive-motor>, <jfive-led>, and <jfive-button>? We'll use the JFive Web Components project. Let’s build a rudimentary quadcopter!

First, we'll need four motors:

<jfive-motor></jfive-motor>
<jfive-motor></jfive-motor>
<jfive-motor></jfive-motor>
<jfive-motor></jfive-motor>

We'll want to control when the motors turn on and off, so we'll add data binding to the on property through the on attribute:

<jfive-motor on="[[motorOn]]"></jfive-motor>
<jfive-motor on="[[motorOn]]"></jfive-motor>
<jfive-motor on="[[motorOn]]"></jfive-motor>
<jfive-motor on="[[motorOn]]"></jfive-motor>

We'll want to also control the speed, so we'll add data binding to the speed property through the speed attribute:

<jfive-motor on="[[motorOn]]" speed="[[speed]]"></jfive-motor>
<jfive-motor on="[[motorOn]]" speed="[[speed]]"></jfive-motor>
<jfive-motor on="[[motorOn]]" speed="[[speed]]"></jfive-motor>
<jfive-motor on="[[motorOn]]" speed="[[speed]]"></jfive-motor>

Now let's hook them up to the appropriate GPIO pins. We're using a popular 3-pin H-Bridge to control the motors' direction, so we need to hook up three pins per motor:

<jfive-motor on="[[motorOn]]" speed="[[speed]]" pwm-pin="GPIO12" dir-pin="GPIO23" cdir-pin="GPIO24"></jfive-motor>
<jfive-motor on="[[motorOn]]" speed="[[speed]]" pwm-pin="GPIO18" dir-pin="GPIO20" cdir-pin="GPIO21"></jfive-motor>
<jfive-motor on="[[motorOn]]" speed="[[speed]]" pwm-pin="GPIO13" dir-pin="GPIO27" cdir-pin="GPIO22"></jfive-motor>
<jfive-motor on="[[motorOn]]" speed="[[speed]]" pwm-pin="GPIO19" dir-pin="GPIO6" cdir-pin="GPIO5"></jfive-motor>

Remember how web components are modular and composable? We'll put those properties to good use by embedding an Express server with the motors to allow for some simple remote control over a local area network:

<jfive-motor on="[[motorOn]]" speed="[[speed]]" pwm-pin="GPIO12" dir-pin="GPIO23" cdir-pin="GPIO24"></jfive-motor>
<jfive-motor on="[[motorOn]]" speed="[[speed]]" pwm-pin="GPIO18" dir-pin="GPIO20" cdir-pin="GPIO21"></jfive-motor>
<jfive-motor on="[[motorOn]]" speed="[[speed]]" pwm-pin="GPIO13" dir-pin="GPIO27" cdir-pin="GPIO22"></jfive-motor>
<jfive-motor on="[[motorOn]]" speed="[[speed]]" pwm-pin="GPIO19" dir-pin="GPIO6" cdir-pin="GPIO5"></jfive-motor>

<express-app port="5000">
    <express-middleware method="get" path="/" callback="[[indexHandler]]"></express-middleware>
    <express-middleware method="post" path="/turn-on" callback="[[turnOnHandler]]"></express-middleware>
    <express-middleware method="post" path="/turn-off" callback="[[turnOffHandler]]"></express-middleware>
    <express-middleware method="post" path="/speed-up" callback="[[speedUpHandler]]"></express-middleware>
    <express-middleware method="post" path="/slow-down" callback="[[slowDownHandler]]"></express-middleware>
</express-app>

And finally we'll add all of the methods to finish our data binding and various logic:

<jfive-motor on="[[motorOn]]" speed="[[speed]]" pwm-pin="GPIO12" dir-pin="GPIO23" cdir-pin="GPIO24"></jfive-motor>
<jfive-motor on="[[motorOn]]" speed="[[speed]]" pwm-pin="GPIO18" dir-pin="GPIO20" cdir-pin="GPIO21"></jfive-motor>
<jfive-motor on="[[motorOn]]" speed="[[speed]]" pwm-pin="GPIO13" dir-pin="GPIO27" cdir-pin="GPIO22"></jfive-motor>
<jfive-motor on="[[motorOn]]" speed="[[speed]]" pwm-pin="GPIO19" dir-pin="GPIO6" cdir-pin="GPIO5"></jfive-motor>

<express-app port="5000">
    <express-middleware method="get" path="/" callback="[[indexHandler]]"></express-middleware>
    <express-middleware method="post" path="/turn-on" callback="[[turnOnHandler]]"></express-middleware>
    <express-middleware method="post" path="/turn-off" callback="[[turnOffHandler]]"></express-middleware>
    <express-middleware method="post" path="/speed-up" callback="[[speedUpHandler]]"></express-middleware>
    <express-middleware method="post" path="/slow-down" callback="[[slowDownHandler]]"></express-middleware>
</express-app>

<script>
    const path = require('path');

    class ExampleQuadcopter {
        static get is() { return 'example-quadcopter'; }
        constructor() { super(); }

        indexHandler(req, res) {
            // send up the client app to control the motors
            res.sendFile(path.join(__dirname, '/../client/index.html'));
        }

        turnOnHandler(req, res) {
            this.motorOn = true;
            res.end();
        }

        turnOffHandler(req, res) {
            this.copterOn = false;
            res.end();
        }

        speedUpHandler(req, res) {
            this.copterSpeed += 50;
            res.end();
        }

        slowDownHandler(req, res) {
            this.copterSpeed -= 50;
            res.end();
        }
    }
</script>

And we have a rudimentary remote-controlled quadcopter. Check out the video of the project this example is based on:

And here is the more detailed complete project if you want to take a look and try it out for yourself: https://github.com/scramjs/web-copter

Scram.js

Currently, universal web components are possible because of Electron and Scram.js, which work together outside of the traditional browser to render custom elements and run their associated JavaScript. This is possible because Electron combines Chromium and Node.js into a single runtime, allowing us to use Node.js code from our custom elements. Scram.js hides the details necessary to load an HTML file with a web component application into Electron.

Because universal web components rely on Electron as their non-browser platform, the system that they run on must be relatively powerful. Eventually we need web components to work on systems with very little resources. I'm currently exploring ways of doing this. For example, jsdom would allow us to drop the Electron dependency (and therefore the Chromium dependency) by allowing us to render our web components directly in Node.js. Then we would only need our system to support Node.js. IoT.js, JerryScript, and dukluv could all help in that respect. Please contact me if you would like to help web components work on less powerful systems.

Conclusion

I want to reiterate some words that Rob Dodson wrote in his post, The Case for Custom Elements: Part 2:

Custom Elements are such a flexible primitive, once you start to see all of the possibilities they become really compelling! I’m excited to see what the next few years will hold as more teams begin rolling out their own element suites and we move to an era of high quality, interoperable, UI components.

And remember, UI means user interface, and there are many kinds of user interfaces, graphical and non-graphical alike.

I'm really excited to see the future of web components. I believe they will help lower the barrier to entry for building complex applications of all kinds, and that their many benefits will prove them to be one of the best ways of building software applications. Let’s Think Outside the Browser, lower the barrier to entry for building servers and hardware, and change the world.

P.S. Join the email list if you would like to stay in touch as the project progresses.

Jordan Last

2 posts

I'm a budding full-stack web platform developer.