🇨🇭Switzerland takes a functional approach to Web Components by applying middleware to your components. Supports Redux, mobx, attribute mutations, CSS variables, React-esque setState/state, etc… out-of-the-box, along with Shadow DOM for style encapsulation and Custom Elements for interoperability.
Clone or download
Latest commit d4dba82 Jan 20, 2019

README.md

Switzerland

Switzerland takes a functional approach to Web Components by applying middleware to your components. Supports Redux, mobx, attribute mutations, CSS variables, React-esque setState/state, etc… out-of-the-box, along with Shadow DOM for style encapsulation and Custom Elements for interoperability.

Travis   npm   License MIT   Coveralls   code style: prettier

npm: npm install switzerland --save
dkr: docker pull wildhoney/switzerland
cdn: https://cdn.jsdelivr.net/npm/switzerland@latest/es/production/index.js

Contents

  1. Motivation
  2. Plug & Play
  3. Getting Started
  4. Middleware

Screenshot


Motivation

One of the largest downsides to creating components in React, Vue, Ember, etc... is that we re-invent the wheel time-and-time again with every new framework that comes about. Although their components may rely on more generic modules, we are still writing components specific to a certain framework, and typically within a certain version range — if our setup lies outside of those constraints then we need to continue our search.

For example, if somebody writes a <mayan-calendar /> component that works nicely with Mayan dates, wouldn't it be nice if we could use that component wherever, irrespective of our chosen framework and version? If there was a ReactMayanCalendar that works with React 15.x then we'd be out of luck if our setup was Ember based — or React 16.x based.

Thankfully by utilising custom elements which are native to the browser, we can write interoperable components that can be used anywhere — on their own or in a framework. In addition we inherit other benefits, such as style encapsulation to prevent cross-contamination, and relative loading of CSS documents and associated images.

Plug & Play

Switzerland is capable of being integrated into any website or app without any formal installation or build process if you wish. Thanks to shadow DOM technology, all styles are also applied since Switzerland detects which host the JS originated from; if the origin and the JS host differ, then absolute paths to the domain are used when loading assets, such as CSS documents and images.

As a little teaser, navigate to Google.com and paste the following snippet of code into the console:

const node = document.createElement('script');
node.type = 'module';
node.src = 'https://switzerland.herokuapp.com/nodes/todo-app/index.js';
document.head.append(node);
document.body.append(document.createElement('todo-app'));

After a couple of milliseconds you should see the todo app embedded into Google with all of the styles applied. If you have any todos in your list then you will also see those due to the IndexedDb that the example utilises. It's worth noting that for this example to work correctly, the host — in the above case switzerland.herokuapp.com — needs the CORS headers configured correctly.

Getting Started

As Switzerland is functional its components simply take props and yield props – middleware can have side-effects such as writing to the DOM, and can also be asynchronous by yielding a Promise. Middleware is processed on each render from left-to-right which makes components very easy to reason about. In the example below we create a component called x-countries that enumerates a few of the countries on planet earth:

import { create, m } from 'switzerland';

create('x-countries', m.vdom(({ h }) =>
    h('ul', {}, [
        h('li', {}, 'United Kingdom'),
        h('li', {}, 'Russian Federation'),
        h('li', {}, 'Republic of Indonesia')
    ])
));

We now have a usable custom element called x-countries which can be used anywhere. We're able to use the element even before the element is declared, as Switzerland subscribes to the progressive enhancement paradigm whereby elements are upgraded asynchronously. In the meantime you could display a loader, placeholder or even nothing at all before the component renders.

<x-countries />

For the x-countries component we only have one middleware function – the vdom middleware which takes props and yields props but has a side-effect of writing to the DOM using superfine's implementation of virtual DOM. It's worth noting that Switzerland doesn't encourage JSX as it's non-standard and unlikely to ever be integrated into the JS spec, and thus you're forced to adopt its associated toolset in perpetuity. However there's nothing at all preventing you from introducting a build step to transform your JSX into hyperdom.

Let's take the next step and supply the list of countries via HTML attributes. For this example we'll use the Switzerland types which transform HTML string attributes into more appropriate representations, such as Number, BigInt, etc...

import { create, m, t } from 'switzerland';

create(
    'x-countries',
    m.attrs({ values: t.Array(t.String) }),
    m.vdom(({ attrs, h }) =>
        h('ul', {}, attrs.values.map(country => h('li', {}, country)))
    )
);

Notice that we've now introduced the attrs middleware before the vdom middleware; we have a guarantee that attrs has completed its work before passing the baton to vdom. It's the responsibility of the attrs middleware to parse the HTML attributes into a standard JS object, and re-render the component whenever those attributes are mutated. Since the list of countries now comes from the values attribute, we need to add it when using the custom element:

<x-countries values="United Kingdom,Russian Federation,Republic of Indonesia" />

By taking a reference to the x-countries element and mutating the values attribute we can force a re-render of the component with an updated list of countries:

const node = document.querySelector('x-countries');
node.attributes.values = `${node.attributes.values},Hungary,Cuba`;

Switzerland components only take string values as their attributes as that's all the HTML spec allows. Using the types we can transform those string values into JS values, and with this approach we allow for greater interoperability. Components can be used as pure HTML, using vanilla JS, or inside React, Vue, Angular, etc... Passing complex state to components only reduces their reusability.

Where other JS libraries fall short, Switzerland considers all web assets to be within its remit. For example in React it is fairly common to use a third-party, non-standard, somewhat hacky JS-in-CSS solution that brings its own set of complexities and issues. With Switzerland it's easy to package up a regular CSS file alongside the component, and have the assets it references load relative to the JS document without any configuration. For that we simply render a style node in the vdom middleware – or the template middleware if we choose to use JS template literals:

import { create, init, m, t } from 'switzerland';

const path = init(import.meta.url);

create(
    'x-countries',
    m.attrs({ values: t.Array(t.String) }),
    m.vdom(({ attrs, h }) =>
        h('section', {}, [
            h.sheet(path('index.css')),
            h('ul', {}, attrs.values.map(country => h('li', {}, country)))
        ])
    )
);

We use the h.sheet helper function that uses @import to import a CSS document into the DOM, which also specifies a static key based on the path to prevent the CSS from being constantly downloaded on re-render. In using the init function we have a function that allows us to resolve assets relative to the current JS file:

:host {
    padding: 15px;
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.25);
    background-image: url('./images/world.png');
}

By utilising shadow DOM you're able to keep your CSS documents as general as possible, since none of the styles defined within it will leak into other elements or components. As the CSS document is imported, all assets referenced inside the CSS document are resolved relative to it. Switzerland also has a special function before a component is resolved to ensure all imported CSS files have been loaded into the DOM.

Adding events to a component is achieved through the dispatch function which is passed through the props. In our case we'll set an event up for when a user clicks on a country name. Switzerland uses the native CustomEvent to handle events, and thus guaranteeing our components stay interoperable and reusable:

import { create, init, m, t } from 'switzerland';

const path = init(import.meta.url);

create(
    'x-countries',
    m.attrs({ values: t.Array(t.String) }),
    m.vdom(({ attrs, dispatch, h }) =>
        h('section', {}, [
            h.sheet(path('index.css')),
            h('ul', {}, attrs.values.map(country => (
                h('li', {
                    onclick: () => dispatch('clicked-country', { country })
                }, country)
            )))
        ])
    )
);

Interestingly it's possible to use any valid event name for the dispatch as we simply need a corresponding addEventListener of the same name to catch it. Once we have our event all set up we can attach the listener by using the native addEventListener method on the custom element itself:

const node = document.querySelector('x-countries');
node.addEventListener('clicked-country', event => (
    console.log(`Country: ${event.detail.country}!`)
));

Middleware

  • adapt – Uses ResizeObserver to re-render whenever the component's dimensions change;
  • attrs – Provides the parsing and observing of a node's attributes.
  • blend – Keep your functions general when using as middleware functions.
  • defer – Invokes function after x milliseconds if the current render hasn't completed.
  • delay – Awaits by x milliseconds before continuing to the next middleware item.
  • wait – Await the resolution of other components to make rendering atmoic.