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.
npm install switzerland --save
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 otuside of those contraints 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
Thankfully by utilising custom elements which are native to the browser, we can write interopable 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.
For the most part, Switzerland uses a
create function and a limited set of middleware functions for creating isolated components. In the "Getting Started" section we'll be creating a cheeseboard because all Swiss like cheese, right?
We'll create the simplest of components consisting of the
create function, a tag name for the custom element, and the
html middleware. All this component does is render HTML when the
<swiss-cheeseboard> component is mounted in the DOM.
We need to import
hbecause that's what the virtual DOM transpiles to:
Although any respectable cheeseboard is decorated with delicious cheese, which we would supply via the attributes of the
<swiss-cheeseboard /> element. We need to apply the
attrs middleware which reads the node's attributes, and watches for any mutations, causing an efficient re-render of the component using DOM diffing.
It's worth noting that middleware is compose left-to-right, similar to Ramda's
Each middleware should take
props and return
props that it may or may not augment. Middleware can also be asynchronous, cancel the processing of the middleware, and throw errors. With the
attrs middleware, it augments the
props object with the node's attributes which we need to supply when mounting it to the DOM:
As HTML attributes are purely string based, we supply the list of cheese as comma-separated values, but frameworks such as React can supply attributes as arrays, objects, symbols, etc... which
Switzerland components are more than happy to accept. Nevertheless, the passing of non-string based attributes is non-standard, and as such depends on your individual setup. When writing components it's a good idea to support standard behaviour, and non-standard behaviour when possible. In our case we could have used
Array.isArray to check whether the
list attribute was passed as a string or an array.
html middleware we split by a comma, and iterate over the three cheeses, outputting each in an
<li /> element.
If we later decided that a slice of Stilton was called for, we could mutate the attribute using whichever approach we preferred, and the component would re-render.
const cheeseboard = document;const cheeses = cheeseboard;cheeseboard;
Nevertheless we may decide that our cheeseboard should be able to manage its own list of cheeses internally, and supply a form for allowing the addition of further cheeses.
Switzerland doesn't support React-esque
state/setState out-of-the-box because it's unnecessary, as a component can re-render by passing in subsequent
We can use the
state middleware which simply takes an initial state when invoked, and uses that for the first render. Whenever you make subsequent calls of the
render function, any passed state will be merged with the current state. In our case we're setting the value when it's typed, and then once the button's clicked we're augmenting the
Slots are a native implementation for Shadow DOM that
Switzerland supports. They allow for the passing of data into the shadow boundary — for example in React you have
this.props.children which is akin to a default
We're going to create a profile card for the beautiful array of cheeses that exist in the world. However, we don't wish to create a profile card for each cheese, rather we'd like to pass in certain variables. For this we could pass the variables through DOM attributes, but in our case we're going to be passing an image as well as a name.
We can setup our component in the usual fashion by using the
create function coupled with the
html middleware. The wonderful part about using
<slot />s is that it doesn't require any additional middleware, and so doesn't take up extra bytes in your component.
As you can see, our component doesn't really do a whole lot, however it is using a single
<slot /> that by default renders a placeholder image using the relative path to the component, and an em-dash for the cheese's name. We're essentially asking the developer who uses the component to pass in some HTML to render into the slot.
In the HTML we'd pass in a single slot that holds both the
<img /> and
<h2 /> tags.
Alternatively we could have asked for two separate slots to be passed in:
name which we'd have named
<slot name="image" /> and
<slot name="name" /> from within the component. Our HTML for the component would then have changed to render two elements, each with a
slot attribute that maps to one of two
<slot /> elements in the component.
One interesting aspect of the slot based approach is that you can easily update their values, and the component will reflect the change without actually re-rendering — this of course saves unnecessary CPU cycles, and is a whole lot more efficient.
Whether you choose to allow a single slot or multple slots depends on the control you'd like over the data passed in. With our
cheese-card component it makes to use one slot as both the image and the name are side-by-side, however if they were in two different locations then it makes perfect sense to ask two slots to be passed, where the component's creator would have control over the HTML in between the two slot regions.
<slot /> nodes can also contain other custom elements, which may themselves use slots.
One of the greatest benefits of the shadow boundary is style encapsulation — all styles apply to a particular component, and don't leak out into other areas without any quirky build techniques, such as CSS Modules which means developers can use
Switzerland components irrespective of their build process.
In styling components we use the
include middleware — you specify the path to the CSS document relative to the component file. All paths specified within the CSS file are relative to the CSS document itself.
include middleware can take one or more CSS paths. Also when you create multiple instances of the
cheese-card component above, only one AJAX request will be made for the CSS document.
When creating a component with asynchronous middleware, all middleware will need to have been run before the component is considered resolved — the
resolved class name will be also added to the component's host element. However if a component hosts other
Switzerland components then you may argue that the parent component is not considered resolved until its child components have been resolved. For such occasions
Switzerland provides a
wait middleware that takes a list of nodes to search for, and if found, awaits their resolution before considering the component resolved.
It's worth bearing in mind that if/when the
props.attrs.list.split.length is zero, no
cheese-item components are rendered to the DOM. Nevertheless, the
wait middleware will only attempt to find
cheese-item components, and if none are found, it will happily continue on its merry way to resolving the component.
In most cases, the order of the middleware in
Switzerland is important, and with the
wait middleware it is no different. For instance, if we placed the
wait middleware before the
html middleware then we wouldn't stand a cat in hell's chance of hoping to find any
cheese-item nodes, and thus its addition to the middleware chain would be entirely useless. By adding it after the
html middleware we can be sure that sometimes one or more
cheese-items will be found.
As you can specify multiple
html middleware items in the middleware chain, likewise you can specify multiple
wait middleware items. In that sense, the middleware chain acts like a long
Promise chain —