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 a CDN:

<!DOCTYPE html>

<html>
  <body>
    <script type="module">
      import { LitElement } from 'https://unpkg.com/lit-element?module';
    </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.


View final result
<!DOCTYPE html>
<html>
  <body>
    <brewery-app></brewery-app>

    <script type="module">
      import { LitElement, html } from 'https://unpkg.com/lit-element?module';

      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.

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.

Tasks to complete this step:

Tips

Using fetch 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;
    });
}
Example connectedCallback
class BreweryApp extends LitElement {
  connectedCallback() {
    super.connectedCallback();

    if (!this.breweries) {
      this.fetchBreweries();
    }
  }
}
Example of breweries rendered as JSON
render() {
  return html`
    <pre>${JSON.stringify(this.breweries, null, 2)}</pre>
  `;
}

View final result
<!DOCTYPE html>
<html>
  <body>
    <brewery-app></brewery-app>

    <script type="module">
      import { LitElement, html } from 'https://unpkg.com/lit-element?module';

      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.

Tasks to complete this step

Maintain a loading state while fetching
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;
}
Prevent rendering the main content while loading
render() {
  if (this.loading) {
    return html`<p>Loading...</p>`;
  }

  return html`
    <pre>${JSON.stringify(this.breweries, null, 2)}</pre>
  `;
}

View final result
<!DOCTYPE html>
<html>
  <body>
    <brewery-app></brewery-app>

    <script type="module">
      import { LitElement, html } from 'https://unpkg.com/lit-element?module';

      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.

Tasks to complete this step

Tips

Create an brewery component
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);
Display a list of breweries
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>
  `;
}

View final result
<!DOCTYPE html>
<html>
  <body>
    <brewery-app></brewery-app>

    <script type="module">
      import { LitElement, html } from 'https://unpkg.com/lit-element?module';

      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.

Tasks to complete this step

Tips

Conditional templating

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)} `;
  }
}
Adding event listeners With lit-html, you can add event listeners using the @ 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.


View final result
<!DOCTYPE html>
<html>
  <body>
    <brewery-app></brewery-app>

    <script type="module">
      import { LitElement, html } from 'https://unpkg.com/lit-element?module';

      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.

Tasks to complete this step

Tips

Remember that with LitElement, you need to use immutable data patterns. Otherwise, it will not be able to pick up data changes.

Firing events
_toggleVisitedStatus() {
  this.dispatchEvent(new CustomEvent('toggle-visited-status'));
}
Catch event from a list of elements

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>
    `,
  )}
`;
Update visited status in main app

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;
  });
}
Calculating derived data

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`...`;
}

View final result
<!DOCTYPE html>
<html>
  <body>
    <brewery-app></brewery-app>

    <script type="module">
      import { LitElement, html } from 'https://unpkg.com/lit-element?module';

      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.

Tasks to complete this step

Tips

Creating a filter

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.


View final result
<!DOCTYPE html>
<html>
  <body>
    <brewery-app></brewery-app>

    <script type="module">
      import { LitElement, html } from 'https://unpkg.com/lit-element?module';

      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 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);

      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.

Tasks to complete this step

Tips

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 'https://unpkg.com/@material/mwc-button?module';
Using the mwc-button component

<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>
`;

View final result
<!DOCTYPE html>
<html>
  <body>
    <brewery-app></brewery-app>

    <script type="module">
      import { LitElement, html } from 'https://unpkg.com/lit-element?module';
      import 'https://unpkg.com/@material/mwc-button?module';

      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> `;
}

Tasks to complete this step

Tips

Handling events in template functions

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> `;

View final result
<!DOCTYPE html>
<html>
  <body>
    <brewery-app></brewery-app>

    <script type="module">
      import { LitElement, html } from 'https://unpkg.com/lit-element?module';
      import 'https://unpkg.com/@material/mwc-button?module';

      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