Web Component Libraries
# Web Component Libraries
There are a number of libraries (over 60 actually) that provide prebuilt web components. Some of these are libraries of components that were created by companies for their own online use but were then exposed for public use.
The use of Web Components has grown over the last couple years. In fact, between May 2021 and July 2021 there was a jump from 10% to 18% of page loads in Chrome using Web Components. A big chunk of that sudden growth was actually Wordle. It uses Web Components and its sudden popularity meant a sudden rise in the use of Web Components on total page loads.
Full cross-browser support was not reached until 2020. Support for Web Components within existing Web Development Frameworks can be found here (opens new window). React and
Vue are outliers that do not currently have 100% support for some of the advanced features of Web Components but you can expect that to change soon with version 19 of React.
Here are some of the existing Web Component libraries that you can use in your web sites Lit (opens new window), Haunted (opens new window),
'Hybrids JS' (opens new window), Stencil JS (opens new window), and more. This site
has a good list and feature/size comparison (opens new window).
# Lit
We will focus on using Lit this semester. The full documentation for Lit can be found on the Lit website (opens new window).
Lit also has an in-browser playground (opens new window) that you can use to experiment with the framework.
Lit uses the class syntax to create the components but has a much smaller, more efficient syntax. The LitElement class that we extend will do things for us like automatically keep attributes and
properties in-sync.
The css tagged template literal function will allow us to define the styles which will automatically be scoped to our own component.
The html tagged template literal function will help us create the HTML template for our component.
To create a web component using Lit, you need to include the Lit JavaScript in your project. Then load your component as a module.
Here is the sample provided on the Lit website as the starter version.
import { LitElement, html, css } from 'https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js';
export class SimpleGreeting extends LitElement {
static styles = css`
p {
color: blue;
}
`;
static properties = {
name: { type: String },
};
constructor() {
super();
this.name = 'Somebody';
}
render() {
return html`<p>Hello, ${this.name}!</p>`;
}
}
customElements.define('simple-greeting', SimpleGreeting);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Since you have already created your own custom web component, a lot of this should look familiar.
There is a class that defines the component, and that class is referenced inside the customElements.define() method along with the name of the custom web component simple-greeting.
The class extends LitElement instead of HTMLElement. Inside the Lit scripts, the LitElement class is extending HTMLElement.
The render() method creates and returns the HTML template that gets used each time a <simple-greeting> is added to your webpage. You can put as much HTML inside template string as you want. And
you can embed any variables or values in there too.
For the attributes and properties, there is a static member called properties. The properties object contains a single property for each attribute|property that you want your component to have.
You even get to define the basic datatype for the property.
There is also a static styles member. This is where you define the styles that will apply to your component. You can add as many styles inside the template string as you want.
# Installing Lit
If you want to add Lit to your project, there are a couple approaches you can use.
First, you can use npm to install Lit. In the Terminal, run the following command.
npm i lit
This will create a node_modules folder, if needed, then add the Lit package to the node_modules folder. If you have previously run the npm init command then it will also add Lit as a
dependency in your package.json project settings file.
Once the package has been added to the project with npm, then you can import it into your main script like this:
import { LitElement, html } from 'lit';
If you are using the npm approach to adding Lit then you will need to have a process, like WebPack, for compiling your code into a final distributable version.
You can also copy the contents from the node_modules Lit package from that location into your own local js folder, if you like.
The second approach is to add the script file via a <script> tag or an import command.
The website JSDelivr.net is a CDN for many JavaScript libraries including Lit. Here is the page with the two options for Lit (opens new window).
The core version has the most common features included. The all version has all the core features plus everything else.
So, you could import the Lit script file like this:
import { LitElement, html } from 'https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js';
Or via a script tag like this:
<script type="module" src="https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js" />
Once you have it added to the site you can use it like you would any JavaScript file.
# Reactive Properties and Attributes
When you define the static properties object in your class you will be defining both the allowed attributes and setting up the properties for the component.
In the following example we are defining two keys inside properties. color is a String and message has no default type.
static properties = {
color: { attribute: false, type: String },
message: { attribute: true, },
};
2
3
4
Lit generates a getter/setter pair for each reactive property. When a reactive property changes, the component schedules an update.
By default, Lit sets up an observed attribute corresponding to the property, and updates the property when the attribute changes.
The allowed values for the type property are Boolean, String, Number, Object, or Array.
When you specify the attribute property and you mark it as true, it means that the developer who is using the component can set the value as either a property or as an attribute. If set to
false then it will only be available as a property.
You can provide a value through the attribute, like this:
<my-component message="static message created through an attribute"></my-component>
Or if you are creating a component through JavaScript then you can set it via the property approach. Both the attribute and property exist and will be kept in-sync.
let mc = document.createElement('my-component');
mc.setAttribute('message', 'new value'); // set through the attribute
mc.message = 'Dynamically created through JS property'; //set through the property version
2
3
Both the property and attribute will automatically be kept in sync with each other.
The reason that they properties are called Reactive is that changing their value will trigger the render method being called again and the element on the page will be updated. More below on the
render method.
Another kind of reactive property is a private internal state property. The name, by convention should start with an underscore. It will have the state property set to true inside the static
properties object.
static properties = {
_something: { state: true, type: Boolean },
}
2
3
This makes it a reactive property that will update the element by calling render if it gets changed. However, the only way to change it will be internally in the class. It is a private property and will not be accessible outside the class.
Initial and default values for any reactive property typically get set inside of the class constructor.
class MyElement extends LitElement {
constructor() {
super();
//set the default values for the static properties object.
this._something = false;
this._color = `#bada55`;
this._message = '';
}
}
2
3
4
5
6
7
8
9
# Listening for Property Changes
If you want to indicate when a property value changes, then you can add the hasChanged property and provide it with a function to run. The function will be passed the old value as well as the
current value. It can compare the two values or do anything else you want.
static properties = {
color: {
type: String,
hasChanged: (value, oldValue) => {
return value === oldValue;
},
}
}
2
3
4
5
6
7
8
If the hasChanged returns false then it tells the component that the value has not changed so a re-render is not required.
# More property options
Inside the static properties object, each of the keys get an object which can have the state, the type or the hasChanged property options, but there are more options too.
static properties = {
name: {
type:String, //defaults to String. For turning an attribute into a property
attribute: 'my-name', //if you want to call the attribute something different than the property name
hasChanged: (value, oldValue) => true, //control if a re-render is triggered
converter: {
fromAttribute: (value, type) => {
// `value` is a string
// Convert it to a value of type `type` and return it
},
toAttribute: (value, type) => {
// `value` is of type `type`
// Convert it to a string and return it
}
},
reflect: false, //if set to true property value is reflected back to the associated attribute.
state: true, //means it is an internal property with no matching attribute
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# The render Method
The render() method is required in your component class. It is responsible for building the thing that will be injected into the webpage.
The method can return one of the following things:
- a
TemplateResultobject, which is what gets return from thehtmltemplate literal function; - a DOM Node;
- a primitive String, Number, or Boolean;
- The sentinel values
nothingandnoChange; - Array or iterable of any of the other possible values.
The render method runs when the element gets added to the web page.
It also gets run when any of the values of the component's reactive properties get changed. This is like a checkmark appearing or disappearing inside a checkbox when the user clicks on the box. The user interacts with the component, a change is made to an internal property value, and then the checkbox gets re-rendered on the page.
# Events and Dispatching Events
Since you are building new web components and they are extending HTMLElement, your custom element will automatically have access to the standard mouse events like click.
If you want, you can listen for other events, or create your own custom events.
Let's start with this sample component.
class MyElement extends LitElement {
static properties = {
color: { attribute: false, type: String }, //no attribute just property
size: { attribute: true, type: Number }, //has an attribute and property
};
constructor() {
super();
this.size = 20; //default value
console.log(MyElement.observedAttributes); // ['size']
//observedAttributes is a static property because it applies to ALL <my-element>s
}
_handleClick() {
//private function
let howHappy = Math.floor(Math.random() * 10) + 1;
const options = {
detail: { howHappy },
bubbles: true,
composed: true,
};
let happyEvent = new CustomEvent('behappy', options);
this.dispatchEvent(happyEvent);
}
render() {
return html`<div>
<p style="font-size:${this.size}px;">Hello from MyElement!</p>
<p><button @click=${this._handleClick}>click me</button></p>
</div>`;
}
//when the user clicks the button, the _handleClick function gets called
//then it dispatches a 'behappy' event
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
The render function creates a div which will contain two paragraphs. The second paragraph shows a button that the user can click. When they click the button, the private function referenced by
@click= will be called.
The _handleClick function is creating a random number that gets store in the variable howHappy. This variable is saved as a property inside the detail object of a new custom event.
When you create a new custom event, you need to pass in a name for the event (which will be used in addEventListener calls) and an options object. The options object will include a property called
detail. The detail property can hold any information that you want about the event. In our example above, the options object will look something like this:
{
detail: { howHappy: 7 },
bubbles: true,
composed: true,
}
2
3
4
5
The last line of the function dispatches the event. This means that it tells the JavaScript engine to call any function that had attached an event listener for a behappy event on any instance of
<my-element>.
We could use it like this:
let me = document.createElement('my-element');
me.addEventListener('behappy', (ev) => {
console.log('The behappy event was trigger on <my-element>');
console.log(ev.detail.howHappy); //the randomly generated number
});
document.body.append(me);
2
3
4
5
6
If the user clicks on the <button> that was created by adding a <my-element> to the page, it calls the internal function which will then dispatch the behappy event. The behappy event triggers
our function above and writes the the console those two messages.
# Directives
Inside the render method, when you are creating the content for your component, there are a series of directives that you can use. Directives are functions that can extend Lit by customizing the way
an expression renders. Read more about the directives here (opens new window).
The directives include methods like when, map, repeat, join, and range which can largely be duplicated with plain JavaScript logical operators and array functions.
A couple other useful ones are cache and guard. The cache method will cache a copy of a rendered template (chunk of html) so it can quickly be re-rendered later, even if temporarily removed from
the screen.
The guard method takes two parameters - an array of dependencies and a function that runs only if one or more of those dependencies are updated.
The dependencies are static properties of the component. In the following example, the myprop property value is monitored and if it or otherprop changes, then the render method will be called
again. When the render method is called, the guard method checks to see if the myprop property specifically changes. If myprop has changed then the guard method's function is called and it
generates new content.
class MyElement extends LitElement {
static properties = {
myprop: { type: Array },
otherprop: { type: String },
};
constructor() {
super();
this.myprop = [1, 2, 3]; //starter value
}
render() {
return html` <div>
<p>The Average value is:</p>
<p>${guard([this.myprop], () => this.myprop.reduce((acc, current) => acc + current, 0) / this.myprop.length)}</p>
</div>`;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# More Lit Resources
This website is the accompanying website tutorial for a Frontend Masters course on building web components and using Lit.
https://htmlwithsuperpowers.netlify.app/get-started/ (opens new window)
# Tagged Template Literals
One of the features that you will see used in the Lit examples are the tagged template literals - css and html.
A template literal is a String created in JavaScript with the backtick character instead of the single or double quotes. If you use the template literal version of the String then you can embed
variables inside the string wrapped inside this ${ }.
A Tagged template literal is a function that accepts a template literal as the input parameter for the function. They have a unique syntax. The name of the function you are calling comes first, and it is immediately followed by the template literal string.
myfunc`this is the template literal ${somevar} string with an embedded ${anothervar} variable.`;
The function needs to actually exist in your code somewhere, or in an imported script.
When the template string is passed to the function, it is actually being split into different parts. The hard-coded string and the variables that are embedded are all passed separately.
function myfunc(strings, varone, vartwo) {
//strings is an array
//varone and vartwo are the individual variables
}
2
3
4
The hard-coded strings from the template literal are gathered and passed in as a single array.
The variables are each passed separately as individual variables. The problem with that is that you might not know how many variables that are embedded in the template literal.
To solve that problem we can use the rest operator .... This will allow us to group ALL the variables in a single array.
function myfunc(strings, ...vars) {
//strings is an array holding all the hard-coded strings
//vars is an array holding all the interpolated variables
}
2
3
4
The same functionality that Lit uses with the css, svg, and html tagged template literals is used in styling systems for frameworks like React.
Styled Components (opens new window) uses a lot of tagged template literals in its functionality.