This codelab is a followup from the Lit basics codelab.
lit-html is an efficient, expressive and extensible HTML templating library for JavaScript. It lets you write HTML templates in JavaScript, then efficiently render and re-render those templates together with data to create and update DOM:
lit-element is a simple base class for creating fast and lightweight web components with Lit.
What you need
What you'll learn
How it works
Unlike the basics codelab, we will not explain the required changes for each step in detail. Instead, we give background information and the desired end-result. In most steps, we offer some tips, most of them hidden behind a toggle.
At the bottom of each section, there is a "View final result" button, this will show you the correct code that you should end up with, in case you get stuck. The steps are sequential, thus results from the previous steps carry over to the next step.
In this codelab we will build a brewery app, using a public API as data source. This is a great exercise to learn the intermediate parts of Lit and lit-element.
You can follow this codelab using anything that is able to display a simple HTML page. We recommend using an online code editor so that you don't need to bother with all the setup. But you can use a local editor as well.
All our examples are shown using Javascript, but Lit supports Typescript as well.
The code editor we recommend is webcomponents.dev. You can use these links to quickly start a new project:
If instead you are creating a new project from the website, make sure to select Lit from the "Libraries" section and not the "HTMLElement based" section.
If you are using another editor, you need to set up a basic index.html
which loads your component:
<!DOCTYPE html>
<html>
<body>
My app
<!--
this is a refernece in the HTML to your web component,
make sure to update it based on the name you have given your component
-->
<my-app></my-app>
<script type="module" src="./src/index.js"></script>
</body>
</html>
To make following further instructions easier, it's recommended to write your JS code in a src/index.js
file to match the webcomponents.dev setup.
The webcomponents.dev editor creates a few files for you. We will be working only with src/index.js
and www/index.html
.
In the src/index.js
file there is already some code with an example element. For the sake of this tutorial, let's empty this file and start over from scratch. We can leave the www/index.html
file as is.
The editor also shows three tabs on the right side of the screen. We will only be using the "Website" tab. If you want, you could delete the stories/index.stories.js
and README.md
so that the other tabs disappear.
You should already know how to create a web component using LitElement
. Go ahead and create one which renders 'My brewery app' to the screen. When it works, you're ready to move on to the next step.
www/index.html
:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<script src="./dist/index.js" type="module"></script>
</head>
<body>
<brewery-app></brewery-app>
</body>
</html>
src/index.js
:
import { LitElement, html } from 'lit';
class BreweryApp extends LitElement {
render() {
return html`My brewery app`;
}
}
customElements.define('brewery-app', BreweryApp);
For this codelab, we will be fetching data from the api of Open Brewery DB. Use the url provided to connect and retrieve data from the Open Brewery API https://api.openbrewerydb.org/breweries
.
Create a variable storing the URL. You can test the URL by visiting it from the browser. You should see the API return a JSON response. The return response object you can expect from the api should look similar to this:
[
{
"id": 2,
"name": "Avondale Brewing Co",
"brewery_type": "micro",
"street": "201 41st St S",
"city": "Birmingham",
"state": "Alabama",
"postal_code": "35222-1932",
"country": "United States",
"longitude": "-86.774322",
"latitude": "33.524521",
"phone": "2057775456",
"website_url": "http://www.avondalebrewing.com",
"updated_at": "2018-08-23T23:19:57.825Z",
"tag_list": []
},
"..."
]
Besides displaying UI, web components can also use any of the available javascript APIs. We are going to use the fetch
function to make an HTTP request to retrieve a list of breweries. fetch
is an asynchronous operation, and it costs system resources. We, therefore, need to be a bit more careful about when and how we use it.
LitElement
has several lifecycle methods available for this, some of them will be familiar to you by now. See this page for a full overview and reference of all the available lifecycle methods.
We could trigger our fetch
in the constructor since it's run only once. But because it's a best practice to not perform any side effects there, it's better to use the connectedCallback.
Because this method can be called multiple times during an element's lifecycle, we should be careful to trigger a fetch
only when the data hasn't already been fetched before.
fetch
is a browser API for making HTTP requests. It's promise based, and it returns a streaming response. We can turn the the stream into JSON using the json
function:
async fetchBreweries() {
const response = await fetch('https://api.openbrewerydb.org/breweries');
const jsonResponse = await response.json();
this.breweries = jsonResponse;
}
If you're not familiar with async await, this is what it looks like using Promise
:
fetchBreweries() {
fetch('https://api.openbrewerydb.org/breweries')
.then(response => response.json())
.then((jsonResponse) => {
this.breweries = jsonResponse;
});
}
class BreweryApp extends LitElement {
connectedCallback() {
super.connectedCallback();
if (!this.breweries) {
this.fetchBreweries();
}
}
}
render() {
return html`
<pre>${JSON.stringify(this.breweries, null, 2)}</pre>
`;
}
import { LitElement, html } from 'lit';
class BreweryApp extends LitElement {
static properties = {
breweries: { type: Array },
};
connectedCallback() {
super.connectedCallback();
if (!this.breweries) {
this.fetchBreweries();
}
}
async fetchBreweries() {
const response = await fetch('https://api.openbrewerydb.org/breweries');
const jsonResponse = await response.json();
this.breweries = jsonResponse;
}
render() {
return html` <pre>${JSON.stringify(this.breweries, null, 2)}</pre> `;
}
}
customElements.define('brewery-app', BreweryApp);
Fetching the breweries is async, so the first time your render
function is called they aren't there yet. This isn't a problem right now, because JSON.stringify
handles null or undefined input. But when we want to start doing things with the list of breweries, our code will crash because the first time this.breweries
is undefined.
We can cover this scenario by preventing the rendering of our main content until the list of breweries are fetched. We can take this opportunity to display a nice loading state for the user as well.
async fetchBreweries() {
this.loading = true;
const response = await fetch('https://api.openbrewerydb.org/breweries');
const jsonResponse = await response.json();
this.breweries = jsonResponse;
this.loading = false;
}
render() {
if (this.loading) {
return html`<p>Loading...</p>`;
}
return html`
<pre>${JSON.stringify(this.breweries, null, 2)}</pre>
`;
}
import { LitElement, html } from 'lit';
class BreweryApp extends LitElement {
static properties = {
loading: { type: Boolean },
breweries: { type: Array },
};
connectedCallback() {
super.connectedCallback();
if (!this.breweries) {
this.fetchBreweries();
}
}
async fetchBreweries() {
this.loading = true;
const response = await fetch('https://api.openbrewerydb.org/breweries');
const jsonResponse = await response.json();
this.breweries = jsonResponse;
this.loading = false;
}
render() {
if (this.loading) {
return html` <p>Loading...</p> `;
}
return html` <pre>${JSON.stringify(this.breweries, null, 2)}</pre> `;
}
}
customElements.define('brewery-app', BreweryApp);
To display individual breweries it's best to create a new component so that we separate the logic from the main app. This should be a plain UI component, which receives the brewery data as plain properties to display.
BreweryDetail.js
brewery-detail
element that displays the brewery's name, type and city.BreweryDetail
component inside the BreweryApp
brewery-detail
elements in the brewery-app
, one for each brewery received from the OpenBreweryDB API.class BreweryDetail extends LitElement {
static properties = {
name: { type: String },
type: { type: String },
city: { type: String },
};
render() {
return html`
<h3>${this.name}</h3>
<p>brewery type: ${this.type}</p>
<p>city: ${this.city}</p>
`;
}
}
customElements.define('brewery-detail', BreweryDetail);
render() {
return html`
<ul>
${this.breweries.map(
brewery => html`
<li>
<brewery-detail
.name=${brewery.name}
.type=${brewery.brewery_type}
.city=${brewery.city}
></brewery-detail>
</li>
`,
)}
</ul>
`;
}
import { LitElement, html } from 'lit';
import './BreweryDetail.js';
class BreweryApp extends LitElement {
static properties = {
loading: { type: Boolean },
breweries: { type: Array },
};
connectedCallback() {
super.connectedCallback();
if (!this.breweries) {
this.fetchBreweries();
}
}
async fetchBreweries() {
this.loading = true;
const response = await fetch('https://api.openbrewerydb.org/breweries');
const jsonResponse = await response.json();
this.breweries = jsonResponse;
this.loading = false;
}
render() {
if (this.loading) {
return html` <p>Loading...</p> `;
}
return html`
<h1>Breweries App</h1>
<h2>Breweries</h2>
<ul>
${this.breweries.map(
brewery => html`
<li>
<brewery-detail
.name=${brewery.name}
.type=${brewery.brewery_type}
.city=${brewery.city}
></brewery-detail>
</li>
`,
)}
</ul>
`;
}
}
customElements.define('brewery-app', BreweryApp);
import { LitElement, html } from 'lit';
class BreweryDetail extends LitElement {
static properties = {
name: { type: String },
type: { type: String },
city: { type: String },
};
render() {
return html`
<h3>${this.name}</h3>
<p>brewery type: ${this.type}</p>
<p>city: ${this.city}</p>
`;
}
}
customElements.define('brewery-detail', BreweryDetail);
On a brewery tour it's useful to know which brewery has already been visited. To allow the user to track whether or not they have already visited a brewery we need a button, so the user can click to mark they have visited the brewery.
As a start, you can maintain a local property for the visited/not-visited status in the brewery-detail
component. We will look into lifting this data to the parent component in the next step.
brewery-detail
component which indicates whether the user has visited the brewery.brewery-detail
component to toggle between the visited/not-visited status, storing this status locally.You can conditionally render something using any valid javascript expression. For simple logic, a ternary operator is sufficient:
render() {
return html`
<h3>${this.name} (${this.visited ? 'visited' : 'not-visited'})</h3>
`;
}
If the logic is a bit more complex, separating this into a pure function is very useful. The advantage here is that we can use regular if statements, so we don't need to squash everything into a single expression:
function visitedStatus(visited) {
if (visited) {
return '(visited)';
}
return '(not-visited)';
}
class MyBrewery extends LitElement {
render() {
return html` Bendërbrāu ${visitedStatus(this.visited)} `;
}
}
@
syntax, which is just syntax sugar for `addEventListener`:
render() {
return html`
<button @click=${this._onClick}></button>
`;
}
_onClick(e) {
}
In this example, we register an event listener for the click
event, and call the _onClick
method on the element when this event is fired.
import { LitElement, html } from 'lit';
import './BreweryDetail.js';
class BreweryApp extends LitElement {
static properties = {
loading: { type: Boolean },
breweries: { type: Array },
};
connectedCallback() {
super.connectedCallback();
if (!this.breweries) {
this.fetchBreweries();
}
}
async fetchBreweries() {
this.loading = true;
const response = await fetch('https://api.openbrewerydb.org/breweries');
const jsonResponse = await response.json();
this.breweries = jsonResponse;
this.loading = false;
}
render() {
if (this.loading) {
return html` <p>Loading...</p> `;
}
return html`
<h1>Breweries App</h1>
<h2>Breweries</h2>
<ul>
${this.breweries.map(
brewery => html`
<li>
<brewery-detail
.name=${brewery.name}
.type=${brewery.brewery_type}
.city=${brewery.city}
.visited=${brewery.visited}
></brewery-detail>
</li>
`,
)}
</ul>
`;
}
}
customElements.define('brewery-app', BreweryApp);
import { LitElement, html } from 'lit';
class BreweryDetail extends LitElement {
static properties = {
name: { type: String },
type: { type: String },
city: { type: String },
visited: { type: Boolean },
};
render() {
return html`
<h3>${this.name} (${this.visited ? 'visited' : 'not-visited'})</h3>
<p>brewery type: ${this.type}</p>
<p>city: ${this.city}</p>
<button @click=${this._toggleVisitedStatus}>
Mark as ${this.visited ? 'not-visited' : 'visited'}
</button>
`;
}
_toggleVisitedStatus() {
this.visited = !this.visited;
}
}
customElements.define('brewery-detail', BreweryDetail);
Now that the user can mark breweries as visited/not-visited, we want to display the total amount of visited breweries and the breweries that still need to be visited in the app. This counter should be displayed in the brewery-app
, but we're storing the visited/not-visited status in the brewery-detail
component. We need to think of a better way to solve this...
It's best to keep the data in your application flowing in one direction from top to bottom. Parent components are responsible for data of child components, including changing this data.
In our case, the brewery-detail
component can fire an event to the brewery-app
component to request a change in the visited/not-visited status.
brewery-app
component.brewery-app
component.Remember that with LitElement
, you need to use immutable data patterns. Otherwise, it will not be able to pick up data changes.
_toggleVisitedStatus() {
this.dispatchEvent(new CustomEvent('toggle-visited-status'));
}
When you add an event listener on an element in a list of templates, you need a way to know which element in the list fired the event. This can be done by passing the list item to the event handler:
html`
${this.breweries.map(
brewery => html`
<li>
<brewery-detail
@toggle-visited-status=${() => this._toggleVisitedStatus(brewery)}
></brewery-detail>
</li>
`,
)}
`;
To update the visited status, we need to use immutable data update patterns. This means we should create a breweries array, and a new object for the brewery that was updated. A quick way to do this, is by using a map function:
_toggleVisitedStatus(breweryToUpdate) {
this.breweries = this.breweries.map(brewery => {
return brewery === breweryToUpdate ? { ...brewery, visited: !brewery.visted } : brewery;
});
}
To display the total amount of visisted/not-visited breweries, we can calculate it on top of the render function:
render() {
const totalVisited = this.breweries.filter(b => b.visited).length;
return html`...`;
}
import { LitElement, html } from 'lit';
import './BreweryDetail.js';
class BreweryApp extends LitElement {
static properties = {
loading: { type: Boolean },
breweries: { type: Array },
};
connectedCallback() {
super.connectedCallback();
if (!this.breweries) {
this.fetchBreweries();
}
}
async fetchBreweries() {
this.loading = true;
const response = await fetch('https://api.openbrewerydb.org/breweries');
const jsonResponse = await response.json();
this.breweries = jsonResponse;
this.loading = false;
}
render() {
if (this.loading) {
return html` <p>Loading...</p> `;
}
const totalVisited = this.breweries.filter(b => b.visited).length;
const totalNotVisited = this.breweries.length - totalVisited;
return html`
<h1>Breweries App</h1>
<h2>Breweries</h2>
<p>(${totalVisited} visited and ${totalNotVisited} still to go)</p>
<ul>
${this.breweries.map(
brewery => html`
<li>
<brewery-detail
.name=${brewery.name}
.type=${brewery.brewery_type}
.city=${brewery.city}
.visited=${brewery.visited}
@toggle-visited-status=${() => this._toggleVisitedStatus(brewery)}
></brewery-detail>
</li>
`,
)}
</ul>
`;
}
_toggleVisitedStatus(breweryToUpdate) {
this.breweries = this.breweries.map(brewery => {
return brewery === breweryToUpdate ? { ...brewery, visited: !brewery.visited } : brewery;
});
}
}
customElements.define('brewery-app', BreweryApp);
import { LitElement, html } from 'lit';
class BreweryDetail extends LitElement {
static properties = {
name: { type: String },
type: { type: String },
city: { type: String },
visited: { type: Boolean },
};
render() {
return html`
<h3>${this.name} (${this.visited ? 'visited' : 'not-visited'})</h3>
<p>brewery type: ${this.type}</p>
<p>city: ${this.city}</p>
<button @click=${this._toggleVisitedStatus}>
Mark as ${this.visited ? 'not-visited' : 'visited'}
</button>
`;
}
_toggleVisitedStatus() {
this.dispatchEvent(new CustomEvent('toggle-visited-status'));
}
}
customElements.define('brewery-detail', BreweryDetail);
Now that the brewery-app
component knows about the visited/not-visited status, we can do more interesting things like allowing the user to filter based on the brewery's status.
It's a good practice to separate concerns in your application, and in a real application, such a filter might grow to be quite complex in UI and logic. In those cases, it can be a good idea to separate it into a separate component.
If the functionality is small, like in our example application, we can keep it in the brewery-app
component for now.
brewery-app
component:To create a filter, each of the three buttons can update a filter
property on the element. Changing this property should trigger a re-render.
Then, on the top of your render
function, you can filter the array of breweries using this filter value. Make sure you're using this new array in your template, and not the original array.
import { LitElement, html } from 'lit';
import './BreweryDetail.js';
class BreweryApp extends LitElement {
static properties = {
loading: { type: Boolean },
breweries: { type: Array },
filter: { type: String },
};
connectedCallback() {
super.connectedCallback();
if (!this.breweries) {
this.fetchBreweries();
}
}
async fetchBreweries() {
this.loading = true;
const response = await fetch('https://api.openbrewerydb.org/breweries');
const jsonResponse = await response.json();
this.breweries = jsonResponse;
this.loading = false;
}
render() {
if (this.loading) {
return html` <p>Loading...</p> `;
}
const totalVisited = this.breweries.filter(b => b.visited).length;
const totalNotVisted = this.breweries.length - totalVisited;
const breweries = this.breweries.filter(brewery => {
if (!this.filter) {
return true;
}
return this.filter === 'visited' ? brewery.visited : !brewery.visited;
});
return html`
<h1>Breweries App</h1>
<h2>Breweries</h2>
<p>(${totalVisited} visited and ${totalNotVisted} still to go)</p>
<button @click=${this._filterNone}>Filter none</button>
<button @click=${this._filterVisited}>Filter visited</button>
<button @click=${this._filterNotVisited}>Filter not-visited</button>
<ul>
${breweries.map(
brewery => html`
<li>
<brewery-detail
.name=${brewery.name}
.type=${brewery.brewery_type}
.city=${brewery.city}
.visited=${brewery.visited}
@toggle-visited-status=${() => this.toggleVisitedStatus(brewery)}
></brewery-detail>
</li>
`,
)}
</ul>
`;
}
toggleVisitedStatus(breweryToUpdate) {
this.breweries = this.breweries.map(brewery => {
return brewery === breweryToUpdate ? { ...brewery, visited: !brewery.visited } : brewery;
});
}
_filterNone() {
this.filter = null;
}
_filterVisited() {
this.filter = 'visited';
}
_filterNotVisited() {
this.filter = 'not-visited';
}
}
customElements.define('brewery-app', BreweryApp);
import { LitElement, html } from 'lit';
class BreweryDetail extends LitElement {
static properties = {
name: { type: String },
type: { type: String },
city: { type: String },
visited: { type: Boolean },
};
render() {
return html`
<h3>${this.name} (${this.visited ? 'visited' : 'not-visited'})</h3>
<p>brewery type: ${this.type}</p>
<p>city: ${this.city}</p>
<button @click=${this._toggleVisitedStatus}>
Mark as ${this.visited ? 'not-visited' : 'visited'}
</button>
`;
}
_toggleVisitedStatus() {
this.dispatchEvent(new CustomEvent('toggle-visited-status'));
}
}
customElements.define('brewery-detail', BreweryDetail);
The great thing about web components is that we can pick up components built with any technology, and use them without knowing the internal implementation.
The Material Web Components is a project to implement material design in web components. It is currently in alpha, but we can already use many of the components.
<button>
elements in the application with <mwc-button>
To import a web component, you can use a 'side effects' import. This just runs the code of the module, which registers the web component.
import '@material/mwc-button';
<mwc-button>
works the same a <button>
element, we can just replace it's usage:
From:
html`
<button @click=${this._filterNone}>Filter none</button>
<button @click=${this._filterVisited}>Filter visited</button>
<button @click=${this._filterNotVisited}>Filter not-visited</button>
`;
To:
html`
<mwc-button @click=${this._filterNone}>Filter none</mwc-button>
<mwc-button @click=${this._filterVisited}>Filter visited</mwc-button>
<mwc-button @click=${this._filterNotVisited}>Filter not-visited</mwc-button>
`;
import { LitElement, html } from 'lit';
import '@material/mwc-button';
import './BreweryDetail.js';
class BreweryApp extends LitElement {
static properties = {
loading: { type: Boolean },
breweries: { type: Array },
filter: { type: String },
};
connectedCallback() {
super.connectedCallback();
if (!this.breweries) {
this.fetchBreweries();
}
}
async fetchBreweries() {
this.loading = true;
const response = await fetch('https://api.openbrewerydb.org/breweries');
const jsonResponse = await response.json();
this.breweries = jsonResponse;
this.loading = false;
}
render() {
if (this.loading) {
return html` <p>Loading...</p> `;
}
const totalVisited = this.breweries.filter(b => b.visited).length;
const totalNotVisited = this.breweries.length - totalVisited;
const breweries = this.breweries.filter(brewery => {
if (!this.filter) {
return true;
}
return this.filter === 'visited' ? brewery.visited : !brewery.visited;
});
return html`
<h1>Breweries App</h1>
<h2>Breweries</h2>
<p>(${totalVisited} visited and ${totalNotVisited} still to go)</p>
<mwc-button @click=${this._filterNone}>Filter none</mwc-button>
<mwc-button @click=${this._filterVisited}>Filter visited</mwc-button>
<mwc-button @click=${this._filterNotVisited}>Filter not-visited</mwc-button>
<ul>
${breweries.map(
brewery => html`
<li>
<brewery-detail
.name=${brewery.name}
.type=${brewery.brewery_type}
.city=${brewery.city}
.visited=${brewery.visited}
@toggle-visited-status=${() => this.toggleVisitedStatus(brewery)}
></brewery-detail>
</li>
`,
)}
</ul>
`;
}
toggleVisitedStatus(breweryToUpdate) {
this.breweries = this.breweries.map(brewery => {
return brewery === breweryToUpdate ? { ...brewery, visited: !brewery.visited } : brewery;
});
}
_filterNone() {
this.filter = null;
}
_filterVisited() {
this.filter = 'visited';
}
_filterNotVisited() {
this.filter = 'not-visited';
}
}
customElements.define('brewery-app', BreweryApp);
import { LitElement, html } from 'lit';
class BreweryDetail extends LitElement {
static properties = {
name: { type: String },
type: { type: String },
city: { type: String },
visited: { type: Boolean },
};
render() {
return html`
<h3>${this.name} (${this.visited ? 'visited' : 'not-visited'})</h3>
<p>brewery type: ${this.type}</p>
<p>city: ${this.city}</p>
<mwc-button @click=${this._toggleVisitedStatus}>
Mark as ${this.visited ? 'not-visited' : 'visited'}
</mwc-button>
`;
}
_toggleVisitedStatus() {
this.dispatchEvent(new CustomEvent('toggle-visited-status'));
}
}
customElements.define('brewery-detail', BreweryDetail);
As a final assignment we will add some styling to our app. To add styles to a LitElement, we have to import the css
template tag and apply it to a static styles
property:
import { LitElement, html, css } from 'lit';
class MyElement extends LitElement {
static styles = css`
h1 {
color: blue;
}
`;
}
LitElement
uses Shadow DOM by default. This is a native browser feature which scopes any styles to the web component itself. This makes it completely safe to use simple selectors such as HTML tag names. The styles will only ever affect the elements inside the web component.
import { LitElement, html, css } from 'lit';
import '@material/mwc-button';
import './BreweryDetail.js';
class BreweryApp extends LitElement {
static properties = {
loading: { type: Boolean },
breweries: { type: Array },
filter: { type: String },
};
static styles = css`
h1 {
color: blue;
}
p {
color: blue;
}
`;
connectedCallback() {
super.connectedCallback();
if (!this.breweries) {
this.fetchBreweries();
}
}
async fetchBreweries() {
this.loading = true;
const response = await fetch('https://api.openbrewerydb.org/breweries');
const jsonResponse = await response.json();
this.breweries = jsonResponse;
this.loading = false;
}
render() {
if (this.loading) {
return html` <p>Loading...</p> `;
}
const totalVisited = this.breweries.filter(b => b.visited).length;
const totalNotVisited = this.breweries.length - totalVisited;
const breweries = this.breweries.filter(brewery => {
if (!this.filter) {
return true;
}
return this.filter === 'visited' ? brewery.visited : !brewery.visited;
});
return html`
<h1>Breweries App</h1>
<h2>Breweries</h2>
<p>(${totalVisited} visited and ${totalNotVisited} still to go)</p>
<mwc-button @click=${this._filterNone}>Filter none</mwc-button>
<mwc-button @click=${this._filterVisited}>Filter visited</mwc-button>
<mwc-button @click=${this._filterNotVisited}>Filter not-visited</mwc-button>
<ul>
${breweries.map(
brewery => html`
<li>
<brewery-detail
.name=${brewery.name}
.type=${brewery.brewery_type}
.city=${brewery.city}
.visited=${brewery.visited}
@toggle-visited-status=${() => this.toggleVisitedStatus(brewery)}
></brewery-detail>
</li>
`,
)}
</ul>
`;
}
toggleVisitedStatus(breweryToUpdate) {
this.breweries = this.breweries.map(brewery => {
return brewery === breweryToUpdate ? { ...brewery, visited: !brewery.visited } : brewery;
});
}
_filterNone() {
this.filter = null;
}
_filterVisited() {
this.filter = 'visited';
}
_filterNotVisited() {
this.filter = 'not-visited';
}
}
customElements.define('brewery-app', BreweryApp);
import { LitElement, html, css } from 'lit';
class BreweryDetail extends LitElement {
static properties = {
name: { type: String },
type: { type: String },
city: { type: String },
visited: { type: Boolean },
};
static styles = css`
h1 {
color: red;
}
p {
color: red;
}
`;
render() {
return html`
<h3>${this.name} (${this.visited ? 'visited' : 'not-visited'})</h3>
<p>brewery type: ${this.type}</p>
<p>city: ${this.city}</p>
<mwc-button @click=${this._toggleVisitedStatus}>
Mark as ${this.visited ? 'not-visited' : 'visited'}
</mwc-button>
`;
}
_toggleVisitedStatus() {
this.dispatchEvent(new CustomEvent('toggle-visited-status'));
}
}
customElements.define('brewery-detail', BreweryDetail);
That's the end of the intermediate Lit codelab! If you're eager to learn more, you can take a look at the following resources:
To get started with your own project we recommend using open-wc's project scaffolding, it's easy to set it up using this command:
npm init @open-wc