Creating a super minimal Vue/React/Polymer lib with Web Components
So I recently decided to play around with web components. Polymer is kind of the goto there; giving you some useful patterns you remember from Vue, Angular, React and other similar libraries. At first glance, the threshold for using Polymer — especially with its seemingly weird in-between-components-communications via attributes presented in the official “get started”-tutorials — was rather high. So I figured I’d go wild and do a small minimal view library myself; inspired mostly by Vue.
My kind of boilerplate requirements for the “library” was:
- “Dumb isolated components”. No passed in data via attributes/props, only identifiers to retrieve data from a global state (Redux or something). (Not sure this is great in the long run though, but passing JSON objects down natively in attrs doesn’t sound like a working solution at first thought).
- The component should care of fetching its own data, or hydrate it from its own innerHTML onload (could be from SSR — might be a good strategy for SEO?)
- An imported component should have separated HTML, CSS and JS — just like Vue’s Singe File Components.
- Should be as easy as just
<link rel="import">
to import the component
Let’s get to coding
Let’s start with the index.html. We’ll import two files which we have not yet created. And add them as custom element tags in the <body>
. One with HTML which, if we get it to work, should hydrate it’s initial data from the innerHTML it began with.
Let’s start making <hello-world>
! We know we want to make a “view lib” of some sort. So let’s import it! It’s not created yet. But let’s name it “Template Component”. We do a <link rel="import" href="template-component.html">
on the top to import our view library. Then we make a script block, in which we create a new class and extend from the imported template component.
We’re obviously going to need a special name for the component. Here we draw some inspiration from Polymer and create a static method on the class named is()
It will return the name of the component. Then we make a constructor method in which we call super()
(template components constructor method) with the name of the component. (The result of the `is()` method run on the uninitialized class!)
For some components we want to fetch some data to render in it or start with some default data. We create a getState method on the class. (It could be an async function which does some network requests!)
Then we define the component as usual ala web components with the customElements.define()
method.
Then alongside the script block we make a <template>
block. It will contain the template for out component. We add a <style>
block in there for our component CSS. And then the HTML for the component; using template literals variables for where the data from getState()
should be injected.
Now we’re pretty much shaped up, and have this;
Making the library
We start by making the TemplateComponent class that extends from HTMLElement — just like all other web components, and adding a constructor which takes one argument; the passed in parameter from the super call a component fires! We also store the name of the component on this
, might be useful later!
With custom elements, you cannot rely on the DOM being ready for use in the constructor. Thus, we have the connectedCallback()
available. When this method is triggered by the browser; we know we can start playing with the DOM! So lets add a listener for this event to our class and lets call a render method that we’ll add later in it.
Let’s start implementing the render method. When making the <hello-world>
component earlier we made a getState()
method on it which returns some data. So the first part of our render method is going to try getting the data from that method. We make a variable called state, then we check if the getState()
method was specified in <hello-world>
, if it is; we run the method with await
(in case getState()
is an async function that does network requests). Then we get the template by running the getTemplate()
method that we’ll soon create.
After that, we just do the regular ShadowDOM setup in the render method. We get scoped CSS and HTML. We throw the result of getTemplate()
into the ShadowDOM. Then we run the method postRender()
if it was specified in the <hello-world>
component. This is in case we want to do something after render (like triggering playback of <audio>
with new sources or what have you).
Now we’re going to make the getTemplate()
method. It takes the data to render as the first argument. (The value from getState()
). Then we have an ugly short-liner — templator()
— which basically is an eval function (sorry 😥) that let us take some JSON and put in a string with template literal variables. This is so we don’t have to inline our template in the JS, but rather have it in a <template>
alongside the component <script>
— just Vue Single File Components!
We then have the loadTemplate()
method inside here. It will find the sibling <template>
to <script>
via currentScript.ownerDocument
. We need to wait for the document to load, after which we lookup the template in the DOM and return it. We then run the templator()
method to inject the variables and then getTemplate()
returns the <template>
for the render method to use.
And that’s basically it! Now we have a working little template library. We put it in a .html file in a <script>
tag so components can import them with <link>
. Our final library code looks like this;
For the SSR component which has some JSON in it’s innerHTML onload; we’ll simply parse that in its getState()
method. The component could look like this;
You can see a live demo of this on Plunker.
This is not a serious endeavour in creating the awesome next view library for the web. But it was a fun experiment. Do not use this code in production without adding XSS protection and such, but please play around with it for the sake of learning more about web components yourself. Please comment if you have any improvement ideas — like a better solution to doing the templating!
Please note that this probably only works in Chrome at the moment.
I also went even crazier and tried to make a Frankenstein monster with Elm-inspired ideas and Web Components. You can check that experiment out here:
I hope the article gave some value to you. Have a great day!