Blog

LitElement templating, properties, and events

By  
Marcus Hellberg
Marcus Hellberg
·
On Dec 11, 2018 10:42:00 AM
·

In this second tutorial in a series about building LitElement apps, we cover templating, events, and properties in a LitElement component.

If you didn’t complete the first part, you can check out the source as a starting point:

Video tutorial for this part

Defining properties

Before we begin building the UI, we define the data model for the view. We’ll do this via LitElement properties. LitElement observes these properties and triggers an update of the component if any of them change.

views/todo-view.js
const VisibilityFilters = { (1)
SHOW_ALL: 'All',
SHOW_ACTIVE: 'Active',
SHOW_COMPLETED: 'Completed'
};

class TodoView extends LitElement {

static get properties() { (2)
return {
todos: { type: Array },
filter: { type: String },
task: { type: String }
};
}

constructor() { (3)
super();
this.todos = [];
this.filter = VisibilityFilters.SHOW_ALL;
this.task = '';
}

render() {
...
}
}
  1. Define an object with the different filter options

  2. Define a static getter for properties that returns an object that defines the property names and types.

  3. Create a constructor and set reasonable default values for the properties.

Installing UI components

We are going to use a text field, button and a few other UI components from the open source Vaadin component set to build the view. Begin by installing them through npm and importing them in the todo view JavaScript file.

$ npm install --save @vaadin/vaadin-text-field @vaadin/vaadin-button @vaadin/vaadin-checkbox @vaadin/vaadin-radio-button
views/todo-view.js
import '@vaadin/vaadin-text-field';
import '@vaadin/vaadin-button';
import '@vaadin/vaadin-checkbox';
import '@vaadin/vaadin-radio-button/vaadin-radio-button';
import '@vaadin/vaadin-radio-button/vaadin-radio-group';

Including the component registers them in the browser’s CustomElements registry the same way as we did with TodoView.

Writing a template with lit-html

lit-html uses JavaScript template strings as it’s format. That means you can use plain JavaScript in your templates. Also, it adds helpers for setting properties, boolean attributes and for listening to events.

value="${}"

Set an attribute (string)

.value="${}"

Set a property (object)

?value="${}"

Toggle a boolean attribute

@event="${}"

Bind an event

We’ll write the template inside the html`` template in the render() function. The tutorial omits the surrounding JavaScript method in the templating section for clarity.

views/todo-view.js
render() {
return html`
<p> Template goes here! </p>
`
;
}
Tip
There is a VS Code plugin for lit-html which gives you HTML syntax highlighting and code completion in lit-html templates. You can download it here.

The Prettier code formatter also supports lit-html syntax.

Adding todos

The first part of the template is the input field for adding new todos.

<div class="input-layout"
@keyup="${this.shortcutListener}">
(1)

<vaadin-text-field
placeholder="Task"
value="${this.task}" (2)
@change="${this.updateTask}">
(3)
</vaadin-text-field>

<vaadin-button
theme="primary"
@click="${this.addTodo}">
(4)
Add Todo
</vaadin-button>
</div>
  1. Listen for keyup events on the surrounding <div> so we can add todos with Enter.

  2. Bind the value of the text field to the task property.

  3. Listen for the change event on the text field and call this.updateTask

  4. Bind the button click event to this.addTodo

Then, implement the methods on TodoView:

views/todo-view.js
class TodoView extends LitElement {
static get properties() {...}
constructor() {...}
render() {...}

addTodo() {
if (this.task) {
this.todos = [...this.todos, { (1)
task: this.task,
complete: false
}];
this.task = ''; (2)
}
}

shortcutListener(e) {
if (e.key === 'Enter') { (3)
this.addTodo();
}
}

updateTask(e) {
this.task = e.target.value; (4)
}
}
  1. Create a new array with the new todo object

  2. Clear the task property

  3. If the keyup event originates from the Enter key, call this.addTodo()

  4. Update the task property to the value of the text field on change events

If you run the app, you should see the input layout and be able to input new tasks (although you won’t see them quite yet).

UI for entering new todos

Listing todos

Now that we can add todos, the next task is to display them. Loop through the todos array and produce a template for each todo.

Under the input layout, add the following:

<div class="todos-list">
${this.todos.map(
todo => html` (1)
<div class="todo-item">
<vaadin-checkbox
?checked="${todo.complete}" (2)
@change="${ e => this.updateTodoStatus(todo, e.target.checked)}">
(3)
${todo.task}
</vaadin-checkbox>
</div>
`
)
}
</div>
  1. Use the .map() operation to map each todo object to a lit-html template

  2. Bind the checked boolean attribute to the complete property on the todo object

  3. Call this.updateTodoStatus with the todo and a boolean for status on change events

Then, implement updateTodoStatus

views/todo-view.js
class TodoView extends LitElement {
static get properties() {...}
constructor() {...}
render() {...}
shortcutListener(e) {...}
updateTask(e) {...}
addTodo() {...}

updateTodoStatus(updatedTodo, complete) {
this.todos = this.todos.map(todo =>
updatedTodo === todo ? { ...updatedTodo, complete } : todo
);
}
}
Note
LitElement only observes changes to objects and arrays, not changes to their contents. That’s why we always create a new array of todos with the changes. Using immutable data structures also makes it easier for us to start using Redux for state management in the next part.
Adding and listing todos

Filtering todos

The last piece of functionality we add in this part of the tutorial is filtering and clearing todos.

Add the following to the template:

<vaadin-radio-group
class="visibility-filters"
value="${this.filter}"
@value-changed="${this.filterChanged}">
(1)

${Object.values(VisibilityFilters).map( (2)
filter => html`
<vaadin-radio-button value="${filter}">
${filter}
</vaadin-radio-button>`
)}
</vaadin-radio-group>
<vaadin-button
@click="${this.clearCompleted}">
(3)
Clear completed
</vaadin-button>
  1. Bind the value to the filter property and the value-changed event to the this.filterChanged method

  2. Loop over the filter values and create a radio button for each

  3. Hook up the clear button click event to this.clearCopleted

Then add the JavaScript methods:

views/todo-view.js
class TodoView extends LitElement {
static get properties() {...}
constructor() {...}
render() {...}
shortcutListener(e) {...}
updateTask(e) {...}
addTodo() {...}
updateTodoStatus(updatedTodo, complete) {...}

filterChanged(e) { (1)
this.filter = e.target.value;
}

clearCompleted() { (2)
this.todos = this.todos.filter(todo => !todo.complete);
}

applyFilter(todos) { (3)
switch (this.filter) {
case VisibilityFilters.SHOW_ACTIVE:
return todos.filter(todo => !todo.complete);
case VisibilityFilters.SHOW_COMPLETED:
return todos.filter(todo => todo.complete);
default:
return todos;
}
}
}
  1. Update the filter property based on the event value.

  2. Update the todos property to a new array only containing the non-completed todos.

  3. Create a method that returns only the todos that pass the filter criteria.

Finally, update the template to use the applyFilter method to only show the filtered todos:

<div class="todos-list">
- ${this.todos.map(
+ ${this.applyFilter(this.todos).map(
todo => html`
Filtering todos

Adding CSS styles

Finally, let’s add a few finishing touches with CSS.

At the top of your template, add the following <style> tag:

<style>
todo-view { (1)
display: block;
max-width: 800px;
margin: 0 auto;
}
todo-view .input-layout {
width: 100%;
display: flex;
}
todo-view .input-layout vaadin-text-field {
flex: 1;
margin-right: var(--spacing); (2)
}
todo-view .todos-list {
margin-top: var(--spacing);
}
todo-view .visibility-filters {
margin-top: calc(4 * var(--spacing));
}
</style>
  1. Scope styles to this view.

  2. Using a CSS custom property defined in styles.css.

By default, the content of a LitElement component is shielded from external CSS by a Shadow DOM. This is a feature that is valuable when building reusable leaf-node components. When building an application, it is often best to not create a shadow root to allow application-level styling.

Tip
Mattia Astorino wrote a good blog post explaining when to use Shadow DOM and when to avoid it.

At the end of TodoView, add the following method to tell LitElement to render directly into the light DOM.

views/todo-view.js
createRenderRoot() {
return this;
}

Run the app, and you should see the CSS styles applied.

The complete, styled todo view

Next

In the next part of the tutorial, we extract the state from the view component into a central Redux store.

lit
Marcus Hellberg
Marcus Hellberg
Marcus is the VP of Developer Relations at Vaadin. His daily work includes everything from writing blogs and tech demos to attending events and giving presentations on all things Vaadin and web-related. You can reach out to him on Twitter @marcushellberg.
Other posts by Marcus Hellberg