SimpleWC - JSON driven web components

btopro

Bryan Ollendyke

Posted on December 30, 2019

SimpleWC - JSON driven web components

The HAXTheWeb team (started at Penn State) has some experience with web components. In the last year we've released over 400 in our mono-repo with a mix of reusable design, reusable function and full application / state driven.

Patterns noticed in migration

We recently migrated 100s of elements from PolymerElement to a mix of LitElement and VanillaJS. In that process, we began to realize a pretty repeatable pattern to well scoped elements we were producing. Here's the major bullet points of what these elements had:

  • Window / ShadowDOM scoped events. Window for higher concept, ShadowDOM for local scope event listening
  • dynamically imported visual assets - Doing import() in constructor cuts down on TTFP (time to first paint)
  • properties that report values, notice changes, and calculate other values
  • CSS and HTML obviously

The patterns as code

window events

/**
 * HTMLElement life cycle
 */
connectedCallback() {
  if (super.connectedCallback) {
    super.connectedCallback();
  }
  window.addEventListener('something-global', this.__somethingGlobalEvent.bind(this));
}

/**
 * HTMLElement life cycle
 */
disconnectedCallback() {
  window.removeEventListener('something-global', this.__somethingGlobalEvent.bind(this));
  if (super.disconnectedCallback) {
    super.disconnectedCallback();
  }
}
Enter fullscreen mode Exit fullscreen mode

Shadow events

/**
 * LitElement shadowRoot ready
 */
firstUpdated() {
  this.shadowRoot.querySelector("#someid").addEventListener('click', this.__someidClick.bind(this));
}
Enter fullscreen mode Exit fullscreen mode

dynamic import

These can be loaded in constructor to avoid the tree needing to dedup everything at run time. We recommend unbundled assets so this small change over the life cycle drastically speeds up the TTFP as well as interactivity of the page overall. Putting it in a simple setTimeout helps prevent this (and events) from blocking progression of the scripts. This is a micro-timing sorta thing but again, over 100s of elements loading this makes a huge difference.

/**
 * HTMLElement instantiation
 */
constructor() {
  super();
  setTimeout(() => {
    import('@lrnwebcomponents/h-a-x/h-a-x.js');
  },0);
}
Enter fullscreen mode Exit fullscreen mode

Abstracting our own magic / sugar

So we end up implementing this pattern over and over again. Anytime I do that, write the same thing over and over I feel like I should be abstracting. Further more, if we abstract enough of this pattern it should make it easier to build new elements AND ship faster because we're not writing import(...) 10x in one file, 2x in another, and then 30 others 1x in a different implementation method.

Before I continue, let me say I'm not sure we're going to use the following code but it's created something to form a discussion with the team about.

SimpleWC

The code concept for SimpleWC (SWC class for short) is that if we passed JSON into a factory function, we could spit out reusable web components but without having to write the same code over and over again in the minutia.

Play now

yarn add @lrnwebcomponents/simple-wc

You can see a demo implementing simple-wc here. Here's the code inline though which has 1 import, calls a function and passes a JSON blob, and will generate two LitEement based web components w/ our standard methodology implemented:

import { createSWC } from "../simple-wc.js";
// create a very simple web component
createSWC({
  // name of the web component to register
  name: "simple-button",
  // HTML contents, el is the element itself and html is the processing function
  html: (el, html) => {
    return html`
      <button id="stuff"><iron-icon icon="save"></iron-icon>${el.title}</button>
    `;
  },
  // CSS styles, el is the element itself and css is the processing function
  css: (el, css) => {
    return css`
      :host {
        display: block;
      }
      :host([color-value="blue"]) button {
        background-color: blue;
        font-size: 16px;
        color: yellow;
      }
      :host([color-value="green"]) button {
        background-color: green;
        font-size: 16px;
        color: yellow;
      }
      iron-icon {
        padding-right: 8px;
      }
    `;
  },
  // dynamically imported dependencies
  deps: [
    "@polymer/iron-icon/iron-icon.js",
    "@polymer/iron-icons/iron-icons.js"
  ],
  // data handling and properties
  data: {
    // default values
    values: {
      colorValue: "blue",
      title: "Button"
    },
    // reflect to css attribute
    reflect: ["colorValue"]
  }
});

// create a slightly more complex "simple" web component
createSWC({
  // name of the web component to register
  name: "simple-wc-demo",
  // HTML contents, el is the element itself and html is the processing function
  html: (el, html) => {
    return html`
      <paper-card raised>
        <simple-button
          id="stuff"
          color-value="${el.color}"
          title="${el.title}"
        ></simple-button>
      </paper-card>
    `;
  },
  // CSS styles, el is the element itself and css is the processing function
  css: (el, css) => {
    return css`
      :host {
        display: block;
      }
    `;
  },
  // dynamically imported dependencies
  deps: ["@polymer/paper-card/paper-card.js"],
  // events to listenr for and react to
  events: {
    // window events are added on connection and disconnection
    window: {
      "hax-app-selected": "_appSelected",
      "hax-store-property-updated": "_haxStorePropertyUpdated"
    },
    // after shadowRoot is available, querySelect the key, then apply the event and callback
    shadow: {
      "#stuff": {
        click: "_clickedStuff"
      }
    }
  },
  // data handling and properties
  data: {
    // default values
    values: {
      shadow: 0,
      border: true,
      color: "blue",
      title: "My Title",
      anotherValue: 1
    },
    // reflect to css attribute
    reflect: ["color"],
    // fire up an event whatever-changed on value change
    notify: ["shadow", "color"],
    // run this function when these values change
    // 3rd optional parameter is what value to compute based on the others
    observe: [
      [["border", "color"], "computeShadow", "shadow"],
      [["color"], "colorChanged"]
    ],
    // HAX data wiring
    hax: {
      color: "colorpicker",
      border: "number"
    }
  },
  // callbacks available to the above code
  // this is where all the logic is put into action
  callbacks: {
    colorChanged(newValue, oldValue) {
      console.log(newValue);
    },
    computeShadow(border, color) {
      console.log(border, color);
    },
    _appSelected(e) {},
    _haxStorePropertyUpdated(e) {},
    _clickedStuff(e) {
      console.log("click");
    }
  }
});
Enter fullscreen mode Exit fullscreen mode

The 1st element is very simple. Just CSS/HTML and some very basic properties implemented but it's streamlining the knowledge required to do dynamic imports the way we do them.
The 2nd element is far more complicated as it's providing events and callbacks to demonstrate how complicated you can get as far as what it's doing while still mostly just providing stub code.

LitElement Magic

Some magic going on (or sugar or abstraction or complexity that will burn us later on, whatever you want to call it ;)) is in the properties. It's going to correctly observe, notify and calculate values based on these small arrays.
You can pick apart the code here that's in the "magic" but I mostly wanted to share to get the concept out there. As I said before, I'm not completely sold on it but I wanted to port a few elements of ours to this approach and get a reaction from the team.

Ultimately... why?

Because I think we can abstract away the syntax another level in order to build a UI (because UI that writes JSON is easy / abstractable further) in order to eliminate hard core developers as a barrier to open web participation. By breaking things down into "what does it require", "what does it look like", "what defines it" and "what is its structure" we can get rid of all the tooling and knowledge gaps between those who know HTML/CSS and can get the idea of "placeholders".
If we make a UI (via HAX Schema cause obviously..) in which "normal" web people can start building web components / new tags for the browser, we can lower gaps to entering into front end development (ultimately reducing elitism of the space but I'll keep the preaching for my own blog..).

Questions..

  • Do you think it's interesting and could work?
  • Is this needlessly abstract?
  • Are we basically just going down the same nightmare as JSX/act/_ular as far as abstraction normal ppl don't grep?
  • Would this lock us to LitElement or free us from it long term (honestly not sure)?
💖 💪 🙅 🚩
btopro
Bryan Ollendyke

Posted on December 30, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related