SimpleWC - JSON driven web components
Bryan Ollendyke
Posted on December 30, 2019
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 December 30, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 30, 2024
November 30, 2024