Componentize Your Web Apps

clock icon Jan 17, 2019


At GDG Developers Festival Southern and Eastern Nigeria 2017 (see how it went down here -> #DevFestSE17), I was opportuned to give a talk on Building Progressive Web Apps with Polymer. One of the slides talked about decoupling a web app into reusable components. This is the natural way of building things - smaller components put together to make up a full system. To add some context to this, let’s imagine we want to build a house. Blocks (or bricks as the case may be) are stacked up on top each other to form the walls of the house, plaster and cement hold them in place, a roof made of metal or asphalt or some other material is put above, doors placed in their hinges, and so on. If we think of the house as a full system, the blocks, cement, roof, doors, are the components that make the house.


Why you should build components?

Besides being the natural way of building things, there are other benefits of building reusable components.

  • Consistent UI - components look consistent across different sections of the web app
  • Reusable Code - components can be imported and used in multiple projects
  • Improved branding - this also influences the branding of the product/company since components look the same anywhere they appear
  • Eliminating redundancy - instead of writing the same HTML and CSS over and over for a button, the button can be created as a component and reused where it’s required

How do we actually decouple an app? It's one thing to want to do it, it's another thing to actually do it. Fortunately, many people have spent time thinking about this problem for a long time and have come up with a number of various solutions. I’ll focus on two of them.


JavaScript Frameworks

There are as many as javascript frameworks as there are stars in the sky. Don’t mind me, I’m just kidding. This is a debate for another day but one popular selling points are of javascript framework is the ability to create reusable elements. JavaScript frameworks allow you to build reusable elements that are put together to make up the web app. The framework handles bundling, routing, minifying and more, allowing you to focus on building these custom elements.


Each framework handles the creation of components differently but similarities can be found across the various frameworks. Once familiar with one, it's a bit easier to pick up another one. In Vue.js, a simple component is created like this:

<template>
<div class=”main”>
{{ message }}
</div>
</template>
<script>
name: ‘app’,
data () {
return {
message: ‘Hello Vue’,
}
}
</script>
<style scoped>
.main {
background-color: #fcfcfc;
}
</style>

There are 3 visible sections: template (DOM), script (behaviour), and style (appearance). The template section holds the markup of the element. Parts of the markup can be bound to properties of the element using the moustache syntax {{ }}, and Vue keeps track of the property and update the markup when the property changes. The script section holds the properties, both the public and local properties. It also holds various methods that are used to provide some functionality by the element. The style section contains presentation information of the element and it can be scoped i.e. prevents the styles from leaking out and affecting other styles in the document.


Shortcomings

Javascript frameworks come with lots of great benefits and speed up development times, but there are some tradeoffs/drawbacks.

  • Overhead cost - framework code has to be downloaded, initialized and setup before the web app can start doing anything
  • Bundle size - the bundle size of your app increases as its dependencies increase. While techniques like code splitting and lazy loading reduce this as much as possible, there's still a slight bump to the bundle size due to unused code or poor implementation
  • SEO - search engines traditionally don’t run javascript and since the web app is built by javascript, search engines are not able to crawl the web app properly. There is active development going on to enable crawlers parse javascript, with the Google crawler now being able to parse javascript but other crawlers still have some catching up to do

Web components spec

According to MDN,

Web Components is a suite of different technologies allowing you to create reusable custom elements — with their functionality encapsulated away from the rest of your code — and utilize them in your web apps.

There are three points to note from this definition.

  • A suite of different technologies - there are different technologies involved in the creation of reusable custom elements
  • Functionality is encapsulated - even though reusable elements can be used in various parts of web apps, they look and behave consistently as core functionality is encapsulated (hidden) and only exposes small segments for interaction
  • Utilize them in your web apps - created once but can be used in various areas of a web app and even in more than one web app

One of the main goals of the web components specifications is to make code reuse as simple as possible. This is one of the reasons, amongst others, that javascript frameworks have been a big hit. Since it is a suite of technologies, let’s take a look at the individual technologies involved.


Custom elements: lays the foundation for the creation of new DOM elements through a set of javascript APIs.

Shadom DOM: allows the creation of a “shadow” DOM tree that’s attached to an element and encapsulated other parts of the web app, which enables styling and scripting without collision with other parts.

HTML Template: enable you to write fragments of markup that reused as needed later.


Creating a Custom Element

Using the various technologies above, let’s create a simple custom element. The custom element I’ll walk you through is a simple button with a count, indicating how many times the button has been clicked. The full source code is hosted on github so you can clone it, build on it, extend it, improve it, whatever suits you. To start off, create a new file called custom-button.js and add this:

class CustomButton extends HTMLElement {
...
}
window.customElements.define('custom-button', CustomButton);

We create the custom element CustomButton by extending the base class HTMLElement. This class holds the markup and functionality of our custom element. Once that is defined, we register it to the browser with customElements.define(...). This function tells the browser to associate the tag <custom-button> with this class, CustomElement.


That’s it. We’ve created a custom element. Import the script from an html document and use the element like so:

<custom-button></custom-button>

At the moment, it doesn’t do much since we’ve not added any markup or functionality. Let’s change that, shall we? Just before the definition of the class CustomButton, add this little snippet of code:

const template = document.createElement('template');
template.innerHTML = `
<p>Counter: <span id="counter"></span></p>
<button>Click Me</span>
`;

Using the <template> element is more of a performance decision than anything. The <template> element allows you to create inert HTML i.e the HTML is not rendered, that can be reused at a later time.


In the CustomButton class, add this:

constructor(){
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.appendChild(template.content.cloneNode(true));
}

Custom elements have a number of callbacks which are called at various stages of the elements lifecycle. This enables authors of custom elements to attach different behaviour at different lifecycle events. The constructor() is called at the initialization of the element. When overriding this callback, always call super() first so the super class can run it’s setup code. It would throw an error if you don’t call super().

this.attachShadow({ mode: 'open' });
this.shadowRoot.appendChild(template.content.cloneNode(true));

The snippet above shows functions from the Shadow DOM API. The Shadow Root has two modes: open and closed. If set to open, you can attach a shadow tree to the element. By default it’s set to closed. The second line clones the template defined earlier and attaches it to the shadow root.


Save and refresh the browser and you should see something similar to the picture below.




We have some user interface now but if you try to click the button, nothing happens.. Let’s imagine this element was put into a web app. It should be able to receive data through attributes or properties and automatically update the user interface. It should also keep both attributes and properties in sync. How can we achieve this?


The Custom Element API has an observedAttributes method. It returns an array of attributes that we would like to observe for changes. Add the method like so:

static get observedAttributes() { return ['count']; }

In this case, we observe just one attribute count. To react to those changes, use the attributeChangedCallback() like:

attributeChangedCallback(attr, oldVal, newVal) {
...
}

It has 3 parameters:

  • attr: the attribute that changed
  • oldVal: the previous value of the attribute
  • newVal: the new value of the attribute

Inside the callback, add this snippet of code:

switch (attr) {      
case 'count':       
if (oldVal !== newVal) {         
this[attr] = newVal;         
this._updateUI();       
}       
break;   
}

I’m using a switch statement. If the element has multiple attributes, we would have multiple cases to match the attributes but in this case, we have just one (count). I check to make sure the old value is not equal to the new value before setting the corresponding count property to the new value. Then I call a _updateUI() to update the user interface.


Each property has a setter (to set the property) and getter (to get the value of the property). To keep them in sync, add the setter and getter like so:

set count(value) {    
if (value) {     
this.setAttribute('count', value);   
} else {     
this.removeAttribute('count');   
}  
}  

get count() {   
return this.getAttribute('count');  
}

In the setter, I get the value passed to it and set the corresponding count attribute with the value. In the getter, I simply reflect the value of the count attribute. To update the user interface, add this method:

_updateUI() {    
const counter = this.shadowRoot.querySelector('#counter');   
counter.innerText = this.count;  
}

The underscore(_) means that the function is a private method available only inside the class. This method simply queries the DOM, then updates it with the value of the count property. Now you can use the element in your html like so:

<custom-button count="10"></custom-button>
<custom-button count="5"></custom-button>

Save and refresh your browser. The user interface should now reflect the attribute.



The button is still not functional so let’s write some code to add some functionality. To do this, we will use the connectedCallback(), which is called when a custom element is first added to the DOM. In the connectedCallback(), add this 2 lines of code:

connectedCallback() {    
const button = this.shadowRoot.querySelector('button');   
button.addEventListener('click', this._increaseCount.bind(this));  
}

As good citizens of the web, let’s also remember to remove any event listeners we set. The disconnectedCallback() is called when the element is removed from the document so it’s a suitable place to remove event listeners.

disconnectedCallback() {    
const button = this.shadowRoot.querySelector('button');   
button.removeEventListener('click', this._increaseCount.bind(this));  
}

In the connectedCallback(), we get a reference to the button and add a listener for the click event. I bind it to the element’s scope, not the button’s, hence the .bind(this). The  function called by the button looks like this:

_increaseCount() {    
const count = parseInt(this.getAttribute('count'), 10);   
this.count = count + 1;   
this._updateUI();  
}

The function gets the value of the count attribute, converts it to an integer, adds 1 to the value and calls the method to update the user interface. When you save and refresh your browser, the count should increase by 1 when you click the button.


That was a crash course on web components specification. You can read further using MDN’s article on web components or on webcomponents.org.


Shortcomings

While being able to tap into the Web platform API’s that were out of reach before the web components specifications is a huge step towards building better web apps, there are still pain points encountered. Some of this include but are not limited to:

  • Keeping attributes and properties in sync - writing setters and getters for one property is doable but doing it for 10, 20, or more becomes very tedious.
  • Updating the DOM - updating the user interface to reflect updated properties is also a pain point

These are just a few pains developers encounter when authoring custom elements. This is a good place for an abstraction to come in and make it easier to handle these pain points. Fortunately, there are abstractions available which are built on the web components spec. One of the popular libraries available is the Polymer library, created by the Polymer team at Google. It makes it easier to create and manage custom elements.


This was just a short introduction to custom elements. If you got to this point, I salute your bravery. Thanks for reading and if you found this useful, please share.