Last Updated: 2020-10-28
Web Components
Web Components are a set of web platform APIs that allow you to create new custom, reusable, encapsulated HTML tags to use in web pages and web apps. Custom components and widgets built on the Web Component standards will work across modern browsers and can be used with any JavaScript library or framework that works with HTML.
LitElement
LitElement is a simple base class for creating fast, lightweight Web Components that work in any web page with any framework.
LitElement provides APIs to simplify common Web Components tasks like managing properties, attributes, and rendering.
What you'll learn
- What is a Web Component
- The concepts of Web Components
- How to build a Web Component
- What are lit-html and LitElement
- How to build a LitElement component
What you'll build
- A vanilla thumbs up / down Web Component
- A thumbs up / down LitElement-based Web Component
What you'll need
- Any updated modern browser (Chrome, Safari, Firefox, Chromium Edge). Web Components work in all modern browsers and polyfills are available for Microsoft Internet Explorer 11 and non-chromium Microsoft Edge.
- Knowledge of HTML, CSS, JavaScript, and Chrome DevTools.
Accessing the Code
You can access the code here on glitch.com, Glitch is a website that provides an online web coding environment.
Exploring the Glitch UI
In the above Glitch UI screenshot you highlight the sections that are likely to be important for your development. The Section 1, on the top, "Show" button will allow you to preview the output. The above screenshot is using the "Next to The Code" option. Section 2, on the left, is the file explorer, section 3, in the center is the file editor, section 4, on the right, is the code preview, and section 5 is the reload button.
When you see the text This page has successfully loaded! Then your glitch instance has finished installing dependencies and the server has started.
Understanding the Project
In this project you have an index.html
file:
index.html
<!DOCTYPE html>
<html>
<head>
<script src="./index.js" type="module"></script>
</head>
<body>
</body>
</html>
This file only does one thing: it imports an index.js
file as an ES module.
index.js
document.body.innerHTML = '<div>This page has successfully loaded!</div>';
This file has some placeholder code to let you know that the glitch instance is ready.
Your Glitch project also includes an images
directory with thumbs up and down SVG files.
Finally, there is the package.json
file used by npm to install dependencies. The project has only three dependencies installed which are: lit-element
, lit-html
, and es-dev-server
which is a simple web server that will resolve bare module specifiers in javascript files.
Custom Elements
Web Components are a collection of 4 native web APIs. They are:
- ES Modules
- Custom Elements
- Shadow DOM
- HTML Templates
You've already used the ES modules specification, which allows you to create javascript modules with imports and exports that are loaded into the page with <script type="module">
.
Defining a Custom Element
The Custom Elements specification lets users define their own HTML elements using JavaScript. The names must contain a hyphen (-
) to differentiate them from native browser elements. Clear the index.js
file and define a custom element class:
index.js
class RatingElement extends HTMLElement {}
customElements.define('rating-element', RatingElement);
A custom element is defined by associating a class that extends HTMLElement
with a hyphenated tag name. The call to customElements.define
tells the browser to associate the class RatingElement
with the tagName ‘rating-elem
ent'. This means that every element in your document with the name <rating-element>
will be associated with this class.
Place a <rating-element>
in the document body and see what renders.
index.html
<body>
<rating-element></rating-element>
</body>
Now, looking at the output, you'll see that nothing has rendered. This is expected, because you haven't told the browser how to render <rating-element>
. You can confirm that the Custom Element definition has succeeded by selecting the <rating-element>
in Chrome Dev Tools' element selector and, in the console, calling:
$0.constructor
Which should output:
class RatingElement extends HTMLElement {}
Custom Element Lifecycle
Custom Elements come with a set of lifecycle hooks. They are:
constructor
connectedCallback
disconnectedCallback
attributeChangedCallback
adoptedCallback
The constructor
is called when the element is first created: for example, by calling document.createElement(‘rating-eleme
nt') or new RatingElement()
. The constructor is a good place to set up your element, but it is typically considered bad practice to do DOM manipulations in the constructor for element "boot-up" performance reasons.
The connectedCallback
is called when the custom element is attached to the DOM. This is typically where initial DOM manipulations happen.
The disconnectedCallback
is called after the custom element is removed from the DOM.
The attributeChangedCallback(attrName, oldValue, newValue)
is called when any of the user-specified attributes change.
The adoptedCallback
is called when the custom element is adopted from another documentFragment
into the main document via adoptNode
such as in HTMLTemplateElement
.
Render DOM
Now, return to the custom element and associate some DOM with it. Set the element's content when it gets attached to the DOM:
index.js
class RatingElement extends HTMLElement {
constructor() {
super();
this.rating = 0;
}
connectedCallback() {
this.innerHTML = `
<style>
rating-element {
display: inline-flex;
align-items: center;
}
rating-element button {
background: transparent;
border: none;
cursor: pointer;
}
</style>
<button class="thumb_down" >
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>
</button>
<span class="rating">${this.rating}</span>
<button class="thumb_up">
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg>
</button>
`;
}
}
customElements.define('rating-element', RatingElement);
In the constructor
, you store an instance property called rating
on the element. In the connectedCallback
, you add DOM children to <rating-element>
to display the current rating, together with thumbs up and thumbs down buttons.
Why Shadow DOM?
In the previous step, you'll notice that the selectors in the style tag that you inserted select any rating element on the page as well as any button. This may result in the styles leaking out of the element and selecting other nodes that you may not intend to style. Additionally, other styles outside of this custom element may unintentionally style the nodes inside your custom element. For example, try putting a style tag in the head of the main document:
index.html
<!DOCTYPE html>
<html>
<head>
<script src="./index.js" type="module"></script>
<style>
span {
border: 1px solid red;
}
</style>
</head>
<body>
<rating-element></rating-element>
</body>
</html>
Your output should have a red border box around the span for the rating. This is a trivial case, but the lack of DOM encapsulation may result in larger problems for more complex applications. This is where Shadow DOM comes in.
Attaching a Shadow Root
Attach a Shadow Root to the element and render the DOM inside of that root:
index.js
class RatingElement extends HTMLElement {
constructor() {
super();
this.rating = 0;
}
connectedCallback() {
const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = `
<style>
:host {
display: inline-flex;
align-items: center;
}
button {
background: transparent;
border: none;
cursor: pointer;
}
</style>
<button class="thumb_down" >
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>
</button>
<span class="rating">${this.rating}</span>
<button class="thumb_up">
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg>
</button>
`;
}
}
customElements.define('rating-element', RatingElement);
When you refresh the page, you will notice that the styles in the main document can no longer select the nodes inside the Shadow Root.
How did you do this? In the connectedCallback
you called this.attachShadow
which attaches a shadow root to an element. The open
mode means that the shadow content is inspectable and makes the shadow root accessible via this.shadowRoot
as well. Take a look at the Web Component in the Chrome inspector as well:
You should now see an expandable shadow root that holds the contents. Everything inside that shadow root is called the Shadow DOM. If you were to select the rating element in Chrome Dev Tools and call $0.children
, you will notice that it returns no children. This is because Shadow DOM is not considered a part of the same DOM tree as direct children but rather the Shadow Tree.
Light DOM
An experiment: add a node as a direct child of the <rating-element>
:
index.html
<rating-element>
<div>
This is the light DOM!
</div>
</rating-element>
Refresh the page, and you'll see that this new DOM node in this Custom Element's Light DOM does not show up on the page. This is because Shadow DOM has features to control how Light DOM nodes are projected into the shadow dom via <slot>
elements.
Why Templates
Using innerHTML
and template literal strings with no sanitization may cause security issues with script injection. Methods in the past have included using DocumentFragment
s, but these also come with other issues such as images loading and scripts running when the templates are defined as well as introducing obstacles for reusability. This is where the <template>
element comes in; templates provide inert DOM, a highly performant method to clone nodes, and reusable templating.
Using Templates
Next, transition the component to use HTML Templates:
index.html
<body>
<template id="rating-element-template">
<style>
:host {
display: inline-flex;
align-items: center;
}
button {
background: transparent;
border: none;
cursor: pointer;
}
</style>
<button class="thumb_down" >
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>
</button>
<span class="rating"></span>
<button class="thumb_up">
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg>
</button>
</template>
<rating-element>
<div>
This is the light DOM!
</div>
</rating-element>
</body>
Here you moved the DOM content into a template tag in the main document's DOM. Now refactor the custom element definition:
index.js
class RatingElement extends HTMLElement {
constructor() {
super();
this.rating = 0;
}
connectedCallback() {
const shadowRoot = this.attachShadow({mode: 'open'});
const templateContent = document.getElementById('rating-element-template').content;
const clonedContent = templateContent.cloneNode(true);
shadowRoot.appendChild(clonedContent);
this.shadowRoot.querySelector('.rating').innerText = this.rating;
}
}
customElements.define('rating-element', RatingElement);
To use this template element, you query the template, get it's contents, and clone those nodes with templateContent.cloneNode
where the true
argument performs a deep clone. You then initialize the dom with the data.
Congratulations, you now have a Web Component! Unfortunately it does not do anything yet, so next, add some functionality.
Property Bindings
Currently, the only way to set the rating on the rating-element is to construct the element, set the rating
property on the object, and then put it on the page. Unfortunately, this is not how native HTML elements tend to work. Native HTML elements tend to update with both property and attribute changes.
Make the custom element update the view when the rating
property changes by adding the following lines:
index.js
constructor() {
super();
this._rating = 0;
}
set rating(value) {
this._rating = value;
if (!this.shadowRoot) {
return;
}
const ratingEl = this.shadowRoot.querySelector('.rating');
if (ratingEl) {
ratingEl.innerText = this._rating;
}
}
get rating() {
return this._rating;
}
You add a setter and getter for the rating property, and then you update the rating element's text if it's available. This means if you were to set the rating property on the element, the view will update; give it a quick test in your Dev Tools console!
Attribute Bindings
Now, update the view when the attribute changes; this is similar to an input updating its view when you set <input value="newValue">
. Luckily, the Web Component lifecycle includes the attributeChangedCallback
. Update the rating by adding the following lines:
index.js
static get observedAttributes() {
return ['rating'];
}
attributeChangedCallback(attributeName, oldValue, newValue) {
if (attributeName === 'rating') {
const newRating = Number(newValue);
this.rating = newRating;
}
}
In order for the attributeChangedCallback
to trigger, you must set a static getter for RatingElement.observedAttributes which defines the attributes to be observed for changes
. You then set the rating declaratively in the DOM. Give it a try:
index.html
<rating-element rating="5"></rating-element>
The rating should now be updating declaratively!
Button Functionality
Now all that's missing is the button functionality. The behavior of this component should allow the user to provide a single up or down vote rating and give visual feedback to the user. You can implement this with some event listeners and a reflecting property, but first update the styles to give visual feedback by appending the following lines:
index.html
<style>
...
:host([vote=up]) .thumb_up {
fill: green;
}
:host([vote=down]) .thumb_down {
fill: red;
}
</style>
In Shadow DOM the :host
selector refers to the node or custom element that the Shadow Root is attached to. In this case, if the vote
attribute is "up"
it will turn the thumb-up button green, but if vote
is "down", then it will turn the thumb-down button red
. Now, implement the logic for this by creating a reflecting property / attribute for vote
similar to how you implemented rating
. Start with the property setter and getter:
index.js
constructor() {
super();
this._rating = 0;
this._vote = null;
}
set vote(newValue) {
const oldValue = this._vote;
if (newValue === oldValue) {
return;
}
if (newValue === 'up') {
if (oldValue === 'down') {
this.rating += 2;
} else {
this.rating += 1;
}
} else if (newValue === 'down') {
if (oldValue === 'up') {
this.rating -= 2;
} else {
this.rating -= 1;
}
}
this._vote = newValue;
this.setAttribute('vote', newValue);
}
get vote() {
return this._vote;
}
You initialize the _vote
instance property with null
in the constructor
, and in the setter you check if the new value is different. If so, you adjust the rating accordingly and, importantly, reflect the vote
attribute back to the host with this.setAttribute
. Next, set up the attribute binding:
index.js
static get observedAttributes() {
return ['rating', 'vote'];
}
attributeChangedCallback(attributeName, oldValue, newValue) {
if (attributeName === 'rating') {
const newRating = Number(newValue);
this.rating = newRating;
} else if (attributeName === 'vote') {
this.vote = newValue;
}
}
Again, this is the same process you went through with the rating
attribute binding; you add vote
to the observedAttributes
, and you set the vote
property in the attributeChangedCallback
. And now finally, add some click event listeners to give the buttons functionality!
index.js
constructor() {
super();
this._rating = 0;
this._vote = null;
this._boundOnUpClick = this._onUpClick.bind(this);
this._boundOnDownClick = this._onDownClick.bind(this);
}
connectedCallback() {
...
this.shadowRoot.querySelector('.thumb_up')
.addEventListener('click', this._boundOnUpClick);
this.shadowRoot.querySelector('.thumb_down')
.addEventListener('click', this._boundOnDownClick);
}
disconnectedCallback() {
this.shadowRoot.querySelector('.thumb_up')
.removeEventListener('click', this._boundOnUpClick);
this.shadowRoot.querySelector('.thumb_down')
.removeEventListener('click', this._boundOnDownClick);
}
_onUpClick() {
this.vote = 'up';
}
_onDownClick() {
this.vote = 'down';
}
In the constructor
you bind some click listeners to the element and keep the references around. In the connectedCallback
you listen for click events on the buttons. In the disconnectedCallback
you clean up these listeners, and on the click listeners themselves, you set vote
appropriately.
Congratulations, you now have a fully-featured Web Component; try clicking on some buttons! The issue now is that my JS file is now reaching 96 lines, my HTML file 43 lines, and the code is quite verbose and imperative for such a simple component. This is where Google's Lit project comes in!
Code Checkpoint
You may remix this glitch project or continue with your own from here.
Why lit-html
First and foremost, the <template>
tag is useful and performant, but it isn't packaged with the logic of the component thus making it difficult to distribute the template with the rest of the logic. Additionally the way template elements work inherently lend to imperative code, which in many cases, leads to less-readable code compared to declarative coding patterns.
This is where lit-html comes in! Lit html is a rendering system created by Google that allows you to write HTML templates in Javascript, then efficiently render and re-render those templates together with data to create and update DOM. It is similar to popular JSX and VDOM libraries but it runs natively in the browser and much more efficiently in many cases.
Using Lit HTML
Next, migrate the native Web Component rating-element
to use lit-html which uses a Tagged Template Literal which are functions that take template strings as arguments with a special syntax. Lit-html then uses template elements under the hood to provide fast rendering as well as providing some sanitization features for security. Start by migrating the <template>
in index.html
into a lit-html template by adding a render()
method to the webcomponent:
index.js
import {render, html} from 'lit-html';
class RatingElement extends HTMLElement {
...
render() {
if (!this.shadowRoot) {
return;
}
const template = html`
<style>
:host {
display: inline-flex;
align-items: center;
}
button {
background: transparent;
border: none;
cursor: pointer;
}
:host([vote=up]) .thumb_up {
fill: green;
}
:host([vote=down]) .thumb_down {
fill: red;
}
</style>
<button class="thumb_down">
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>
</button>
<span class="rating">${this.rating}</span>
<button class="thumb_up">
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg>
</button>`;
render(template, this.shadowRoot);
}
}
You may also delete your template from index.html
. In this render method you define a variable called template
and invoke the html
tagged template literal function. You will also notice that you have performed a simple data binding inside the span.rating
element by using the template literal interpolation syntax of ${...}
. This means that you will eventually no longer need to imperatively update that node. Additionally, you call the lit-html render
method which synchronously renders the template into the shadow root.
Migrating to Declarative Syntax
Now that you have gotten rid of the <template>
element, refactor the code to instead call the newly-defined render
method. You can start by leveraging lit-html's event listener binding to clear up the listener code:
index.js
<button
class="thumb_down"
@click=${() => {this.vote = 'down'}}>
...
<button
class="thumb_up"
@click=${() => {this.vote = 'up'}}>
Lit-html can add an event listener to a node with the @EVENT_NAME binding syntax where, in this case, you update the vote
property every time these buttons are clicked.
Next, clean up the initialization code in the constructor
and the connectedCallback
and disconnectedCallback
:
index.js
constructor() {
super();
this._rating = 0;
this._vote = null;
}
connectedCallback() {
this.attachShadow({mode: 'open'});
this.render();
}
You were able to remove the click listener logic from all three callbacks and even remove the disconnectedCallback
entirely! You were also able to remove all of the DOM initialization code from the connectedCallback
making it look much more elegant. This also means that you can get rid of the _onUpClick
and _onDownClick
listener methods!
Finally, update the property setters to utilize the render
method so that the dom can update when either the properties or attributes change:
index.js
set rating(value) {
this._rating = value;
this.render();
}
...
set vote(newValue) {
const oldValue = this._vote;
if (newValue === oldValue) {
return;
}
if (newValue === 'up') {
if (oldValue === 'down') {
this.rating += 2;
} else {
this.rating += 1;
}
} else if (newValue === 'down') {
if (oldValue === 'up') {
this.rating -= 2;
} else {
this.rating -= 1;
}
}
this._vote = newValue;
this.setAttribute('vote', newValue);
this.render();
}
Here, you were able to remove the dom update logic from the rating
setter and you added a call to render
from the vote
setter. Now the template is much more readable as you now can see where the bindings and event listeners are applied.
Refresh the page, and you should have a functioning rating button that should look like this when the upvote is pressed!
Why LitElement
Some problems are still present with the code. First, if you change the vote
property or attribute, it may change the rating
property which will result in calling render
twice. Despite one of those calls essentially being a no-op and being efficient, the javascript VM is still spending time calling that function twice synchronously. Second, it is tedious adding new properties and attributes as it requires a lot of boilerplate code. This is where LitElement
comes in!
LitElement is a base class made by Google for creating fast, lightweight Web Components that can be used across frameworks and environments. Next, take a look at what LitElement can do for us in the rating-element
by changing the implementation to use it!
Using LitElement
Start by importing and subclassing the LitElement
base class from the lit-element
package:
index.js
import {LitElement, html, css} from 'lit-element';
class RatingElement extends LitElement {
...
You import LitElement
which is the new base class for the rating-element
. Next you import html
which lit-element just re-exports from lit-html. And finally, you import css
which allows us to define css tagged template literals for css math, templating, and other features under the hood.
Next, move the styles from the render method to LitElement's static stylesheet:
index.js
class RatingElement extends LitElement {
static get styles() {
return css`
:host {
display: inline-flex;
align-items: center;
}
button {
background: transparent;
border: none;
cursor: pointer;
}
:host([vote=up]) .thumb_up {
fill: green;
}
:host([vote=down]) .thumb_down {
fill: red;
}
`;
}
...
This is where most styles live in LitElements. LitElement will take these styles and use features such as Constructable Stylesheets to provide faster rendering times as well as pass it through the Web Components polyfill on older browsers if necessary.
Lifecycle
LitElement introduces a set of render lifecycle callback methods on top of the native Web Component callbacks. These callbacks are triggered when declared LitElement properties are changed.
To use this feature, you must statically declare which properties will trigger the render lifecycle.
index.js
static get properties() {
return {
rating: {
type: Number,
},
vote: {
type: String,
reflect: true,
}
};
}
Here, you define that rating
and vote
will trigger the LitElement rendering lifecycle as well as defining the types that will be used to convert the string attributes into properties.
<user-profile .name=${this.user.name} .age=${this.user.age}>
${this.user.family.map(member => html`
<family-member
.name=${member.name}
.relation=${member.relation}>
</family-member>`)}
</user-profile>
Additionally, the reflect
flag on the vote
property will automatically update the host element's vote
attribute that you manually triggered in the vote
setter.
Now that you have the static properties block, you can remove all of the attribute and property render updating logic. This means you can remove the following methods:
connectedCallback
observedAttributes
attributeChangedCallback
rating
(setters and getters)vote
(setters and getters but keep the change logic from the setter)
What you keep is the constructor
as well as adding a new update
lifecycle method:
index.js
constructor() {
super();
this.rating = 0;
this.vote = null;
}
update(changedProps) {
if (changedProps.has('vote')) {
const newValue = this.vote;
const oldValue = changedProps.get('vote');
if (newValue === 'up') {
if (oldValue === 'down') {
this.rating += 2;
} else {
this.rating += 1;
}
} else if (newValue === 'down') {
if (oldValue === 'up') {
this.rating -= 2;
} else {
this.rating -= 1;
}
}
}
super.update(changedProps);
}
Here, you simply initialize rating
and vote
and you move the vote
setter logic to the update
lifecycle method. The update
method is called before render
whenever any updating property is changed, because LitElement batches property changes and makes rendering asynchronous. You then call super.update
after you make the changes to this.rating
otherwise you would trigger an unnecessary render lifecycle.
Finally, render
is a LitElement lifecycle method which requires us to return a template:
index.js
render() {
return html`
<button
class="thumb_down"
@click=${() => {this.vote = 'down'}}>
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>
</button>
<span class="rating">${this.rating}</span>
<button
class="thumb_up"
@click=${() => {this.vote = 'up'}}>
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg>
</button>`;
}
You no longer have to check for the shadow root, and you no longer have to call the render
function previously imported from the lit-html package.
Your element should render in the preview now; give it a click!
Congratulations, you've successfully built a Web Component from scratch and evolved it into a LitElement!
LitElement is super small, super fast, and really fun to code with! You can make components to be consumed by other frameworks, or you can build full-fledged apps with it!
You now know what a Web Component is, how to build one, and how lit-html and LitElement make it easier to build them!
Code Checkpoint
Do you want to check your final code against ours? Remix it here.
What's next?
Check out some of the other CDS 2020 codelabs!
Visit the Lit Room in CDS 2020 and play the Component 64 coding challenges!
Further reading
- Try LitElement
- The Lit Element guide
- Open Web Components - A community run guidance community
- WebComponents.dev - Create a Web Component in all known frameworks
Community
- The Polymer Slack - The largest Web Components community
- Polymer Twitter - The Twitter account of the team that made Lit
- Web Components SF - A Web Components meetup for San Francisco