LitElement is currently in development. As long as LitElement is, so is create-lit-app. This repo will be kept up to date with the latest version of LitElement, but there are things that haven't been finalized yet and you can expect some changes.
Demo: https://create-lit-app.herokuapp.com/
You don’t need to install or configure tools like Webpack or Babel. They are preconfigured so that you can focus on the code.
Clone/fork the create-lit-app-advanced repo or install the CLI if you want to get started quickly building a fullstack LitHTML app with:
- Routing
- Express api
- Redux
- Build with webpack
- Quick start
- Folder Structure
- Redux
- Routing
- Decorators
- Adding an api
- Usage
- Basic template
- Passing props
- Event handlers
- Conditional rendering
- Setting default values
- Arrays
- Updating arrays and objects
- Attributes
- Reflecting props to attributes
- Adding styles
- Querying dom
- Making api calls
- Using directives
- Installing components
- Upwards data flow
- Slotted components
- Light dom
- Typescript
- Lifecycle
- Cheatsheet
- Polyfills
- Installing a dependency
- Testing your components
- Add LitElement to a website
- Extensions
- Frequently asked questions
- Browser support
- Contributing
- Credits
- Further reading
- Acknowledgements
npm install --global create-lit-app
create-lit-app my-app
cd my-app
npm start
Start webpack-dev-server
on localhost http://127.0.0.1:3000
:
npm run start
Run tests:
npm run test
Run the production build:
npm run build
node server.js
As easy as that! Your app is ready to be deployed.
After creation, your project should look like this:
create-lit-app/
dist/
node_modules/
src/
assets/
favicon.ico
github.svg
logo.svg
hello-world.js
lit-app-styles.css
index.html
lit-app.js
test/
hello-world.html
index.html
.babelrc
.eslintignore
package-lock.json
package.json
README.md
server.js
webpack.config.js
Check out the create-lit-app-advanced repo for a full example.
Create-lit-app-advanced uses Vaadin Router for its routing. Check out the create-lit-app-advanced repo for a full example.
Instead of using the static get properties()
way of setting properties, you may also use decorators. Decorators require the following two babel plugins that live in your .babelrc
file.
.babelrc
:
[
"@babel/plugin-proposal-decorators",
{ "legacy": true }
],
[
"@babel/plugin-proposal-class-properties",
{ "loose": true }
]
Here's an example on how to use decorators:
import { LitElement, html, property } from '@polymer/lit-element/';
class DemoElement extends LitElement {
@property({type: String})
foo = 'bar';
render() {
return html`
<h1>${this.foo}</h1>
`;
}
}
customElements.define('demo-element', DemoElement);
You can edit the server.js
file and start adding endpoints straight away. Add the following to the devServer section in webpack.config.js
:
proxy: {
'/api': {
target: 'http://localhost:8000/',
secure: false
},
Check out the create-lit-app-advanced repo for a full example.
- Basic template
- Passing props
- Event handlers
- Conditional rendering
- Setting default values
- Arrays
- Updating arrays and objects
- Attributes
- Reflecting props to attributes
- Adding styles
- Querying dom
- Making api calls
- Using directives
- Installing components
- Upwards data flow
- Slotted components
- Light dom
- Typescript
-
render()
(protected): Implement to describe the element's DOM usinglit-html
. Ideally, therender
implementation is a pure function using only the element's current properties to describe the element template. This is the only method that must be implemented by subclasses. Note, sincerender()
is called byupdate()
, setting properties does not trigger an update, allowing property values to be computed and validated. -
shouldUpdate(changedProperties)
(protected): Implement to control if updating and rendering should occur when property values change orrequestUpdate()
is called. ThechangedProperties
argument is a Map with keys for the changed properties pointing to their previous values. By default, this method always returnstrue
, but this can be customized as an optimization to avoid updating work when changes occur, which should not be rendered. -
update(changedProperties)
(protected): This method callsrender()
and then useslit-html
in order to render the template DOM. It also updates any reflected attributes based on property values. Setting properties inside this method will not trigger another update. -
firstUpdated(changedProperties)
: (protected) Called after the element's DOM has been updated the first time, immediately beforeupdated()
is called. This method can be useful for capturing references to rendered static nodes that must be directly acted upon, for example inupdated()
. Setting properties inside this method will trigger the element to update. -
updated(changedProperties)
: (protected) Called whenever the element's DOM has been updated and rendered. Implement to perform post updating tasks via DOM APIs, for example, focusing an element. Setting properties inside this method will trigger the element to update. -
updateComplete
: Returns a Promise that resolves when the element has completed updating. The Promise value is a boolean that istrue
if the element completed the update without triggering another update. The Promise result isfalse
if a property was set insideupdated()
. This getter can be implemented to await additional state. For example, it is sometimes useful to await a rendered element before fulfilling this Promise. To do this, first awaitsuper.updateComplete
then any subsequent state. -
requestUpdate(name?, oldValue?)
: Call to request the element to asynchronously update regardless of whether or not any property changes are pending. This should be called when an element should update based on some state not triggered by setting a property. In this case, pass no arguments. It should also be called when manually implementing a property setter. In this case, pass the propertyname
andoldValue
to ensure that any configured property options are honored. Returns theupdateComplete
Promise which is resolved when the update completes. -
createRenderRoot()
(protected): Implement to customize where the element's template is rendered by returning an element into which to render. By default this creates a shadowRoot for the element. To render into the element's childNodes, returnthis
. See an example here
Example:
my-app.js
:
import { LitElement, html } from '@polymer/lit-element/';
import './lifecycle-demo.js';
class myApp extends LitElement {
static get properties() {
return {
showElement: { type: Boolean }
};
}
constructor() {
super();
this.showElement = true;
}
render() {
let { showElement } = this;
return html`
<!--
Removing the element from dom will trigger a `disconnectedCallback()` in `lifecycle-demo.js`,
adding it to dom will trigger a first `firstUpdated()` and a `updated()` in `lifecycle-demo.js`
-->
<button @click=${() => this.showElement = !this.showElement}>toggle element</button>
${showElement
? html`<lifecycle-demo></lifecycle-demo>`
: ''
}
`;
}
}
customElements.define('my-app', myApp);
lifecycle-demo.js
:
import { LitElement, html } from '@polymer/lit-element/';
class LifecycleDemo extends LitElement {
static get properties() {
return {
myArr: { type: Array }
};
}
constructor() {
super();
this.myArr = ['foo', 'bar'];
}
/**
* Called after the element's DOM has been updated the first time, immediately before updated()
* is called. This method can be useful for querying dom. Setting properties inside
* this method will trigger the element to update.
*/
firstUpdated() {
console.log('first updated!');
}
/**
* Implement to perform post-updating tasks via DOM APIs, for example, focusing an element.
* Setting properties inside this method will *not* trigger another update.
*/
updated(changedProps) {
super.updated(changedProps);
console.log('updated!');
}
/**
* Invoked each time the custom element is disconnected from the document's DOM.
* Useful for running clean up code.
*/
disconnectedCallback() {
console.log('disconnected!');
}
_addItem() {
this.myArr = [...this.myArr, 'baz'];
}
render() {
let { myArr } = this;
return html`
<!-- Adding an item will cause myArr to change, the property change will get picked up and trigger an update -->
<button @click=${this._addItem}>add item</button>
${myArr.map((item) => {
return html`<li>${item}</li>`;
})}
`;
}
}
customElements.define('lifecycle-demo', LifecycleDemo);
Text:
html`<h1>Hello ${name}</h1>`
Expression:
html`<div>${disabled ? 'Off' : 'On'}</div>`
Attribute:
html`<div id=${id}></div>`
Boolean Attribute:
html`<input type="checkbox" ?checked=${checked}>`
Property:
html`<input .value=${value}>`
Event Handler:
html`<button @click=${(e) => console.log('clicked')}>Click Me</button>`
Create-lit-app includes the following polyfills:
-
fetch()
viawhatwg-fetch
. -
Web Components
viawebcomponentsjs
. This loader performs client side feature detection and requests only needed polyfills. E.g., for IE11 it will load webcomponents-lite.js which includes full list of polyfills. But for Edge webcomponents-hi-ce-sd.js which contains polyfills for HTML Import, Custom Element and ShadowDOM. -
custom-elements-es5-adapter.js
According to the spec, only ES6 classes (https://html.spec.whatwg.org/multipage/scripting.html#custom-element-conformance) may be passed to the native customElements.define API. For best performnace, ES6 should be served to browsers that support it, and ES5 code should be serve to those that don't. Since this may not always be possible, it may make sense to compile and serve ES5 to all browsers. However, if you do so, ES5-style custom element classes will now not work on browsers with native Custom Elements because ES5-style classes cannot properly extend ES6 classes, like HTMLElement. As a workaround, if your project has been compiled to ES5, load custom-elements-es5-adapter.js before defining Custom Elements. This adapter will automatically wrap ES5. The adapter must NOT be compiled.
You may install other dependencies (for example, Axios) with npm
:
npm install --save axios
Alternatively you may use yarn
:
yarn add axios
This works for any library, not just axios
.
Create-lit-app uses karma for testing. You can start the test runner with npm test
, and edit test files in the test/
folder. Here's an example of a test:
test/hello-world.spec.js
:
import { html, render } from 'lit-html';
import { expect } from 'chai';
import '../src/hello-world';
describe('hello-world', () => {
let element;
const fixture = html`
<hello-world .greeting=${'Welcome'}></hello-world>
`;
beforeEach(async () => {
render(fixture, document.body);
element = document.body.firstElementChild;
await element.updateComplete;
});
afterEach(() => {
element.remove();
});
it('should render a welcome message', () => {
const title = element.shadowRoot.querySelector('h1');
expect(title.innerText).to.equal('Welcome');
});
});
In this section, we will show how to add a LitElement component to an existing HTML page. You can follow along with your own website, or create an empty HTML file to practice.
There will be no complicated tools or install requirements — to complete this section, you only need an internet connection, and a minute of your time.
<html>
<head>
<!-- Polyfills only needed for Firefox and Edge. -->
<script src="https://unpkg.com/@webcomponents/webcomponentsjs@next/webcomponents-loader.js"></script>
</head>
<body>
<!-- Works only on browsers that support Javascript modules like
Chrome, Safari, Firefox 60, Edge 17 -->
<script type="module">
import {LitElement, html} from 'https://unpkg.com/@polymer/lit-element@latest/lit-element.js?module';
class MyElement extends LitElement {
static get properties() {
return {
mood: { type: String }
}
}
render() {
const { mood } = this;
return html`<style> .mood { color: green; } </style>
Web Components are <span class="mood">${mood}</span>!`;
}
}
customElements.define('my-element', MyElement);
</script>
<my-element mood="great"></my-element>
</body>
</html>
These are some extensions made by the community on top of create-lit-app:
- create-lit-app-typescript - Get started with typescript.
- create-lit-app-advanced - Get started with redux, routing, and an express API.
- create-lit-app-auth - Get started with authentication using jwt, passportjs, and mongo. By Steve Heideman.
Lit-html lets you write HTML templates with JavaScript template literals and efficiently render and re-render those templates to DOM. Tagged template literals are a feature of ES6 that can span multiple lines, and contain javascript expressions. A tagged template literal could look something like this:
const planet = "world";
html`hello ${planet}!`;
Tagged template literals are just standard ES6 syntax. And these tags are actually just functions! Consider the following example:
function customFunction(strings) {
console.log(strings); // ["Hello universe!"]
}
customFunction`Hello universe!`;
They can also handle expressions:
const planet = "world";
function customFunction(strings, ...values) {
console.log(strings); // ["Hello ", "! five times two equals "]
console.log(values); // ["world", 10]
}
customFunction`Hello ${planet}! five times two equals ${ 5 * 2 }`;
And if we look in the source code we can see that's exactly what lit-html does:
/**
* Interprets a template literal as an HTML template that can efficiently
* render to and update a container.
*/
export const html = (strings: TemplateStringsArray, ...values: any[]) =>
new TemplateResult(strings, values, 'html', defaultTemplateProcessor);
Lit-html's html
tag doesn't return dom, it returns an object representing the template, called a TemplateResult.
A <template>
element is an inert fragment of DOM. Inside a <template>
, script don't run, images don't load, custom elements aren't upgraded, etc. <template>
s can be efficiently cloned. They're usually used to tell the HTML parser that a section of the document must not be instantiated when parsed, and will be managed by code at a later time, but it can also be created imperatively with createElement and innerHTML.
Lit-html creates HTML <template>
elements from the tagged template literals, and then clone's them to create new DOM.
On the initial render it clones the template, then walks it using the remembered placeholder positions, to create Part objects.
A Part is a "hole" in the DOM where values can be injected. lit-html includes two type of parts by default: NodePart and AttributePart, which let you set text content and attribute values respectively. The Parts, container, and template they were created from are grouped together in an object called a TemplateInstance.
LitElement reacts to changes in properties and rerenders your component if a value has changed. You can declare these properties in the properties getter:
static get properties() {
return {
myBoolean: { type: Boolean }
}
}
constructor() {
super();
this.myBoolean = false;
}
Setting this.myBoolean = true;
will trigger a rerender.
Deep changes in objects or arrays are not observed and need to be immutably set or require you to manually call this.requestUpdate()
. Consider the following example:
import { LitElement, html } from '@polymer/lit-element/';
class UpdatingDemo extends LitElement {
static get properties() {
return {
myObj: { type: Object }
};
}
constructor() {
super();
this.myObj = { id: 1, text: "foo" };
}
_updateObj() {
// this.myObj.text = "bar"; This change will not get picked up and wont trigger a rerender. You can however call this.requestUpdate(); to manually cause the rerender.
this.myObj = {...this.myObj, text: "bar"}; // This change will get picked up and cause your component to rerender.
}
render() {
const { myObj } = this;
return html`
<div>
${myObj.text}
<button @click=${this._updateObj}>Update</button>
</div>
`;
}
}
customElements.define('updating-demo', UpdatingDemo);
Lit-html uses them to keep track of its expression/parts locations, so it can update efficiently, consider the following example:
code:
class DemoEl extends LitElement {
static get properties() {
return {
myString: { type: String }
};
}
constructor(){
super();
this.myString = 'bar';
}
render() {
const { myString } = this;
return html`
<h1>foo</h1>
<h1>${myString}</h1>`; // will wrap `${myString}` with <!--->
}
}
output:
<demo-el>
#shadow-root (open)
<!---->
<h1>foo</h1>
<h1>
<!---->
"bar"
<!---->
</h1>
<!---->
</demo-el>
VDOM implementations keep a separate JavaScript structure representing the DOM structure in the browser. For all the changes to the structure the VDOM implementation will perform a diffing operation and will perform updates to the DOM itself.
While this method is effective, it does mean a lot of excessive processing is done. Lit-html leverages the ECMAScript ES6 tagged template literals feature to use native browser rendering engine implementations to perform the same task.
If you expect to the order of elements to change (swapping position of elements, deleting elements within the array) use repeat
. If your array length never changes, or if you only append to to it use map
.
If you have an array [a,b,c]
, map
will render 3 nodes. When when you change the array to [b,a,c]
the dom nodes stay in the same position but the data passed to the nodes changes
While repeat
will swap the nodes along with the data. this can be very useful if you modify dom nodes which isn't expressed in any of your data.
TL;DR use map for very small lists and very small templates. Iterating over a small hardcoded list of options to build a form for example.
Read this post by Leon Revill on shadow dom and the difference between the open
and closed
modes.
Screenreaders have no difficulty with piercing shadow dom. From the Polymer FAQ:
“A common misconception is that the Shadow DOM doesn’t play nicely with assistive technologies. The reality is that the Shadow DOM can in fact be traversed and any node with Shadow DOM has a shadowRoot property which points to its shadow document. Most assistive technologies hook directly into the browser’s rendering tree, so they just see the fully composed tree.”
Lots of libraries using global API like document.querySelector and relying on global CSS would be broken by Shadow DOM. Libraries themselves should be updated to work with the new concepts, where possible. Webcomponents are still in early stages of adaptation, lots of libraries would actually make great webcomponents, so if you're interested in using one of your favourite libraries, please consider contributing.
Some libraries could potentially still work by querying the shadowroot of an element.
Read more about it in this medium post by Rob Dodson
IE / Edge |
Firefox |
Chrome |
Safari |
iOS Safari |
Opera |
---|---|---|---|---|---|
IE11, Edge | last 2 versions | last 2 versions | last 2 versions | last 2 versions | last 2 versions |
We'd love to have your helping hand on create-lit-app! Feel free to create a pull request if you want to help out.
- Awesome lit-html
- litHTML
- Polymer slack
- Vaadin Router
- Polymer3-webpack-starter
- Polymer PWA starter kit
- Redux
- Owen Buckley of Project Evergreen
- Web-padawan
- Lars den Bakker