[go: up one dir, main page]

DEV Community

Cover image for lit-html templates from zero to hero
Julio Castillo Anselmi
Julio Castillo Anselmi

Posted on • Edited on

lit-html templates from zero to hero

📣 UPDATE! 📣
Now lit-html and LitElement are unified under Lit.
I'm writing new posts about Lit, meanwhile you can:

  • read this post because the principles are the same
  • upgrade your code with this guide
  • visit Lit site to know what's new

You can also use lit-html standalone

After a very smooth introduction to lit-html, I will go hard and get my hands dirty. I will show you how to write templates so this will be a session full of code!

I know you are here because you want to learn LitElement, so you read this title and find out that we continue with lit-html. The reason why I've structured these sessions this way is because lit-html is the basis for LitElement and also because I want you to see the great potential and versatility of lit-html by itself, probably the perfect match for a small or medium project. I assure you that everything you learn about lit-html will be used in LitElement. Be patient, we will arrive soon.

Writing templates

☝️Remember from last post: templates are written using tagged template literals. We tag the template literal with the html function provided by lit-html. This function returns a TemplateResult object.
render is the other function provided by lit-html. render receives a TemplateResult object and renders it into the DOM.

This is a very simple example:

// import functions from lit-html library
import {html, render} from 'lit-html';

// define the template as function that takes the data
// and returns a TemplateResult object
const template = name => {
  return html`
    <p>Hello ${name}</p>
  `;
};

// node where the template will be rendered
const mainNode = document.getElementById('main');

// render the template
render(template('Silvia'), mainNode);

// render the template again with different data
render(template('Benjamin'), mainNode);
Enter fullscreen mode Exit fullscreen mode

The dynamic parts of a template are JavaScript expressions that are binded with values. TemplateResult supports bindings of certain data types that we will see now.

Supported bindings

  • Text: An expression that is resolved as text can be used as the text content of a node. Be aware that an empty string ('') in a text binding will render an empty text node.

  • Attribute: an expression that returns a text can be used as the value of an attribute. When an attribute behaves like a boolean (it is present or not) we denote it with the name of the attribute preceded by ?. The value of the expression must be boolean, if it is true lit-html puts the attribute and if it is false it removes it.

  • Property: An expression can also be binded to a JavaScript node's property. We write the property name with a . at the beginning. In that case, the type of expression must match the type of the property (it could be any type even a complex object).

  • Event: An expression can be the handler of an event. For this we write the event name preceded by @ and in this case we have several alternatives for the binding.

    • Global handler function: the expression resolves to a global function that will handle the event.
    • Inline function: the expression resolves to an inline function.
    • Instance function: the expression resolves to a function that belongs to your object.
    • Event listener object: the expression returns an object that must have a function named clickHandler.
  • HTML node element: the expression can return a DOM node.

  • TemplateResult: the expression can be another TemplateResult object. This makes it possible to have composition of nested templates.

  • Iterable of TemplateResult objects: expressions that returns an array or iterables of TemplateResult objects.

  • Promise: the expression can return a promise that must be resolved returning a valid binding value.

// Text binding
html`<p>${someText}</p>`;
html`<div>${capitalize(user.name, user.firstName)}</div>`;

/**** Given theses variables and values... **************

let someText = 'Lorem ipsum';
let user = { name : 'JEN', firstName: 'MONROE' };  

***** The code above will render... *********************

<p>Lore ipsum</p>
<div>Jen Monroe</div>
*********************************************************/


// Attribute binding
html`<div class=${classList}>Stylish text.</div>`;

/**** Given these variables and values... *****************

let classList = ['main', 'required', 'modern'].join(' ');

***** The code above will render... ***********************

<div class="main required modern">Stylish text.</div>
***********************************************************/


// Attribute binding (boolean)
html`<input type="submit" ?disabled=${formWithErrors} value="Submit">
     <span ?hidden=${!formWithErrors}>Form has errors!</span>`;


/**** Given these variables and values... *****************

let formWithErrors = true;

***** The code above will render... ***********************

<input type="submit" disabled value="Submit">`;
<span>Form has errors!</span>`
***********************************************************/


// Property binding
html`<custom-list .items=${users} id="user-list"></custom-list>`;

/**** Given these variables and values... *****************

const users = ['Diego', 'Ana', 'Laura', 'Piero'];
const customList = document.getElementById('user-list');
console.log(customList.items);

***** The code above will show in console *****************

 ["Diego", "Ana", "Laura", "Piero"]

***********************************************************/


// Event binding to a global handler function
html`<button @click=${handleClick}>Click Me!</button>`; 


// Event binding to an inline function
html`<button @click=${()=>console.log('clicked')}>Click Me!</button>`; 


// Event binding to an instance function
html`<button @click=${this.handleClick}>Click Me!</button>`;


// Event binding to listener object
html`<button @click=${clickHandler}>Click Me!</button>`;

const clickHandler = {
  handleEvent(e) { console.log('clicked!'); }
};


// Binding to a DOM node
const h1 = document.createElement('h1');
h1.textContent = 'Chapter 1'
const page = html`${h1}<p>Once upon a time...</p>`;

/***** The code above will render... **********************

<h1>Chapter 1</h1>
<p>Once upon a time...</p>
***********************************************************/


// Binding to a TemplateResult object
const header = html`<h1>Chapter 1</h1>`;
const article = html`<article>${header}<p>Once upon a time...</p></article>`;

/***** The code above will render... **********************

<article>
  <h1>Chapter 1</h1>
  <p>Once upon a time...</p>
</article>
***********************************************************/


// Binding to arrays/iterables
const items = [1, 2, 3];
const listItems = items.map(i => html`<li>${2*i}</li>`);
const template = html`<ul>${listItems}</ul>`;

/***** The code above will render... **********************

<ul>
 <li>2</li>
 <li>4</li>
 <li>6</li>
</ul>
***********************************************************/


// Binding to a promise
const text = fetch(url).then(response => response.text());
const page = () => html`<p>${text}</p>`;

/***********************************************************
Let's say that after some seconds the fetch operation 
resolves with a the string 'some text...'

Until the promise is resolved, the code above will render
<p></p> 

Once the promise is resolved, it will render...
<p>some text...</p>

***********************************************************/
Enter fullscreen mode Exit fullscreen mode

Composition

One consequence of having bindings to expressions that return TemplateResult objects is that by composition we can create templates using other templates. The composition allows:

  • Create a complex template using simpler templates.
  • Refactor a complex template by diving it into simpler templates.
  • Reuse of templates (the use of JavaScript modules makes reuse much easier, for example, a module of common templates, partial templates, etc.)
const header = data => html`
    <h1>${data.title}<h1>
    ${data.subtitle ? html`<h2>${data.subtitle}<h2>` : ''}`;


const main = data => html`<p>${makeParagraph(data.text)}</p>`;

const composedTemplate = data => html`
    ${header(data)}
    ${main(data)}`;
Enter fullscreen mode Exit fullscreen mode

Conditionals

A template can have parts that are only visible if a condition is met, or it could have parts that are represented in different ways depending on one or more conditions. These behaviors can be expressed using conditional expressions such as the ternary operator ? or conditional structures such as the if or switch-case statements.

// using ternary operator
const main = data => html`
    ${data.text ?
        html`<p>${data.text}</p>` :
        html`<img src=${data.image}></img>`}`;

// using if
const main = data => {
    if (data.text) {
        return html`<p>${data.text}</p>` :
    } else {
        return html`<img src=${data.image}></img>`;
    }
}

// using switch-case
const main = data => {
    switch (data.screenSize) {
    case 's':
        return html`<img src="${data.image}-small.png"></img>`;
    case 'm':
        return html`<img src="${data.image}-medium.png"></img>`;
    case 'l':
        return html`<img src="${data.image}-medium.png"></img>
                    <img src="${data.logo}.png"></img>`;
    default:
        return html`<p>${data.text}</p>`;
    }
}

Enter fullscreen mode Exit fullscreen mode

Iterations

It is very common for a part of a template to be repeated with different values. Think of a list, where the part that paints each item is always the same but with different values. For these cases we have already seen that it is possible to make a binding to expressions that return iterables from TemplateResult objects. This is the way we can repeat parts or blocks in a template.

const weekDays = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Staurday', 'Sunday'];

// loops with arrays
html`<ul>${weekDays.map((day) => html`<li>${day}</li>`)}</ul>`;

// loop statement (better if DOM node update is cheap)
const itemTemplates = [];
for (const day of weekDays) {
  itemTemplates.push(html`<li>${day}</li>`);
}
html`<ul>${itemTemplates}</ul>`;

/*************************************
* We'll see the use of directives 
* in a future post !!
*************************************/
// repeat directive (better if moving DOM node is cheap)
html`<ul>${repeat(weekDays, day => day /*id*/, (day, index) => html`<li>${day}</li>`)}</ul>`;
Enter fullscreen mode Exit fullscreen mode

Setting things up

lit-html is distributed as a npm package so to install it run the command:

npm install --save lit-html
Enter fullscreen mode Exit fullscreen mode

lit-html uses JavaScript modules therefore to use the html and render functions we have to import them from the module.

At the moment, browsers don't import ES6 modules using the short syntax:

import {html, render} from 'lit-html'
Enter fullscreen mode Exit fullscreen mode

We should write the full path to the module

import {html, render} from './node_modules/lit-html/lit-html.js'
Enter fullscreen mode Exit fullscreen mode

But if you want to use the short syntax you can use a built tool such as Webpack or you can install the Polymer command line tool that can handle the short syntax import when it serves the application.

With Polymer CLI:

## install it
npm install -g polymer-cli

## use it
polymer serve
Enter fullscreen mode Exit fullscreen mode

You're ready to start coding! I strongly recommend you to experiment and try your own examples. The best way to learn is by doing!

Conclusion

As we have seen, a template is pure JavaScript code so any expression and valid structure of the language can be used inside the template. There are so many ways to define a template that is only limited by the preferences or styles of each developer. The result is that the templates turn out to be tremendously expressive and because it is just JavaScript you don't need to learn nothing new.

Top comments (9)

Collapse
 
kuscamara profile image
Kus Cámara

Great article!

As a small addition suitable for a comment, it's worth mentioning the until directive that can be used along with promises to render a content until the promise resolves.

const content = fetch('./content.txt').then(r => r.text());

html`${until(content, html`<span>Loading...</span>`)}`
Enter fullscreen mode Exit fullscreen mode

lit-html.polymer-project.org/guide...

Collapse
 
julcasans profile image
Julio Castillo Anselmi

Hi Kus! Thank you for your contribution. Really useful. I want to write a post about directives and I will incluye your comment.

Collapse
 
chanduprem profile image
Chandu Reddy

Hi Julio!

This is a great article for understanding lit-html...
But was wondering , if u will be releasing some sort of article for directives in lit-html...
The official documentation little hard to grasp...

Collapse
 
hyperpress profile image
John Teague

Very nicely done. Really like how this steps beyond the beginners examples but is still easy enough for other framework devs to see the upside and proofs that there's no component created by any framework that can't be created using LitElement built web components that are lightweight, high speed, and low drag. Keep em coming 😉

Collapse
 
julcasans profile image
Julio Castillo Anselmi

Hi John! Thank you for your comment. I'm glad you liked this article. I write these posts because I think it is necessary to show the world the excellent work that the Polymer team has done. I ❤️ LitElement. I hope one day it will be as popular as React or Vue and the web components rule the web.

Collapse
 
newlegendmedia profile image
Jeff Hilton

I have used mustache style templating for a long time on the client and even on the server using mustache PHP. I’m just taking a hard look now at web components and by reading thru articles like this one I’ve got a good idea how the lit-* stuff works and it seems like a strong and lightweight approach.

One thing I still haven’t wrapped my head around is having the templates so integrated with the JavaScript code. I usually kept templates as separate files or as essentially HTML type strings. Do you see any benefits or problems with the templates and code being coupled like this? One of the first things I found myself thinking about lit-HTML was how could I store templates separately for reuse and to be able replace a template used by a component dynamically based on conditions etc.

Any thoughts on this? Perhaps once I put them in use more, the benefits of this approach will be more clear.

Collapse
 
julcasans profile image
Julio Castillo Anselmi

Hi! Thanks for leaving your comment.

Honestly, at first, I was not convinced by the idea of ​​having HTML, CSS and JavaScript in the same file, much less that everything was embedded in a JavaScript class. But I have gotten used to it and now I think it is a matter of personal taste rather than a problem or technical convenience. And I could also say it's a standard considering React and Vue. The good point about it is that I can understand the whole piece just seeing one file.

Anyway, if you want to have the HTML and CSS code in separate files, you can do it, but they will have to be ES6 modules so you can reuse them by importing them.

Imagine you have a template menu and a template footer that you will surely reuse in many pages templates.
So you can have a separate file for each one.

menu.js


export const menu = (opts) =>  html`
  <menu type="context" id="popup-menu">
    <menuitem>${opts.op1}</menuitem>
    <menuitem>${opts.op2}</menuitem>
    <hr/>
    <menuitem>${opts.op3}</menuitem>
  </menu>`;

footer.js


export const footer = (data) =>  html`<footer>
  <p>Posted by: ${data.author}</p>
  <p>Contact information: <a href="mailto:${data.email}">${data.email}</a>.</p>
</footer>`;

So you can reuse them in your pages by importing each module. (Yes, I know it's not HTML, it has to be a JavaScript module with export clause, but I think it's pretty convenient and thus you get reusable pieces)

home-page.js

import {menu} from 'menu.js';
import {footer} 'footer.js';
import {html, render} from 'lit-html';

const myData = { .... } // here you have an object with the information you need

const homePage = data => html`
    ${menu(data)}

     <!-- your homePage content here -->

    ${footer(data)}`;

render(homePage(myData), document.body);

I hope I have clarified something. Do you feel OK with this approach?

Collapse
 
newlegendmedia profile image
Jeff Hilton

Yes, that’s great. I wasn’t getting that they could be imported and reused that way. I really like the promise of web components and now they seem to be more like first class parts of JavaScript. Thanks for your response and examples. Time to create some sweet new components 😙

Thread Thread
 
julcasans profile image
Julio Castillo Anselmi

Your're welcome! I would like to see those components ;-)