Posted on • Originally published atblog.ltgt.net
The benefits of Web Component Libraries
Web component browser APIs aren't that many, and not that hard to grasp (if you don't know about them, have a look at Google'sLearn HTML section and MDN'sWeb Components guide);
but creating a web component actually requirestaking care of many small things.
This is where web component libraries come in very handy, freeing us of having to think about some of those things by taking care of them for us.
Most of the things I'll mention here are handled one way of another by other libraries (GitHub'sCatalyst,Haunted,Hybrids, Salesforce'sLWC,Slim.JS, Ionic'sStencil) but I'll focus on Google'sLit and Microsoft'sFAST here as they probably are the most used web component libraries out there (ok,I lied, Lit definitely is, FAST not that much, far behind Lit and Stencil; but Lit and FAST have many things in common, starting with the fact that they arejust native web components, contrary to Stencil thatcompiles to a web component).
Both Lit and FAST leverage TypeScript decorators to simplify the code even further so I'll use that in examples,
even though they can also be used in pure JS (decorators are coming to JS soon BTW). I'll also leave the most apparent yet most complex aspect for the end.
Let's dive in!
Registration
It's a small detail, and I wouldn't even consider it abenefit;
it's mostly syntactic sugar, but with FAST at least it also does a few other things that we'll see later: registering the custom element.
In vanilla JS, it goes like this:
classMyElementextendsHTMLElement{}customElements.define('my-element',MyElement);
With Lit:
@customElement('my-element')classMyElementextendsLitElement{}
And with FAST:
@customElement({name:'my-element',// other properties will come here later})classMyElementextendsFASTElement{}
Attributes and Properties
With native HTML elements, we're used to accessing attribute (also known ascontent attributes in the HTML spec) values as properties (akaIDL attributes) on the DOM element (thinkid
,name
,checked
,disabled
,tabIndex
, etc. evenclassName
andhtmlFor
although they use sligthly different names to avoid conflicting with JS keywords) with sometimes some specificities: thevalue
attribute of<input>
elements is accessible through thedefaultValue
property, thevalue
property giving access to the actual value of the element (along withvalueAsDate
andvalueAsNumber
that additionally convert the value).
Custom elements have to implement this themselves if they want it, and web component libraries make it a breeze.
They help usreflect properties to attributes when they are modified (if that's what we want; and all while avoiding infinite loops), convert attribute values (always strings) to/from property values, or handleboolean attributes (where we're only interested in their absence or presence, not their actual value: thinkchecked
anddisabled
).
Let's compare some of these cases, first without library:
classMyElementextendsHTMLElement{// Attributes have to be explicitly observedstaticgetobservedAttributes(){return['reflected-converted','reflected','non-reflected','bool'];}#reflectedConverted;// In a real element, you'd probably use a getter and setter// to somehow update the element when the property is set.nonReflected;attributeChangedCallback(name,oldValue,newValue){switch(name){case'reflected-converted':// Convert the attribute valuethis.reflectedConverted=newValue!=null?Number(newValue):null;break;case'non-reflected':this.nonReflected=newValue;break;// other attributes handled in accessors below}}getreflectedConverted(){returnthis.#reflectedConverted;}setreflectedConverted(newValue){// avoid infinite loop with attributeChangedCallbackif(newValue!==this.#reflectedConverted){this.#reflectedConverted=newValue;if(newValue==null){this.removeAttribute('reflected-converted');}else{// Here we let the browser automatically convert to stringthis.setAttribute('reflected-converted',newValue);}}}getreflected(){returnthis.getAttribute('reflected');}setreflected(newValue){if(newValue==null){this.removeAttribute('reflected');}else{this.setAttribute('reflected',newValue);}}getbool(){returnthis.hasAttribute('bool');}setbool(newValue){this.toggleAttribute('bool',newValue);}}
Now with Lit:
classMyElementextendsLitElement{@property({attribute:'reflected-converted',type:Number,reflect:true})reflectedConverted?:number;@property({reflect:true})reflected?:string;@property({attribute:'non-reflected'})nonReflected?:string;@property({type:Boolean})bool:boolean=false;}
And with FAST:
classMyElementextendsFASTElement{@attr({attribute:'reflected-converted',converter:nullableNumberConverter})reflectedConverted?:number;@attrreflected?:string;@attr({attribute'non-reflected',mode:'fromView'})nonReflected?:string;@attr({mode:'boolean'})bool:boolean=false;}
Early-initialized properties
Another thing with properties is that they could be set on a DOM element even before it'supgraded:
the script that defines the custom element does not need to be loaded by the time the browser parses the custom tag in the HTML,
and some script might access that element in the DOM before the script thatdefines it has loaded;
only then will the element beupgraded: the class instantiated to take control of the custom element.
When that happens, you wouldn't want properties that would have been set on the element earlier to be overwritten by the upgrade process.
Without a library, you would have to take care of it yourself with code similar to the following:
classMyElementextendsHTMLElement{constructor(){super();// "upgrade" propertiesfor(constpropNameof['reflectedConverted','reflected','nonReflected','bool']){if(this.hasOwnProperty(propName)){letvalue=this[propName];deletethis[propName];this[propName]=value;}}}}
Again, libraries do that for you, automatically, based on the previously seen declarations.
Responding to property changes
The common way to respond to property changes is to implement a setter. This requires also implementing a getter though, as well as storing the value in a private field. When changing the value fromattributeChangedCallback
, make sure to also use the setter and not assign directly to the backing field.
To respond to changes to thenonReflected
property in the above example, one would have to write it like so:
#nonReflected;getnonReflected(){returnthis.#nonReflected;}setnonReflected(newValue){this.#nonReflected=newValue;// respond to change here}
Both Lit and FAST provide their own way of doing this, though most of the time this is not really needed given that most reaction to change is to update the shadow tree, and Lit and FAST have their own ways of doing this (see below for more about rendering).
With Lit, you listen to changes to any property and have to tell them apart by name, a bit similar toattributeChangedCallback
butbatched for several properties at a time:
@property({attribute:'non-reflected'})nonReflected?:string;// You could also use updated(changedProperties), depending on your needswillUpdate(changedProperties:PropertyValues<this>){if(changedProperties.has("nonReflected")){// respond to change here}}
With FAST, you can implement a method with aChanged
suffix appended to the property name:
@attr({attribute'non-reflected',mode:'fromView'})nonReflected?:string;nonReflectedChanged(oldValue?:string,newValue?:string){// respond to change here}
Shadow DOM and CSS stylesheets
The most efficient way to manage CSS stylesheets in Shadow DOM is to use so-calledconstructable stylesheets: construct aCSSStyleSheet
instance once (or soonimport
a CSS file), then reuse it in each element's shadow tree throughadoptedStyleSheets
:
constsheet=newCSSStyleSheet();sheet.replaceSync(` :host { display: block; } :host([hidden]) { display: none; }`);classMyElementextendsHTMLElement{constructor(){super();this.attachShadow({mode:'open'});this.shadowRoot.adoptedStyleSheets=[sheet];}}
With Lit, you'd rather use this moredeclarative syntax:
classMyElementextendsHTMLElement{staticstyles=css` :host { display: block; } :host([hidden]) { display: none; } `;}
and similarly with FAST:
conststyles=css` :host { display: block; } :host([hidden]) { display: none; }`;@customElement({name:'my-element',styles,})classMyElementextendsFASTElement{}
Constructable stylesheets currently still require a polyfill in Safari though (this is being added in Safari 16.4), but both Lit and FAST take care of this for you.
Rendering and templating
The most efficient way to populate the shadow tree of a custom element is by cloning atemplate that has been initialized once.
That template could be any document fragment but the<template>
element was made specifically for these use-cases.
You would then retrieve nodes inside the shadow tree to add event listeners and/or manipulate it in response to those inside events or to attribute and property changes (see above) from the outside.
consttemplate=document.createElement('template');template.innerHTML=` <button>Add</button> <output></output>`;classMyElementextendsHTMLElement{#output;constructor(){super();// … (upgrade properties as seen above) …this.attachShadow({model:'open'});this.shadowRoot.append(template.content.cloneNode(true));// Using an <output> element makes it easier// We could also create a text node and append it ourselvesthis.#output=this.shadowRoot.querySelector('output');this.shadowRoot.querySelector('button').addEventListener('click',()=>this.count++);this.#output.value=this.count;}// … (count property, with attribute changed callback and converter) …setcount(value){// … (reflect to attribute or whatever) …this.#output.value=value;}}customElements.define('my-element',MyElement);
Things get much more complex when you want to conditionally render some subtrees (the easiest probably being to toggle theirhidden
attribute), or render and update a list of elements.
This is where Lit and FAST (and a bunch of other libraries) work much differently from the above, introducing the concept ofreactive orobservable properties and arender lifecycle based on a specific syntax for templates allowing placeholders for dynamic values, a syntax to register event listeners right from the template, and composability.
With Lit, that could look like:
@customElement('my-element')classMyElementextendsLitElement{@propertycount:number=0;render(){// No need for an <output> element here, though we could// (and it would possibly even be better for accessibility)returnhtml` <button @click=${this.#increment}>Add</button>${this.count} `;}#increment(){this.count++;}}
and with FAST:
// No need for an <output> element here, though we could// (and it would possibly even be better for accessibility)consttemplate=html<MyElement>` <button @click=${x=>x.count++}>Add</button>${x=>x.count}`;@customElement({name:'my-element',template,})classMyElementextendsFASTElement{@attr({converter:numberConverter})count:number=0;}
The way Lit and FAST work is by observing changes to the properties and scheduling an update everytime it happens.
With Lit, the update (also calledrerender) will call therender()
method of the component and then process the template. The rerender is scheduled using amicrotask such that it canbatch changes to multiple properties into a single rerender.
With FAST, the update is instead scheduled usingrequestAnimationFrame
(achieving the samebatching as Lit) and will call every lambda of the template that needs to be: FAST tracks which dynamic part uses which properties to only reevaluate those parts when a given property changes.
In the examples above, any change to thecount
property, either from the outside or in response to the click of the button, schedules a update.
And in FAST's case, only thex => x.count
lambda is called and the corresponding DOM node updated.
In Lit's case, the button's click listener would also be evaluated, but determined to be the same as before so no change would be performed.
Thehtml
tagged template literal (both in Lit and FAST, also in other libraries such as@github/jtml
) will use a<template>
under the hood to parse the HTML once and reuse it later, just like thecss
seen earlier uses a constructable stylesheet. It puts markers in the HTML (special comments, elements or attributes) in place of the dynamic parts so it can find them back to attach event listeners and inject values, making it possible tosurgically update only the nodes that need it, without touching anything else (FAST actually being even moresurgical by tracking the properties used in each dynamic part).
With Lit'srender()
method returning such atemplate and called each time a property or internal state changed, its programming model looks a bit like React, rerendering and returning a new JSX each time a prop or state changed;
while FAST's approach looks a bit more like Angular (or Vue or Svelte) where each component is associated with a single template at definition time.
Other niceties
Lit and FAST also provide some helpers to peek into the shadow tree: to get a direct reference to some node, or the<slot>
s' assigned nodes.
Lit also pioneersreactive controllers that allow code reuse between components through composition, where the controller canhook into the render lifecycle (i.e. trigger a rerender of the component that uses the controller).
The goal is ultimately to make themreusable across frameworks too.
Some have already embraced it, like Haunted withitsuseController()
that allows using reactive controllers in Haunted components, orApollo Elements that's built around reactive controllers. Lit also provides auseController()
React hook as part of its@lit-labs/react
package (that also makes it easier to use a Lit component in a React application by wrapping it as a React component), andthere are prototypes for several frameworks such as Angular or Svelte, or even native web componentsthrough a mixin.
FAST developers are interested in supporting reactive controllers too, but those currently don't quite match with the way FAST updates the rendering.
The Lit team provides reactive controllers tomanage contextual values, easilywire asynchronous tasks to rendering,wrap a bunch of native observers (mutation, resize, intersection, performance), orhandle routing. Others are embracing them too: Apollo Elements for GraphQL already cited above; James Garbutt hasa collection of utilities for Lit, many of them being reactive controllers usable outside Lit;Nano Stores provide reactive controllers; Guillem Cordoba hascontrollers for Svelte Stores; etc.
Conclusion
Web component libraries are really helpful to streamline the development of your components.
While you could develop custom elements without library, chances are you'd be eventually creating your own set of helpers, reinventing the wheel (there aremore than enough ways to build web components already).
Understand what each library brings and pick one.
Top comments(4)

A read worth the time, excellent and thank you.
How do you see the relation between J2CL and web-components in the future? or should we consider J2CL for business logic only and not for UI logic? do we expect the possibility to be able to optimize web-components?

- LocationDijon, France
- Joined
I wouldn't use Java to create web components (YMMV). To consume components, generate JsInterop interfaces (maybe even widgets) from acustom element manifest, or a.d.ts
.

Makes sense, but I would love to learn about why not?

- LocationDijon, France
- Joined
I don't think there's any web component library that's (directly) usable from J2CL, so you'd first have to make one. It might be possible to use Lit or FAST from J2CL but you'd likely want to first create some tooling to make the calls to tagged templates more readable; e.g. from an interface similar toSafeHtmlTemplate
:
interfaceMyTemplateextendsLitHtmlTemplates{@Template("<span class=\"{3}\">{0}: <a href=\"{1}\">{2}</a></span>")TemplateResultmessageWithLink(TemplateResultmessage,SafeUriurl,StringlinkText,Stringstyle);}
to
staticfinalString[]STRINGS={"<span class=\"","\">",": <a href=\"","\">","</a></span>"};// …publicTemplateResultmessageWithLink(TemplateResultmessage,SafeUriurl,StringlinkText,Stringstyle){returnLit.html(STRINGS,style,message,url.asString(),linkText);}
assuming a definition like:
@JsMethodpublicnativeTemplateResulthtml(String[]strings,Object...values);
But I don't see much use for J2CL or GWT nowadays (J2CL might be useful for sharing logic with the backend or a native app, the same way Google uses it, but not really for building UI). I mean, even Vaadin is pivoting with Hilla. That's my personal opinion though, and I won't try to push it through anyone's throat. Feel free to disagree, but I'm not interested in arguing about any of it.
For further actions, you may consider blocking this person and/orreporting abuse