November 27, 2017
Even though styling reusable components is a hard problem, the Elix project needs a solution if it’s to keep moving forward. The library’s goal is to provide general-purpose components that can be styled/themed to meet customers’ needs. As far as we’re aware, neither the web platform nor component frameworks give us the styling primitives we need. For now, Elix is tackling this styling challenge with subclassing.
Asking a component what it wants to update
To begin, our solution relies on the previously-discussed
ReactiveMixin, to define components in a React-ish, functional-reactive style. That post includes a live demo of a canonical increment/decrement component created with ReactiveMixin
. The source shows a render
function that updates DOM whenever state changes.
Let’s add custom styling and behavior to that increment/decrement component. We’ll start by using an Elix mixin called ShadowTemplateMixin
to populate the shadow with the same template we used before:
<template id="template">
<button id="decrement">-</button>
<span id="visibleValue"></span>
<button id="increment">+</button>
</template>
Now we’ll make use of a new Elix mixin called RenderUpdatesMixin that asks the component for a set of updates to apply during rendering. The mixin will then update the DOM as requested. The component supplies this updates
object as a property, indicating the attributes, classes, styles, and other properties to update:
get updates() {
return {
style: {
color: this.state.value < 0 ? 'red' : null
},
$: {
visibleValue: {
textContent: this.state.value
}
}
};
}
The top level keys of the updates
object will be applied to the component’s host element. Here, the style
key says that the host element’s style.color
should be updated to 'red'
when the value is negative, and left unspecified otherwise. It’s not shown above, but a component can also specify keys for attributes
and classes
to modify those aspects of the host element. Any keys that aren’t special are treated as custom properties and set directly.
The $
section contains updates that should be applied to elements in the component’s shadow. When ShadowTemplateMixin
sees a template element with an id
like <span id="visibleValue"></span>
, it defines a reference this.$.visibleValue
to point to that span. Here, the updates
object is asking to update that span’s textContent
to the current number in this.state.value
.
This updates
getter is equivalent to the imperative:
[symbols.render]() {
this.style.color = this.state.value < 0 ? 'red' : null;
this.$.visibleValue.textContent = this.state.value;
}
A component interaction pipeline
The use of RenderUpdatesMixin
and other Elix mixins lets us construct a pipeline of sorts inside the component:
events → methods/properties → setState → render → updates → updated DOM
When the user clicks the “+” or “-” buttons:
- A
click
event fires, which…
- sets the
value
property via a public API, which…
- invokes
setState
to update this.state.value
, which…
- invokes an internal
symbols.render()
method that…
- asks the component for the state-dependent
updates
it wants to make which…
- get applied to the DOM.
And, as a result, the user sees the visible value number go up or down.
The updates
are applied via helper functions that make the underlying DOM API calls. There’s no virtual DOM diff’ing going on here, but the number of updates
is generally small and targeted to the elements that are actually changing. For the time being, performance seems reasonable.
As an aside, I’ve come to generally shy away from declarative formats like the updates
object above. People like the concise nature of a declarative format for UI structure or behavior, and such a format can have a place in systems devs are willing to learn.
I think that learning cost is steep, so in code I want other people to use or contribute to, I try to avoid introducing declarative formats. Doing so is tantamount to shouting, Whee! I’ve invented a new domain-specific language for you to learn! The syntax may be JavaScript, but the semantics are opaque — it’s really a tiny interpreted language. Though my concise declarative language may be easy for me to understand, it’s impossible for you to know what effect it will have unless and until you’re willing to learn my new language.
So I currently avoid declarative code unless it has some concrete advantages.
Styling and specializing via subclasses
That said, in this case defining updates
as an object does offer a real advantage: the updates can be augmented by mixins and subclasses.
When we say we want to let customers style a reusable component, that’s another way of saying we want to let people take existing code and specialize it. A component is just a class, and a traditional means to specialize a class is to create a subclass. So let’s see how subclassing can work here.
Since the updates
property sits on the prototype chain, it can be overridden by a mixin or subclass that wants to add or adjust updates
for the current state. A mixin/subclass can invoke super
to get the base updates
, then modify as that base value as it sees fit. E.g., someone could create a custom version of the generic increment/decrement component above:
class CustomIncrementDecrement extends IncrementDecrement {
get updates() {
const base = super.updates;
const baseColor = base.style && base.style.color;
// Pick a color if the base class didn't specify one.
const color = baseColor || (this.state.value > 0 ? 'dodgerblue' : null);
// Merge updates on top of those defined by the base class. This lets us
// preserve some of the base rendering, while adding our own styling and
// some unique behavior.
return merge(base, {
style: {
background: 'lightgray',
color,
'font-family': 'Helvetica, Arial, sans-serif',
'font-weight': 'bold'
}
});
}
}
Here the component indicates that its host element style
should be updated with custom colors and fonts. Rather than focusing on CSS rule precedence, the prototype chain determines what updates
apply — last writer wins. If you customize my class by subclassing it, your subclass has the last say.
This code relies on a merge
helper that generally does a shallow merge, but goes deeper when merging the special keys attributes
, classes
, style
, or $
. The merging allows the updates cooperatively constructed by the base class, any mixins, and any subclasses to be efficiently computed and applied.
Applying such state-dependent styling is tricky in CSS: all state would first need to get rendered to the DOM as attributes, then CSS rules would have to be conditional on the presence of those attributes. Overriding such CSS rules requires carefully matching their precedence, otherwise customizations might be overly general or overly specific.
It’s worth noting that mixins/subclasses can inspect the updates
requested by the base class, and incorporate those values into their own calculations. In the sample above, the subclass provides a blue color
for positive values, but leaves alone the red color the base class provides for negative values.
Updating shadow parts
The above code only customizes the host element, which we could do via CSS directly. What we’re really after is a way to customize shadow parts: elements inside the shadow tree. Our customized increment/decrement component can do that through the $
key described earlier:
get updates() {
const base = super.updates;
const baseColor = base.style && base.style.color;
const color = baseColor || (this.state.value > 0 ? 'dodgerblue' : null);
const buttonStyle = {
background: '#444',
border: 'none',
'border-radius': 0
};
const decrementDisabled = this.state.value <= -5;
const incrementDisabled = this.state.value >= 5;
return merge(super.updates, {
style: {
background: 'lightgray',
color,
'font-family': 'Helvetica, Arial, sans-serif',
'font-weight': 'bold'
},
$: {
decrement: {
attributes: {
disabled: decrementDisabled
},
style: merge(buttonStyle, {
color: decrementDisabled ? 'gray' : 'white'
})
},
increment: {
attributes: {
disabled: incrementDisabled
},
style: merge(buttonStyle, {
color: incrementDisabled ? 'gray' : 'white'
})
}
}
});
}
Live demo
Above we style the buttons with some base styling. We can also modify attributes or other properties. Here we arrange for the buttons to only allow input values between -5 and 5. (For completeness, we can also impose the same input bounds on the value
property exposed in the public API.) We apply conditional styling to show the buttons differently when they’re enabled or disabled.
Mixins that update light and shadow DOM
If you’re reluctant to create a class hierarchy, you can do what Elix does and factor most of your code into functional mixins. Mixins allow your code to be reused across classes, and permit a great deal of flexibility.
For example, I’ve previously described how components often need to update light DOM to support ARIA attributes. To address that scenario, we’ve factored out ARIA attribute handling for list-like components into a mixin called AriaListMixin. That mixin augments the component’s updates
getter to apply attributes like role
, aria-orientation
, and aria-activedescendant
.
Results
We’ve successfully applied this architecture to the current Elix component set. Using a declarative updates
object makes the code very concise, which is good — but also makes the code opaque to outsiders, which is bad. The main win is that we now have a workable method for creating custom-themed versions of these general-purpose components. Significantly, the themed components are just custom elements that can be used by clients like any other web components.
If others come up with other ways to style general-purpose web components, we’d be very interested. In the meantime, we at least have a way to keep moving forward.
November 20, 2017
The easiest way to create web components with a distinctive visual style is to bake that style directly into the components’ code. Most user interface components are designed to be used solely within the company creating them, in which case baking in styles may be acceptable. But anyone aspiring to create or consume reusable general-purpose web components will have to grapple with the fact that styling components is currently an unsolved problem.
That’s worrisome. Most web components we’ve seen have a built-in visual style so distinctive that, without modification, it would look out of place in an app with someone else’s brand. Not being able to easily theme such a component limits its utility.
Suppose you’re writing a hypothetical custom element called reusable-component
and want to let other people style that element. Let’s consider your options for doing so.
Use CSS Custom Properties
If you think someone wants to change the background color of your reusable-component
, you can define its background-color
style with a CSS Custom Property:
:host {
background-color: var(--reusable-component-background-color);
}
and your users can then style your component by defining that custom property:
:root {
--reusable-component-background-color: blue;
}
With the recent release of Edge 16, the above would now work in all modern browsers.
Unfortunately, this just doesn’t scale well. It’s a pain for you, who must define a new custom property for every CSS attribute someone might conceivably want to override. If your component has internal shadow parts — buttons, etc. — that your users might want to style, you have to define new custom properties for all the interesting CSS attibutes on all those parts. And this will be painful for your users, who have to learn a long new list of custom properties for every component they might want to style.
Wait for CSS parts and themes
To solve the above problem, there’s a proposal for CSS Shadow Parts which would make it possible to expose designated internal parts as pseudo-elements that can be styled from the outside. This is similar to the way you can style certain native HTML elements in a non-standard way with certain pseudo-elements. For example, WebKit exposes the thumb (handle) of a scroll bar as a pseudo-element ::-webkit-scrollbar-thumb
, so you can write
::-webkit-scrollbar-thumb {
background: pink;
}
to make scroll bar thumbs pink.
The CSS Shadow Parts spec would let you expose your component’s internal parts for styling. If you had a commit button in the shadow of your reusable-component
, you could expose it as a part:
<button part="commit-button">...</button>
And someone can then write:
reusable-component::part(commit-button) {
background: pink;
border: 1px solid red;
border-radius: 5px;
}
to style the button. That’s a more convenient way to achieve the same thing as CSS Custom Properties. There’s less to document for your users and more flexibility. Your users can apply whatever styling they want without you needing to anticipate everything they might want to do.
There are some downsides, though, when it comes to reusing such a styled custom element.
Wrapping reusable custom elements + styling
One key advantage to giving your customer a custom element is that they end up with a single thing that produces a consistent result. But if your customer tries to style your reusable-component
, they’ll end up creating a new reuse problem for themselves. People at that company now have to deal with two separate things: 1) your original reusable-component
definition, and 2) a stylesheeet with the company’s styling for reusable-component
and its parts. On their own, those two entities are independent, with no explicit relationship. Having to track and apply them correctly creates maintenance headaches.
Your customer could define a new component-wrapper
element that wraps your original reusable-component
and applies the desired styling:
<template>
<style>
reusable-component::part(commit-button) {
background: pink;
border: 1px solid red;
border-radius: 5px;
}
</style>
<reusable-component>
<slot></slot>
</reusable-component>
</template>
Then your customer can distribute this component-wrapper
component internally, and everyone gets both your internal reusable-component
and the correct styling in a nice package. (Even if reusable-component
is actually defined elsewhere, component-wrapper
can express that dependency, so the pieces are linked together.)
But this introduces new challenges:
- The wrapped component won’t expose the same programmatic API as your original. Your customer will need to carefully reflect the inner component’s API, which may be non-trivial.
- Your
reusable-component
may have styling that’s contingent upon CSS classes or attributes applied to the host element. Unfortunately, that host element is now sitting inside the shadow of the customer’s component-wrapper
. Again, the customer writing component-wrapper
may have to carefully reflect any classes or attributes to the inner reusable-component
. Even if they do that correctly, that may still end up with unexpected behavior.
- The inner
reusable-component
needs to be styled to fill the host component-wrapper
, so that if the latter is stretched, the former will be stretched to fit. That’s not hard, but could easily be forgotten.
- Certain layout differences will arise. If someone applies
padding
to the component-wrapper
, that will apply to the wrapper, not within the inner reusable-component
as they may intend. The customer could expose the inner reusable-component
as a new part
to address that, but that introduces complexity and conceptual overhead.
- Both the element’s tag and class identity will change. A
querySelector
that looks for reusable-component
won’t match component-wrapper
. And an instanceof ReusableComponent
check will fail when applied to an instance of WrappedComponent
. The customer could potentially implement Symbol.hasInstance
on their class, but that’s getting complex. In general, it’ll be real work for your customer to create component-wrapper
as a drop-in replacement for your reusable-component
.
- Accessibility may be affected, as the wrapper may show up in the accessiblity tree unless measures are taken to avoid that. This can confuse things. My instinct would be to apply
role="none"
to the wrapper to keep it out of the accessibility tree. But if component-wrapper
is given a tabindex
, a screen reader might get confused when keyboard focus moves to the component.
Overall, without an easy repackaging mechanism for themed components, organizations may have difficulty adopting and styling components they acquire from elsewhere.
Overriding styles can be complex
It’s been our experience that even general-purpose components can end up with complex styling. People tend to approach component styling/theming as if the components were completely static, but components have dynamic state. State is often implicated in styling. As an example, a native button that’s disabled
shows different styling than an enabled button. If someone isn’t carefully considering the :disabled
pseudo-class in their button styling, they may end up applying an enabled button appearance to a disabled button.
Web components can have complex internal states, resulting in correspondingly complex internal stylesheets. Overriding such styles will be a delicate matter. To look at some concrete examples, I looked through some internal stylesheets in web components we’ve previously written. Here are some of the CSS selectors I found:
// From a carousel component
:host(.overlayArrows) .navigationButton:hover:not(:disabled) { ... }
// From a tabs component
:host([generic=""][tab-position="right"]) .tab:not(:last-child) { ... }
// From a toast component
:host([from-edge="bottom-right"].opened:not(.effect)) #overlayContent,
:host([from-edge="bottom-right"].effect.opening) #overlayContent { ... }
Each complex CSS selector in your component may create a challenge for your customer. If the tabs component above exposes an individual tab
as a part
, what about that :not(:last-child)
bit? If your customer writes styles that target the tab part, what styling should apply to the last tab?
In general, even if you can expose an interesting internal part of the component for outside styling, your customer will need to be aware of a large number of conditions that may apply. They could easily end up writing rules that don’t apply (because more specific conditions exist that take precedence) or apply when they shouldn’t (they write rules that are too general).
This is not to say that the CSS Shadow Parts spec won’t be a step forward — it will be — but rather to say that styling components with of normal complexity might turn out to be extremely challenging in practice.
(Aside: It goes without saying that, even if the browser vendors are excited about CSS Shadow Parts and the spec speeds through the standards process, it could still be a very long time until you can take advantage of them in all the browsers and devices you care about. And for what it’s worth, polyfilling new CSS syntax is notoriously difficult to do well.)
Inline styling
Your customer trying to use your reusable-component
might accomplish a certain degree of styling by applying inline styles to the component’s host element (the top-level <reusable-component>
instance sitting in the light DOM). Users of React and other FRP frameworks have found inline styling a powerful way to have a component apply styles to subelements. And more generally, inline styling is usually the easiest way to programmatically adjust an element’s appearance regardless of framework.
However, there are serious challenges using inline styles with web components. Inline styles can’t be used to style internal component parts. That will remain true even if and when the CSS Shadow Parts proposal is adopted, as that only addresses styling with stylesheets. And as noted above, components can have complex state. Writing inline styles for the host element that apply in all conditions is likely too blunt an instrument.
React and similar frameworks already struggle somewhat to deal with styling, but an increase in the presence of complex general-purpose components will make the issue more pressing.
Other options?
Given the above, we’re not sure that either CSS Custom Properties, CSS Shadow Parts, or inline styling will be sufficent. We think those platform features are really interesting — it’s just that they may not be enough for what we want to do. We want to create reusable web components that companies can easily brand for their applications, and we’re unsure how to deliver that.
We’re exploring alternative ideas for letting people style our web components, but are very interested in hearing how other people are tackling this problem. If you have ideas, please share.
November 6, 2017
I spent the last week on my least favorite engineering task: trying to get a body of code that works on Chrome/Safari/Firefox to work on Microsoft Edge and Microsoft Internet Explorer. In this case, I’ve been trying to get the Elix project’s unit tests and basic component set working as expected in Edge and IE 11. Such work is never fun. Lately I’ve been wondering whether it’s worth the Elix project’s time to support Microsoft’s browsers.
Internet Explorer 11
IE 11 is still supported by Microsoft, but as the mainstream browsers have accelerated away from it, working in IE feels increasingly anachronistic. Although many modern web technologies come with a polyfill or other means to accommodate IE 11, the set of workarounds required today has really piled up.
In the case of the Elix project, here’s the current set of things we need to do for IE 11:
- Transpile everything to ES5.
- Bundle everything into old school script files, since IE can’t handle modules. (While bundling is currently appropriate for production deployments in all browsers, we prefer to have our unit tests and demos run as native modules. Performance is not the primary consideration in those contexts, and we prefer to work directly against the real code. We test in Firefox with module support turned on, although production Firefox doesn’t yet support modules by default.)
- Maintain a build process in general. In modern browsers, all of Elix can run directly as is.
- Load a Shadow DOM polyfill.
- Load a Custom Elements polyfill.
- Load a
Promise
polyfill.
- Load a runtime that lets us use transpiled
async
functions.
- Load a
Symbol
polyfill.
- Load a polyfill for
Object.assign
.
- Incorporate many, many workarounds for deficiencies or quirks in IE. Did you know that IE’s
classList
object has a toggle
method that does not support the standard second argument? We know that now, and have had to work around that.
Everything we’re forced to add to the above list moves us further and further away from the metal. When we hit a bug, it’s really hard to be confident about where the bug lies. Is it our code? Or somewhere in the list above?
I joked on Twitter that getting a modern web app to run on IE is possible, in the same way it’s possible to play Doom on a thermostat. In truth, the situation is worse. For all I know, a modern thermostat has better hardware than the 1993 PCs which Doom originally ran on. A better comparison might be that getting a modern web app to run on IE is like getting a modern game title like Horizon Zero Dawn to run on a thermostat.
Edge
Microsoft Edge 16 is much better than IE, but it’s still no picnic.
While Edge supports many modern web technologies, Microsoft still hasn’t begun implementing Shadow DOM and Custom Elements. And Edge still suffers from some of the same painful, glaring problems as its predecessor:
- Edge’s debugging tools are godawful. The debugger is slow. It hangs. It crashes. Its feature set is weak, weak, weak. It’s so flaky, there was a point I couldn’t get Elix’s unit tests to pass in Edge unless the debug tools were closed. Opening the Edge debug tools would introduce unit test failures. How’s that for a friendly developer experience?
- Edge’s release cadence is too slow. Today I isolated and filed an Edge flex box bug. Even if Microsoft fixes the bug immediately, we could be waiting for the better part of a year to see the fix widely available.
- Microsoft has offered no compelling vision of its own for the web. Interest in the Windows API as an application platform appears to be negligible and getting smaller. If that’s correct, you would think Microsoft would invest heavily in a web-focused future. But at this past summer’s Edge Developer Conference, Microsoft presentations were almost entirely focused on ideas introduced by Google long ago.
It’s not that Microsoft has forgotten about developers and how to cater to them. I’m continually impressed by the speed and quality of the work currently going into Visual Studio Code and TypeScript, for example. But when it comes to the developer experience in modern browsers, Edge is dead last.
Market share and inertia
In discussions about browser support, IE and Edge support are often presumed to be important. But given the current state of the market and some usage summaries, I’m not sure that makes sense.
Mobile browser usage exceeds desktop browser usage. On mobile, China’s UC Browser appears to have significantly more market share than IE and Edge on the desktop. Samsung Internet for Android likewise may have already passed Edge in market share, and may soon pass IE.
The cost to keep things working on IE steadily grows. Even when new web advances come with polyfills that run on IE, the combined weight of all that’s necessary to support IE is considerable. How much faster could your team go if it didn’t have to support IE? In the case of the Elix project, I’m guessing IE support soaks up 10% of our time, and 50% of our positive emotions.
And though Edge is Microsoft’s replacement for IE, it’s not clear Edge is on a path to any kind of interesting market position. In the 2 years Edge has been on the market, it’s made miniscule gains. As far as I can tell, everyone who can abandon IE has already moved to Chrome. And those Chrome users must be sticking with Chrome even when they upgrade to a Windows machine capable of running Edge.
In the global political order, the country of France retains one of 5 permanent seats on the U.N. Security Council solely for historical reasons, out of all proportion to its current importance. It feels like similar historical reasons may soon be the primary justification for Microsoft’s position on web app browser requirements lists. Microsoft acts as if it automatically deserves a seat at the table, but I question that. It’s reasonable to ask Microsoft: What are you doing, today, as a browser vendor, that makes you worth the time and energy you force developers to spend on you?
As someone who worked at Microsoft for many years, I hold no grudge against them. To the contrary, as an alum, I really want them to be successful. But if they’re going to be relevant as a browser vendor, they’re going to have to do a lot better. In the meantime, I’m wondering whether their browsers are worth the trouble.
October 30, 2017
Web components and Shadow DOM are practically synonymous, but even web components with a shadow subtree often need to render information into the light DOM. A component might need to:
- Trigger conditional styling by applying CSS classes or attributes to itself.
- Pass information to its light DOM children through CSS classes or attributes.
- Set ARIA attributes on itself and its children.
Example: ARIA support
Suppose you’re creating a single-selection list component, and want to follow the
ARIA best practices for list boxes. Perhaps you use your favorite web component library to create a shadow root for your component and clone a template into it. Your component’s shadow might include, among other things, styling for your list container and its light DOM items:
<template>
<style>
:host {
/* Host element styling goes here */
}
::slotted(*) {
/* General list item styling goes here */
}
::slotted([aria-selected="true"]) {
/* Styling for the selected list item goes here */
}
</style>
<slot></slot>
</template>
Maybe that’s all that’s happening on the Shadow DOM side of things. Your component will also need to do the following work in the light DOM:
- Set
role="listbox"
on the host element.
- If the list is horizontal, set
aria-orientation="horizontal"
.
- Set
role="option"
on all items in the list. Be careful not to mark any
auxiliary content like style
as items in the list!
- Set
aria-selected="true"
on the selected item. (For what it’s worth: I’ve encountered at least one web component
bug
where the NDVA
screen reader required aria-selected="false"
to be set on all other elements, even for a single selection list.)
- Set the host’s
aria-activedescendant
attribute to be the id
of the currently-selected item. If the page author hasn’t supplied id
attributes for every item, you will need to generate and assign an id
for those items.
That’s a lot of work going on in the light DOM! These updates to the light DOM may surprise a page author if they include your list component in markup:
<accessible-list aria-label="Fruits" tabindex="0">
<div>Apple</div>
<div>Banana</div>
<div>Cherry</div>
</accessible-list>
At runtime, when this component updates the light DOM, the result might be:
<accessible-list aria-label="Fruits" tabindex="0" role="listbox"
aria-activedescendant="_option0">
<div role="option" id="_option0" aria-selected="true">
Apple
</div>
<div role="option" id="_option1" aria-selected="false">
Banana
</div>
<div role="option" id="_option2" aria-selected="false">
Cherry
</div>
</accessible-list>
All this ARIA work is happening in the light DOM, not the Shadow DOM. Work is underway on a better accessibility API, but ARIA attributes are the only solution for the foreseeable future. And as outlined above, your component might have other reasons to update the light DOM.
Generally speaking, you’ll need to write the code to update the light DOM yourself. Most web component frameworks to date have focused on updating Shadow DOM, not light DOM.
Conclusion
What goes on in a component’s shadow may only be half the picture — a substantial amount of work may be going on in the light DOM. That’s an important point to consider when you’re deciding how you want to write your component. Most component frameworks are focused on rendering Shadow DOM, so you’ll need to understand what light DOM updates are appropriate and make them yourself.
Code to handle such cases can be complex. For that reason, the Elix project tries to identify common scenarios for updating light DOM and address those with mixins like AriaListMixin.
October 23, 2017
Perhaps you like the benefits of functional reactive programming, but would
like to create native web components with minimal overhead. This post explores
a relatively simple JavaScript mixin that lets you author web components in a
functional reactive programming (FRP) style modeled after React. This mixin
focuses exclusively on managing state and determining when the state should be
rendered. You can use this mixin with whatever DOM rendering technology you
like: virtual-dom, hyperHTML, lit-html, plain old DOM API calls, etc.
I spent several months this summer writing React and Preact components, and
the values of an FRP model were immediately clear. As React advocates claim,
using FRP does indeed make state easier to reason about and debug, code
cleaner, and tests easier to write.
I wanted to bring these reactive benefits to the
Elix web components
project which Component Kitchen leads. Elix already uses
functional mixins
extensively for all aspects of component functionality for everything from
accessibility to touch gestures. I wanted to see if it were possible to
isolate the core of an FRP architecture into a functional mixin that could be
applied directly to HTMLElement to create a reactive web component.
You can use the resulting
ReactiveMixin
to create native web components in a functional reactive style. FRP frameworks
often use a canonical increment/decrement component as an example. The
ReactiveMixin version looks like this:
import ReactiveMixin from '.../ReactiveMixin.js';
// Create a native web component with reactive behavior.
class IncrementDecrement extends ReactiveMixin(HTMLElement) {
// This property becomes the initial value of this.state at constructor time.
get defaultState() {
return { value: 0 };
}
// Provide a public property that gets/sets state.
get value() {
return this.state.value;
}
set value(value) {
this.setState({ value });
}
// Expose "value" as an attribute.
attributeChangedCallback(attributeName, oldValue, newValue) {
if (attributeName === 'value') {
this.value = parseInt(newValue);
}
}
static get observedAttributes() {
return ['value'];
}
// … Plus rendering code, with several options for rendering engine
}
customElements.define('increment-decrement', IncrementDecrement);
Live demo
You end up with something that’s very similar to React’s Component class (or,
more specifically, PureComponent), but is a native HTML web component. The
compact mixin provides a small core of features that enable reactive web
component development in a flexible way.
Defining state
ReactiveMixin gives the component a member called this.state
, a
dictionary object with all state defined by the component and any of its other
mixins. The state
member, which is read-only and immutable, can
be referenced during rendering, and to provide backing for public properties
like the value
getter above.
ReactiveMixin provides a setState
method the component invokes to
update its own state. The mixin sets the initial state in the constructor by
passing the value of the defaultState
property to
setState
.
Detecting state changes
When you call setState
, ReactiveMixin updates the component’s
state, and then invokes a shouldComponentUpdate
method to
determine whether the component should be rerendered.
The default implementation of shouldComponentUpdate
method
performs a shallow check on the state properties: if any top-level state
properties have changed identity or value, the component is considered dirty,
prompting a rerender. This is comparable to the similar behavior in
React.PureComponent
. In our explorations, we have found that our
web components tend to have shallow state, so pure components are a natural
fit. You can override this to provide a looser dirty check (like
React.Component
) or a tighter one (to optimize performance, or
handle components with deep state objects).
If there are changes and the component is in the DOM, the new state
will be rendered.
Rendering
This mixin stays intentionally independent of the way you want to render state
to the DOM. Instead, the mixin invokes an internal component method whenever
your component should render, and that method can invoke whatever DOM updating
technique you like. This could be a virtual DOM engine, or you could just do
it with plain DOM API calls.
Here’s a plain DOM API render implementation for the increment/decrement
example above. We’ll start with a template:
<template id="template">
<button id="decrement">-</button>
<span id="value"></span>
<button id="increment">+</button>
</template>
To the component code above, we’ll add an internal render method for
ReactiveMixin to invoke. The mixin uses a Symbol
object to
identify the internal render method. This avoids name collisions, and
discourages someone from trying to invoke the render method from the outside.
(The render method can become a private method when JavaScript supports
those.)
import symbols from ‘.../symbols.js’;
// This goes in the IncrementDecrement class ...
[symbols.render]() {
if (!this.shadowRoot) {
// On our first render, clone the template into a shadow root.
const root = this.attachShadow({ mode: 'open' });
const clone = document.importNode(template.content, true);
root.appendChild(clone);
// Wire up event handlers too.
root.querySelector('#decrement').addEventListener('click', () => {
this.value--;
});
root.querySelector('#increment').addEventListener('click', () => {
this.value++;
});
}
// Render the state into the shadow.
this.shadowRoot.querySelector('#value').textContent = this.state.value;
}
That’s all that’s necessary. The last line is the core bit that will update
the DOM every time the state changes. The two buttons update state by setting
the value
property, which in turn calls setState
.
This ReactiveMixin would also be a natural fit with template literal libraries
like
lit-html or
hyperHTML. That
could look like:
import { html, render } from ‘.../lit-html.js’;
import symbols from ‘.../symbols.js’;
// Render using an HTML template literal.
[symbols.render]() {
if (!this.shadowRoot) {
this.attachShadow({ mode: 'open' });
}
const template = html`
<button on-click=${() => this.value-- }>-</button>
<span>${this.state.value}</span>
<button on-click=${() => this.value++ }>+</button>
`;
render(template, this.shadowRoot);
}
The creation of the shadow root and the invocation of the rendering engine are
boilerplate you could factor into a separate mixin to complement
ReactiveMixin.
Web component and FRP lifecycle methods
Since components created with this mixin are still regular web components,
they receive all the standard lifecycle methods. ReactiveMixin augments
connectedCallback
so that a component will be rendered when it’s
first added to the DOM.
The mixin provides React-style lifecycle methods for
componentDidMount
(invoked when the component has finished rendering for the first time) and
componentDidUpdate
(whenever the component has completed a
subsequent rerender). The mixin doesn’t provide
componentWillUnmount
; use the standard
disconnectedCallback
instead. Similarly, use the standard
attributeChangedCallback
instead of
componentWillReceiveProps
.
Conclusion
This ReactiveMixin gives us much of what we like about React, but lets us
write web components which are closer to the metal. All it does is help us
manage a component’s state, then tell our component when it needs to render
that state to the DOM. Separating state management from rendering is useful —
we’ve already changed our minds about which rendering engine to use several
times, but those changes entailed only minimal updates to our components.
The coding experience feels similar to React’s, although I don’t see a need to
make the experience identical. For example, I thought setState would work well
as an `async` Promise-returning function so that you can wait for a new state
to be applied. And it’s nice to avoid all the platform-obscuring abstractions
(e.g., synthetic events) React pushes on you.
We’re using this ReactiveMixin to rewrite the Elix components in a functional
reactive style. That work is proceeding fairly smoothly, and we’re moving
towards an initial 1.0 release of Elix that uses this approach in the near
future. In the meantime, if you’d like to play with using this mixin to create
web components, give it a try and let us know how it goes.