1. Introduction
Last Updated: 2021-08-10
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.
What is Lit
Lit is a simple library for building fast, lightweight web components that work in any framework, or with no framework at all. With Lit you can build shareable components, applications, design systems, and more.
Lit 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
- What Lit does on top of a web component
What you'll build
- A vanilla thumbs up / down Web Component
- A thumbs up / down Lit-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.
2. Getting set up & exploring the Playground
Accessing the code
Throughout the codelab there will be links to the Lit playground like this:
The playground is a code sandbox that runs completely in your browser. It can compile and run TypeScript and JavaScript files, and it can also automatically resolve imports to node modules. e.g.
// before
import './my-file.js';
import 'lit';
// after
import './my-file.js';
import 'https://unpkg.com/lit?module';
You can do the entire tutorial in the Lit playground, using these checkpoints as starting points. If you're using VS Code, you can use these checkpoints to download the starting code for any step, as well as using them to check your work.
Exploring the lit playground UI
The Lit playground UI screenshot highlights sections that you'll use in this codelab.
- File selector. Note the plus button...
- File editor.
- Code preview.
- Reload button.
- Download button.
VS Code setup (Advanced)
Here are the benefits to using this VS Code setup:
- Template type checking
- Template intellisense & autocompletion
If you have NPM, VS Code (with the lit-plugin plugin) installed already and know how to use that environment, you may simply download and start these projects by doing the following:
- Press the download button
- Extract the tar file's contents into a directory
- Install a dev server that can resolve bare module specifiers (the Lit team recommends @web/dev-server)
- Here is an example
package.json
- Here is an example
- Run the dev server and open your browser (if you are using
@web/dev-server
you can usenpx web-dev-server --node-resolve --watch --open
)- If you are using the example
package.json
usenpm run serve
- If you are using the example
3. Define A Custom Element
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-element'
. 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-element')
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.
4. Shadow DOM
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.
5. HTML Templates
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 its 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.
6. Adding 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!
7. Lit-html
Code Checkpoint
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 are used 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 the rendering system of Lit 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 template which use Tagged Template Literals which are functions that take template strings as arguments with a special syntax. Lit 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 template by adding a render()
method to the webcomponent:
index.js
// Dont forget to import from Lit!
import {render, html} from 'lit';
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 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'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 templates 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 event listener 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();
}
// remove disonnectedCallback and _onUpClick and _onDownClick
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);
// add render method
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!
8. LitElement
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 repeat calls of render essentially being a no-op and 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 Lit's base class 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
package:
index.js
import {LitElement, html, css} from 'lit';
class RatingElement extends LitElement {
// remove connectedCallback()
...
You import LitElement
which is the new base class for the rating-element
. Next you keep your html
import and finally 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 Lit'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 Lit. Lit will take these styles and use browser 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
Lit introduces a set of render lifecycle callback methods on top of the native Web Component callbacks. These callbacks are triggered when declared Lit 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,
}
};
}
// remove observedAttributes() and attributeChangedCallback()
// remove set rating() get rating()
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 willUpdate
lifecycle method:
index.js
constructor() {
super();
this.rating = 0;
this.vote = null;
}
willUpdate(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;
}
}
}
}
// remove set vote() and get vote()
Here, you simply initialize rating
and vote
and you move the vote
setter logic to the willUpdate
lifecycle method. The willUpdate
method is called before render
whenever any updating property is changed, because LitElement batches property changes and makes rendering asynchronous. Changes to reactive properties (like this.rating
) in willUpdate
will not trigger unnecessary render
lifecycle calls.
Finally, render
is a LitElement lifecycle method which requires us to return a Lit 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'
package.
Your element should render in the preview now; give it a click!
9. Congratulations
Congratulations, you've successfully built a Web Component from scratch and evolved it into a LitElement!
Lit is super small (< 5kb minified + gzipped), 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 makes it easier to build them!
Code Checkpoint
Do you want to check your final code against ours? Compare it here.
What's next?
Check out some of the other codelabs!
- Lit for React Developers
- Build a Brick Viewer with lit-element
- Build a Stories Component with lit-element
Further reading
- Lit interactive tutorial
- The Lit Docs
- Open Web Components - A community run guidance and tooling community
- WebComponents.dev - Create a Web Component in all known frameworks
Community
- Lit and Friends Slack - The largest Web Components community
- @buildWithLit on Twitter - The Twitter account of the team that made Lit
- Web Components SF - A Web Components meetup for San Francisco