This codelab is a followup from the lit-html & lit-element 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-html.
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. This is a great exercise to learn the intermediate parts of lit-html and lit-element.
You can follow this codelab using anything that can display a simple HTML page. For the best editing experience, we recommend setting this up using your favorite IDE. Alternatively, you can use an online code editor like jsbin, stackblitz or webcomponents.dev.
Let's create a basic HTML page with a module script, and import LitElement from our local node_modules:
<!DOCTYPE html>
<html>
<body>
<script type="module">
import { LitElement } from 'lit';
</script>
</body>
</html>
Make sure that you add type="module"
to the script tag.
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.
<!DOCTYPE html>
<html>
<body>
<brewery-app></brewery-app>
<script type="module">
import { LitElement, html } from 'lit';
class BreweryApp extends LitElement {
render() {
return html` My brewery app `;
}
}
customElements.define('brewery-app', BreweryApp);
</script>
</body>
</html>
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
.
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>
`;
}
<!DOCTYPE html>
<html>
<body>
<brewery-app></brewery-app>
<script type="module">
import { LitElement, html } from 'lit';
class BreweryApp extends LitElement {
static get properties() {
return {
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);
</script>
</body>
</html>
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>
`;
}
<!DOCTYPE html>
<html>
<body>
<brewery-app></brewery-app>
<script type="module">
import { LitElement, html } from 'lit';
class BreweryApp extends LitElement {
static get properties() {
return {
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);
</script>
</body>
</html>
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.
brewery-detail
element that displays the brewery's name, type and city.brewery-detail
elements in the brewery-app
, one for each brewery received from the OpenBreweryDB API.class BreweryDetail extends LitElement {
static get properties() {
return {
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>
`;
}
<!DOCTYPE html>
<html>
<body>
<brewery-app></brewery-app>
<script type="module">
import { LitElement, html } from 'lit';
class BreweryApp extends LitElement {
static get properties() {
return {
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);
class BreweryDetail extends LitElement {
static get properties() {
return {
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);
</script>
</body>
</html>
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.
<!DOCTYPE html>
<html>
<body>
<brewery-app></brewery-app>
<script type="module">
import { LitElement, html } from 'lit';
class BreweryApp extends LitElement {
static get properties() {
return {
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);
class BreweryDetail extends LitElement {
static get properties() {
return {
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);
</script>
</body>
</html>
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.visited } : 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`...`;
}
<!DOCTYPE html>
<html>
<body>
<brewery-app></brewery-app>
<script type="module">
import { LitElement, html } from 'lit';
class BreweryApp extends LitElement {
static get properties() {
return {
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);
class BreweryDetail extends LitElement {
static get properties() {
return {
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);
</script>
</body>
</html>
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.
<!DOCTYPE html>
<html>
<body>
<brewery-app></brewery-app>
<script type="module">
import { LitElement, html } from 'lit';
class BreweryApp extends LitElement {
static get properties() {
return {
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>
<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);
class BreweryDetail extends LitElement {
static get properties() {
return {
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);
</script>
</body>
</html>
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>
`;
<!DOCTYPE html>
<html>
<body>
<brewery-app></brewery-app>
<script type="module">
import { LitElement, html } from 'lit';
import '@material/mwc-button';
class BreweryApp extends LitElement {
static get properties() {
return {
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);
class BreweryDetail extends LitElement {
static get properties() {
return {
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);
</script>
</body>
</html>
For our brewery-detail
we created a separate web component. A web component creates a strong encapsulation boundary between the parent and child components. This is a great feature, we can develop components in complete isolation.
But sometimes this can be overkill for just simple templates, or we may want to have no boundaries so that we can share styles or select DOM nodes.
Since lit-html templates are actual javascript variables, we could write our template as a function which returns our template:
function BeerTemplate(beer) {
return html` <h1>${beer}</h1> `;
}
brewery-detail
component with a template function.We cannot fire any events from the template function. Instead, we should pass along the event handler from the parent component.
function breweryTemplate(brewery, toggleVisitedStatus) {
return html`
<h3>${brewery.name} (${brewery.visited ? 'visited' : 'not-visited'})</h3>
<p>brewery type: ${brewery.brewery_type}</p>
<p>city: ${brewery.city}</p>
<mwc-button @click=${toggleVisitedStatus}>
Mark as ${brewery.visited ? 'not-visited' : 'visited'}
</mwc-button>
`;
}
Then, to render the template:
html` <li>${breweryTemplate(brewery, () => this.toggleVisitedStatus(brewery))}</li> `;
<!DOCTYPE html>
<html>
<body>
<brewery-app></brewery-app>
<script type="module">
import { LitElement, html } from 'lit';
import '@material/mwc-button';
function breweryTemplate(brewery, toggleVisitedStatus) {
return html`
<h3>${brewery.name} (${brewery.visited ? 'visited' : 'not-visited'})</h3>
<p>brewery type: ${brewery.brewery_type}</p>
<p>city: ${brewery.city}</p>
<mwc-button @click=${toggleVisitedStatus}>
Mark as ${brewery.visited ? 'not-visited' : 'visited'}
</mwc-button>
`;
}
class BreweryApp extends LitElement {
static get properties() {
return {
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>${breweryTemplate(brewery, () => this.toggleVisitedStatus(brewery))}</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);
</script>
</body>
</html>
That's the end of the intermediate lit-html & lit-element 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