Web Components

For a live demo of the Web Components proof of concept, please visit https://fx-components.flexera.info/

Dictionary

WC = Web Components

Introduction

This research piece for WC aims to see if WC are:

  • Easy to add and maintain components
  • Easy to theme
  • Easy to test
  • Accessible
  • Able to work with and without React
  • Reliant on little to no 3rd party libraries
  • Easy to refactor to our existing codebase

What are Web Components?

Web components are a set of web platform APIs that allow you to create new custom, reusable, encapsulated HTML tags to use in web pages and web apps. Custom components and widgets build on the Web Component standards, will work across modern browsers, and can be used with any JavaScript library or framework that works with HTML.Reference: https://www.webcomponents.org/introduction

To add on to the above reference from the official domain for WC, WC are a framework agnostic method of creating a suite of reusable components.

  • Custom elements: A set of JavaScript APIs that allow you to define custom elements and their behavior, which can then be used as desired in your user interface.
  • Shadow DOM: A set of JavaScript APIs for attaching an encapsulated "shadow" DOM tree to an element — which is rendered separately from the main document DOM — and controlling associated functionality. In this way, you can keep an element's features private, so they can be scripted and styled without the fear of collision with other parts of the document.
  • HTML templates: The <template> and <slot> elements enable you to write markup templates that are not displayed in the rendered page. These can then be reused multiple times as the basis of a custom element's structure.

Analysis

Without going straight into the code, it's important to think why WC might be appropriate for Flexera at a high level. As mentioned in the Introduction section, WC provide a framework agnostic method of creating a suite of reusable components, which could be a great advantage for a larger organization such as Flexera due to the many different technology stacks that we have.

At the time of writing, our current Component Library is written purely in React. This is fine for importing to React codebases, but we are only really left with one decision to make for other codebases such as Angular or similar, and that is refactoring the technology stack to React. This would mean rewriting an entire Angular codebase to React just so as it can make use of our Component Library.

This reason alone make WC a compelling prospect, so let's dig a little deeper.


  const primaryButtonStyles = `
    button {
      background-color: var(--buttonPrimaryBgColor);
      border: none;
      color: var(--buttonPrimaryTextColor);
      cursor: pointer;
      outline: none;
      padding: calc(var(--marginMd) / 2) var(--marginMd);
    };
  `;
  
  const secondaryButtonStyles = `
    button {
      background-color: var(--buttonSecondaryBgColor);
      border: none;
      color: var(--buttonSecondaryTextColor);
      cursor: pointer;
      outline: none;
      padding: calc(var(--marginMd) / 2) var(--marginMd);
    };
  `;
  
  const applyStyles = (theme) => {
    switch (theme) {
      case 'primary':
        return primaryButtonStyles;
      case 'secondary':
        return secondaryButtonStyles;
      default:
        break;
    }
  }
  
  
  class Button extends HTMLElement {
    constructor() {
      super();

      this.template = document.createElement('template');
      this.template.innerHTML = `
          <style>${applyStyles(this.getAttribute('theme'))}</style>
          <button>
              <slot></slot>
          </button>
      `;

      this.attachShadow({ mode: 'open' });
      this.shadowRoot.appendChild(this.template.content.cloneNode(true));
    }
  }
  
  customElements.define('fx-button', Button);
  

The above code snippet is how we can create a Button WC. The styles can be abstracted out to a different file, but I have added them to the snippet just as a reference.

For every WC that we create, we must create a class (in this instance, Button) that always extends from HTMLElement. Unfortunately we cannot extend from anything else, as in the case of creating a Button it would have been nice to extend from HTMLButtonElement.

To render the component we first create a new template tag, then simply add to the innerHTML of it. As we are creating a button, I have used the native HTML <button> element. The <slot></slot> element is how we can add syntax inside our Button WC when we are using it.


  /*
    Anything inside of <fx-button></fx-button> is rendered via <slot />

    In this instance, the text of "Submit" is added to <slot />, and
    renders as you would expect in the DOM
  */
 
  <body>
    <div>
      <fx-button>
        Submit
      </fx-button>
    </div>

    /*
      Then we simply reference the file where the WC came from
    */

    <script src='./Button.js'></script>
  </body>
  

That's the native implementation, but what about a 3rd party library that compiles to native WC?

Enter, Stencil.


  import { Component, h, Prop } from '@stencil/core';
 
  @Component({
    tag: 'fx-button', // This is how the component is written (<fx-button></fx-button>)
    styleUrl: 'fx-button.scss', // Reference a stylesheet
    shadow: true, // This adds the component via the Shadow Root, meaning that it is encapsulated from the rest of the DOM
  })
  export class FxButton {
    /**
     * @Prop() is how we define props that our web component
     * will have. The below example of "theme", of various string types,
     *
     */
    @Prop() theme: 'primary' | 'secondary' | 'danger';
  
    render() {
      return (
        <button type="button" class={`btn ${this.theme}`}>
          <slot />
        </button>
      );
    }
  }
  

As you can see in the above code example for Stencil JS, this gives us a more familiar approach to React as it uses JSX. Stencil by default also uses TypeScript, so even better we still get to define our props and types. We then reference this in the same way that we do in the native example.

Stencil also provides us with a way of exporting WC as React Components, which could provide to be quite useful if we were to go down this path. This would mean that we could theoretically still have syntax such as <Button />, but behind it is still a WC. Although after further investigation, reading through their React integration docs they recommend that you have 2 libraries - 1 for your Stencil Components, and another for that acts as a sibling to the main library, but compiles to React.

If the concept for Team Obsidian is to have one reusable component library, using the React integration method would negate the consistent syntax we would be aiming to achieve between our applications, but still something to be aware of.

Syntax Changes

This seems like a very small detail, but take for example a Button component. We currently use a Button component like this:


  <Button onClick={handleOnClick}>Submit</Button>
  

But if we were to convert our buttons to the Web Component equivalent, we need to write this:


  // Using native HTML onclick attribute
  <fx-button onclick="handleOnClick()">Submit</fx-button>
  
  <script>
    const handleOnClick = () => {
      console.log('clicked!')
    })
  </script>
  
  // Or via an event listener, depending on developer preference
  <fx-button id="submit-button">Submit</fx-button>
  
  <script>
    document.querySelector('#submit-button').addEventListener('click', () => {
      console.log('clicked!');
    })
  </script>
  

Yes this is a small change, but when we take into account the amount of Button components that are used within Flexera One, this will create an abundance of re-writing in our codebase. This is something to keep in mind if we were to go ahead with a Web Components library, as every component inside Fusion will have to be updated individually and with caution.

Support

https://caniuse.com/?search=web%20components

Using current data from can i use, have support for:

  • Chrome
  • Chrome for Android
  • Firefox
  • Firefox for Android
  • Microsoft Edge
  • Android Browser

Using the same data, we only have partial support at the minute for:

  • Safari
  • Safari iOS

There is no support for any version of Internet Explorer.

https://custom-elements-everywhere.com/libraries/react/results/results.html

Should we use Web Components (or an accompanying Web Component library)?

There are pro's and con's to deciding if we should go with WC or not, so I think it's important to ask ourselves a few key questions:

  • Do we have adequate time to refactor our existing React codebase to WC? At this stage, I do not think we would have the resources available to refactor our existing codebase to use WC. As well as swapping out our React Component Library to a new WC Library, the majority of our tests inside Fusion would also need to be updated. This would create severe overhead for Team Obsidian, and that is before the actual creation and testing of a new WC Library in the first place.
  • Are we a "React" company? It would be advantageous to have a flexible technology stack on a per project basis instead of being constrained to one setup. Let's say we wanted to try VueJS, Svelte, or another UI Framework, are we in a position to do this? Currently, our project architecture is so deeply entwined in React that it's difficult to see us being in this position, especially as all of our packages are housed inside one huge monorepo instead of being micro-frontends. All of our routing and lazy-loading is handled via React also. The team at Flexera are also planning to refactor our legacy applications that were built in Angular to React codebases, so we will be even more deeply invested into the React ecosystem.
  • How do we future-proof ourselves? Members of Team Obsidian will know all too well that the world of Front-End changes at a rapid pace. The library we choose today will probably be out of fashion tomorrow. So try to imagine 2 or 3 years from now - what position do we want to find ourselves in? I understand that this is a very hypothetical question, but one that we should definitely think about. What if a new UI Framework comes along and the team decides it's time to replace our React codebases with "Library X". Likely by that stage, our entire product suite will be written in React, as well as having a likely bigger React Component Library. Will we be in a good position to refactor everything?

In closing, if WC isn't currently the answer, I still think we should keep our eyes on WC and keep our options open.