Web Components Test Drive

Yonatan Kra
WalkMe Engineering
Published in
9 min readNov 26, 2018

It took me a while to get to it, but I’ve finally put on my big boy pants and learned the long festering Web Components specification. If you are using Angular, React, Vue, or any self respecting front-end framework, you must be familiar with the components concept.

In this tutorial, we will build a web component that can be published to npm. We’ll be using Chrome, as Web Components will not work on many other browsers at this time (thanks @westbrook for pointing out that FF and Safari now have bridged the gap). For any residual limitations, please refer to this article to learn to polyfill away all your Web Component problems!

Background

The Web Components specification includes four technologies:

  1. Custom elements;
  2. Templates;
  3. Shadow DOM; and
  4. ES modules (until recently it was HTML imports, but these are since deprecated.)

We’ll go over them in turn as we build the application. Have patience, and don’t make me come down there…

Building an app with Web Components

One frequently used UI component is a modal window. It’s actually an HTML div element that displays above the rest of the content and contains some user defined content. It is sometimes accompanied by an overlay/spotlight that obscures the rest of the website/app.

API

Here’s a suggested API for this modal window:

  1. Open: opens the modal window with user defined content in the middle of the parent element. It could should be customizable in the following fashion:
    1) Replace the content;
    2) Specify height;
    3) Specify width; and
    4) Show or hide the overlay.
  2. Close: closes the modal window.

The Open method needs a config object:

Infrastructure

I’ve setup a small build tool using webpack, Karma and Jasmine in order to make the development flow easier. Click here to view the infrastructure. Wanna be like me? Follow these steps:

  1. Make sure you have git installed, and enter the following into your command-line interface:
  2. git clone https://github.com/YonatanKra/web-components-ui-elements.git
  3. cd web-components-ui-elements
  4. git checkout infrastructure

There are four things you need to know here:

  1. All files go into the src folder;
  2. You will develop the app using npm run serve
  3. You will build the app using npm run build
  4. You will test the app using npm test

How I’ve set this up is out of the scope of this tutorial. You can read more about webpack in my webpack series. We’ll go over the infrastructure in more detail in the next article.

Now that we have our modal window specifics down and our infrastructure is as solid as graphene, we can start writing some code.

First code

In our src folder, we’ll create a file called ce-modal-window.spec.js. ‘ce’ is our prefix, and it stands for ‘custom element.’ Here’s the content of the file:

After inspecting the specs, here’s what we should do:

  1. We import a class CEModalWindow from ce-modal-window.js
  2. We create a ce-modal-window element and append it to the DOM. That’s our custom element.
  3. In our test, we expect to find a child node containing the overlay and overlay-hidden classes.

The above is easy to implement, so let’s do it now.

In the src folder, we’ll create a file called ce-modal-window.js. Here’s its content:

Studying the above sheds some light on the first of the four structures comprising the Web Components tetrarchy — custom elements.

In the code above, I’ve created a class (CEModalWindow) that extends HTMLElement. This means that from now on this class has all the properties and methods that an HTMLElement has.

In the constructor I’ve used the super method, which allows us to access and call parent functions.

Elements also have lifecycle hooks. One of them is connectedCallback, which fires after the element is connected to the DOM. This is a safe place to add our template, since it is illegal to change the DOM when the element is not yet connected to the DOM.

Eventually, we define our custom element via the window.customElements.define method which accepts the tag name (ce-modal-window) and the class we’ve just created (CEModalWindow).

Once it is defined, every instance of the custom element is “upgraded” to the new extended element with our template.

How our ce-modal-window renders in the DOM.

Now that we have our custom element, we can add our API to it.

The open API method

As mentioned above, our open method should accept to the following object:

First, let’s add the tests to our spec file:

Look at the describe(‘open’) section.

We would like to test that every time we call open with a certain part of our config, it takes the correct action. Let’s make the tests pass.

All of our tests failed because we did not have the open method on the class. That’s the first thing we must do.

We also know it accepts a config object as a parameter. Now, for each condition in our tests, we parse the config object accordingly.

Eventually, all tests pass. What comes next is part of test-driving: refactoring.

Refactoring the open method

Our open method works, but it is not written very well; too many ‘if’ statements, and the more properties we add to config, the more the method will bloat.

Let’s refactor and see if we can make it better while leaving our tests green.

One thing we can do is to create a static function that changes a style. It will accept an element, a style and a value and alter the element accordingly.

This small refactor went well. We’ve delegated the style-handling to another method. This way, we just need to add any additional style changes in the supportedStyles array.

Note that we could have created our API in many other more robust ways. For instance, changing the config object to have a styles property over which the style parser would run. There’s much that can be improved, but for the scope of this tutorial, it’ll do.

If you have a suggestion for improving the API that you want to share, let me know in the comments below.

The close API method

Since displaying the modal is done by removing the overlay-hidden class, the close method will be super easy to create. Let’s write its test and make it pass:

This works, and all the tests pass. Can you spot a good place for a refactor? That’s right — we query for the overlay twice. Since our element’s overlay and content remain the same as long as our element exists, we can just cache them.

Note the caching in the connectedCallback hook and the usage in the open and close methods. Code is now simpler, tests still pass, and we’re okay to move on.

The app in action

So far, we’ve just created tests and code. Does it really work? Let’s create a sandbox file! We’ll create a file called index.js in the src folder with the following content:

Here we import our custom element’s file so it’s now registered as an element. We then create it and append it to the body (just like in the tests). We also create a click event handler on the modal so clicking anywhere will close it.

Oh wait… What about toggling? We’ve just changed the class. Where do we put the actual CSS for this class?

Remember our template const at the head of the ce-modal-window.js file? Change it to:

Bap!! Now the modal should toggle. But that’s boring! Let’s use better CSS:

Ready for a taste of the good life? Here’s a live demo of the app:

https://demos-b2c3e.firebaseapp.com

We now have a tried, tested and true modal window. Let’s refactor using two other technologies available in the Web Components repertoire: Template tags and shadow DOM.

The template tag

So far, we’ve created a string, and configured the app so that every time we create our element, we add the element and string to the DOM via innerHTML. But every time we create an element, the browser parses our template.

With our small modal, usually just a singleton in the page, this is fine. But with components that can be reused many times in an app and may be added and removed frequently, this incessant parsing can have negative performance implications.

This is why the template tag is the perfect candidate to hold a custom element’s HTML template. Let’s refactor our code to use it:

Note the creation of the template tag in line 30. Note the change in line 39. Now instead of using the costly innerHTML we are using appendChild of a clone of the template. As mentioned above, this saves on parsing and boosts performance.

The shadow DOM

When we added the styles, some of you might have wondered: “What would happen if there were CSS that overrides our component’s styling? Or what if our modal, when injected into an app, clashes with the existing CSS?” (Kids say the darnedest things!)

Enter the shadow DOM! Shadow DOM enables us to encapsulate our CSS inside our web component.

In our case, using shadow DOM will require us to change some test preparations. The expectations would remain the same though:

So what changed? Well now, we use element.shadowRoot every time we query the DOM. Encapsulation means that DOM queries can’t penetrate the shadow DOM. Using the shadowRoot enables us to query again.

Note that the expects themselves didn’t change, only the preparations, so we’re good. As expected, the tests now fail; let’s use shadow DOM in our component and make sure that the tests pass:

Note that we’ve moved the template insertion into the constructor. This is because manipulating the shadow DOM in the constructor is considered safe, and we do not need to use the hook any more.

Styling the host

Is that all there is to Shadow DOM? In regards to JavaScript, yes. But CSS-wise, we have one more surprise. The :host pseudo selector.

The :host pseudo selector enables us to style the element itself from inside the shadow DOM.

We won’t be using it here, but it might come in handy. For instance, let’s say we want to add some margin to our ce-modal-window element. We’ll just add the following style:

:host {   margin: 5px;}

The results would be as follows:

Applying “padding: 50px;” to the host.

But remember, inheritable properties override the :host definitions. So if we define background-color on the :host and some global CSS targets the same element, the global definition wins.

Not all elements can live in the shade

In this example, we’ve extended HTMLElement. In some cases, we’d like to extend a specific kind of element, like HTMLInputElement or HTMLLIElement.

These elements already implement their own shadow DOM and you cannot use shadow DOM with them.

Try changing the extends statement on our class to HTMLLIElement and see what happens.

Happens when we’re trying to define shadow DOM for a subtype of HTMLElement.

ES modules

As you noticed, we are using import in our code — both in the demo code, and in the demo index.js file. This is the ES modules specification (with which you can import modules into other modules) in action. We’ll use them more heavily in the next article, where we will prepare our app for npm publish.

Some of you might be familiar with the HTML imports specifications. These have not been finalized since their inception back in 2010, and Firefox announced that they will not implement it yet — a stance that hasn’t changed in years. Moreover — Chrome has also deprecated this specification. We are using ES modules (export/import statements) to get what we want.

Summary

Wow — we’ve been through a LOT together! We’ve created tests, a custom element to pass these tests, refactored, and used template tags and shadow DOM. ALL IN ONE AFTERNOON!!!

You can see the full repository here.

Hopefully you will now agree that Web Components can be a powerful tool in your development arsenal. You can use them right away in Chrome, and can use polyfills for older browsers. You could also decide they’re dangerous and ugly and never use them again… The choice is yours, my friend.

Above all, I hope this article has impacted the way you think about web application architecture: You can now build small components and (re)use them.

Parting challenge: Can you extend the current project? Can you build something of your own?

--

--