loading...

SimpleWC - JSON driven web components

btopro profile image Bryan Ollendyke ・5 min read

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();
  }
}

Shadow events

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

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);
}

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");
    }
  }
});

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)?

Posted on by:

btopro profile

Bryan Ollendyke

@btopro

@elmsln @haxcamp @btopro #HAXTheWeb #drupal #webcomponents #edtech ✻ Full stack unicorn Adjunct professor teaching about webdev, ethics, and everything in between

Discussion

markdown guide
 

Getting rid of repetitive coding tasks is always good... But it comes with some danger. Which features define a simple wc from a complex one? Will you not be tempted to support more and more capabilities into SimpleWC({}) untiliit becomes some competing solution to lit-element, stencil, etc.

Also, do you not run the risk that newbies will never learn the more complex and powerful wc lifecycle in favor of the SimpleWC meta-lifecycle?

In the end, I think that creating this to solve your specific maintenance problem is different than making this a generally available solution.

 

I agree w/ your concerns, they are valid and ones I share. Less so in the "simple" name space as our team makes lots of "simple" elements as a joke. It's built on top of LitElement so really if it had a scope (again, this is mostly PoC and I'm not entirely sure I want to go this way) I'd say the scope is:

  • A user knows what Javascript is but can't write it well
  • A user can write basic css and HTML
  • A user can conceptualize what a new element could be

I could see this as a way of getting a lot of the bits out of the way. Most likely I'd see it used with just CSS,HTML and the data properties area. Working at a teaching university we're always looking for a mesh of productivity and teaching tools; these "simple" elements could provide a way for junior developers to begin implementing design comps and converting old jQuery / component mock ups to legitimate, data binding aware web components.

With a json based approach we could easily build a HAX based UI that abstracts this and allows these type of users to create new web components by filling out a form.

Maintenance is really my concerns with the approach long term as I've just come off of refactoring our ~400 element portfolio from polymer.html to Polymer to PolymerElement to LitElement and am getting a bit port'ed out :).

Another branching thought from this is taking the json blob and using SimpleWC to make a real time updating UI via HAX, but then having a "download web component" button that actually translates it into the real deal. Almost stencil-esk but ripping it to LitElement instead of VanillaJS.

 

I've been dreaming of json powered UI's a lot recently. There's only so many ways you can rebuild the same base component before you figure out the data you need to start automating the boring stuff.

The real power comes when you combine json powered components with graphql and tailwindcss or something similar. You can declare any css you need using an array of tailwind classes, which can be passed around and composed however you want. Storing entire theme presets would be very doable.

If you're storing components as primarily JSON data then your gql schema would already map almost perfectly to your component structure, which I imagine would be a fun convention to prototype with.

And I don't think this locks you into a framework, I think it's your way out. If your data is stored in a portable schema you can transform it into whatever you need it to be and back again.

because the like doesn't do it justice. I agree 100% here, well put. While this post was just a prototype I've started seeing some other similar concepts and while I don't love the abstraction === magic route all the time, I love the notion of GQL or even traditional CMS monolithic backends being able to dynamically inject definitions for front end elements on the fly.

I agree you can only go so far with magic conventions, but you would really only be able to do this with dumb, themeable components anyway. Which I think is a good candidate for abstraction.

being able to dynamically inject definitions for front end elements on the fly

This, exactly. Dynamic module imports should help open the door to this but you need some kind of easily understandable central data contract to keep everything in line. Types & Classes just don't travel as well as a JSON schema.

Sanity.io has a fun example showing what you can do with structured content: github.com/sanity-io/sanity-templa...

don't travel as well as a JSON schema. --- We use JSON Schema as well as an abstraction of it we wrote called HAXSchema to power headless forms via a single tag -- npmjs.com/package/@lrnwebcomponent...

Can see it running here: haxtheweb.org/ when you edit anything and it builds the form on the left that says Layout, Configure, Advanced; that's from HAXSchema being read off of the element and set as schema / values on the simple-fields element :)

 

This sort of configuration requisitive syntax strikes me (as a person who never used these things, so take it with a grain of salt) as very similar to React.createClass({}) and new Vue({}). Particularly, this listing of methods/callbacks/data/et al is very similar to what you get currently in Vue. It was also part of web component past in the form of Polymer during the v0 times...yes, I volunteer as tribute on that prime little code snippet. In all cases, this reliance on JSON gave way to classes and then later (or very soon, at least if the Vue RFC on it gets approved) to functional programming styles. That progression likely is saying something about capability/flexibility/maintainability that is worth listening to, even if it might not be directly pointing to "eliminat[ing] hardcore developers as a barrier".

With that as a stated goal, I'd think you'd be more likely to find a solution like matthewphillips.info/programming/d... in order to take JS out of the equation. It likely will never manage the scope of functionality you're pumping into SimpleWC, but with the amount of functionality that's packed in, hardcore developers might be a prerequisite... If you're looking to lower that barrier while also staying in the JS realm, I wonder what you think of class-based composition (like you see in the Elix project). It's not quite as explicit as what you're getting at with SimpleWC but it does has the side effect of making functionality a clear buy-in process for those "less hardcore" developers.

For my money, I want to work on the composability of web components in HTML or as close to HTML as possible (lit-html allows you a lot of flexibility there!). I think the promise of the slot API shouldn't be the end of the road, but the beginning. Combining that with pointed usage of Mutation Observers, "hardcore" developers should be able to make components that can better support others in bringing what they see in their minds into the world. And, all the while, helping us all in getting back to the beautiful ideals behind web components being the Legos of the internet.

Excited to see more research put into this part of the community, though. JS devs (generally, myself included, sadly) spend so much time being "clever" and so little time being "helpful" that it's really refreshing to see your thoughts here! Looking forward to seeing where this goes.