Intro to LitElement

What is LitElement?

LitElement is a base class for creating web components.

Think of it as a lightweight library that helps you build ShadowDOM web components.

What the heck is ShadowDOM?

For more info about ShadowDOM check

http://slides.com/alininayeh/shadow-dom-intro

Shadow DOM refers to the ability of the browser to include a subtree of DOM elements into the rendering of a document, but not into the main document DOM tree.

 

https://glazkov.com/2011/01/14/what-the-heck-is-shadow-dom​

Getting started with LitElement

Let's play a bit with lit-element

Just follow the instructions from the next slides

Create a new folder and run npm init

# create a new folder
mkdir lit-element-demo

# go to that folder
cd lit-element-demo

#run npm init
npm init -y

Setup

Install lit-element

# install lit-element as a dependency
npm install lit-element --save

Setup

Install webpack

# install webpack, webpack-cli, html-webpack-plugin, and webpack-dev-server as dev dependencies
npm install webpack webpack-cli html-webpack-plugin webpack-dev-server --save-dev

Setup

Install typescript

# install typescript and typescript-loader as dev dependencies
npm install typescript ts-loader --save-dev

Setup

Install support for styling lit-element components

# install node-sass, style-loader, sass-loader, and lit-css-loader as dev dependencies
npm install node-sass style-loader sass-loader lit-css-loader --save-dev

Setup

Setup webpack.config.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: './src/index.ts',
  devtool: 'inline-source-map',
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: 'ts-loader',
        exclude: /node_modules/
      },
      {
        test: /\.lit\.scss$/,
        use: ['lit-css-loader', 'sass-loader']
      }
    ]
  },
  resolve: {
    extensions: [ '.tsx', '.ts', '.js' ]
  },
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist')
  },
  plugins: [
    new HtmlWebpackPlugin()
  ],
  devServer: {
    contentBase: path.join(__dirname, 'dist'),
    compress: true,
    port: 9000
  }
};

Setup

Setup tsconfig.json

// These options are used to set up and enable some features for typescript
{
  "compilerOptions": {
    "outDir": "./dist/",
	"sourceMap": true,
	"experimentalDecorators": true,
	"module": "es6",
	"target": "es6",
	"moduleResolution": "node"
  }
}

Setup

Setup declarations.d.ts

// This enables proper reading of the styles used for elements
// The styles are named as [name].list.scss
declare module '*.lit.scss' {
    import { CSSResult } from "lit-element";
    const css: CSSResult;
    export default css;
}

Setup

Create index.ts file

// create a new directory called src and add an index.ts file
console.log('It works!');

Setup

Add some scripts in package.json

{
  "name": "lit-element-demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    // add these scripts
    "start": "webpack-dev-server",
    "build": "webpack"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "lit-element": "^2.2.1"
  },
  "devDependencies": {
    "html-webpack-plugin": "^3.2.0",
    "lit-css-loader": "0.0.3",
    "node-sass": "^4.13.0",
    "sass-loader": "^8.0.0",
    "style-loader": "^1.0.1",
    "ts-loader": "^6.2.1",
    "typescript": "^3.7.2",
    "webpack": "^4.41.2",
    "webpack-cli": "^3.3.10",
    "webpack-dev-server": "^3.9.0"
  }
}

Setup

Let's test the initial setup

# run this command
npm start

# then open localhost:9000

Setup

Create a folder in /src called components, then create a button.ts file

// src/components/button.ts
import { LitElement, customElement, html } from 'lit-element';

@customElement('button-component')
export class Button extends LitElement {
    render() {
        return html`<button>Click me</button>`;
    }
}

Create a basic component

Import the Button class in index.ts

// src/index.ts
import './components/button';

// Create a div with id=root, then append the web component
const div = document.createElement('div');
div.id = 'root';
document.body.appendChild(div);
document.querySelector('#root').innerHTML = '<button-component></button-component>';

// test on localhost:9000

Create a basic component

Create a stylesheet for the button-component component

// src/components/button.lit.scss
button {
  background: green;
  border: none;
  color: white;
  padding: 10px;
}

Create a basic component

Add the stylesheet to the button-component component

Create a basic component

// src/components/button.ts
import { LitElement, customElement, html } from 'lit-element';
import styles from './button.lit.scss';

@customElement('button-component')
export class Button extends LitElement {
    static styles = styles;
    
    render() {
        return html`<button>Click me</button>`;
    }
}

// test on localhost:9000

Let's build a todo list using lit-element

Delete button.ts and button.lit.scss, keep the rest and continue following the instructions

Create the main placeholder

// src/components/todo-list-placeholder.ts
import { customElement, LitElement, html } from 'lit-element';

@customElement('todo-list-placeholder')
export class TodoListPlaceholder extends LitElement {
    render() {
        return html`
            <div class="todo-list-placeholder">
                <div class="todo-list-header-placeholder">
                </div>
                <div class="todo-list-content-placeholder">
                </div>
            </div>
        `;   
    }
}

Create the todo list

Create the main component

// src/components/todo-list.ts
import { customElement, LitElement, html } from 'lit-element';
import './todo-list-placeholder';

@customElement('todo-list')
export class TodoList extends LitElement {
    render() {
        return html`
            <div class="todo-list">
                <todo-list-placeholder>
                </todo-list-placeholder>
            </div>
        `;   
    }
}

Create the todo list

Import the main component

// src/index.ts
import './components/todo-list';

const div = document.createElement('div');
div.id = 'root';
document.body.appendChild(div);
document.querySelector('#root').innerHTML = '<todo-list></todo-list>';

Create the todo list

Create the header

// src/components/todo-list-header.ts
import { customElement, LitElement, html } from 'lit-element';

@customElement('todo-list-header')
export class TodoListHeader extends LitElement {
    render() {
        return html`
            <div class="todo-list-header">
                <input type="text" placeholder="Add item..." />
                <button>Add</button>
            </div>
        `;   
    }
}

Create the todo list

Make space in the placeholder for the header

// src/components/todo-list-placeholder.ts
import { customElement, LitElement, html } from 'lit-element';

@customElement('todo-list-placeholder')
export class TodoListPlaceholder extends LitElement {
    render() {
        return html`
            <div class="todo-list-placeholder">
                <div class="todo-list-header-placeholder">
                    <slot></slot>
                </div>
                <div class="todo-list-content-placeholder">
                </div>
            </div>
        `;   
    }
}

Create the todo list

Add the header

// src/components/todo-list.ts
import { customElement, LitElement, html } from 'lit-element';
import './todo-list-placeholder';
import './todo-list-header';

@customElement('todo-list')
export class TodoList extends LitElement {
    render() {
        return html`
            <div class="todo-list">
                <todo-list-placeholder>
                    <todo-list-header></todo-list-header>
                </todo-list-placeholder>
            </div>
        `;   
    }
}

Create the todo list

Create the content

// src/components/todo-list-content.ts
import { customElement, LitElement, html } from 'lit-element';

@customElement('todo-list-content')
export class TodoListContent extends LitElement {
    render() {
        return html`
            <div class="todo-list-content">
                <div class="todo-list-item">
                    <input type="checkbox" />
                    Item 1
                    <button>Delete</button>
                </div>
                <div class="todo-list-item">
                    <input type="checkbox" />
                    Item 2
                    <button>Delete</button>
                </div>
                <div class="todo-list-item">
                    <input type="checkbox" />
                    Item 3
                    <button>Delete</button>
                </div>
            </div>
        `;   
    }
}

Create the todo list

Make space in the placeholder for the content

// src/components/todo-list-placeholder.ts
import { customElement, LitElement, html } from 'lit-element';

@customElement('todo-list-placeholder')
export class TodoListPlaceholder extends LitElement {
    render() {
        return html`
            <div class="todo-list-placeholder">
                <div class="todo-list-header-placeholder">
                    <slot name="header"></slot>
                </div>
                <div class="todo-list-content-placeholder">
                    <slot name="content"></slot>
                </div>
            </div>
        `;   
    }
}

Create the todo list

Add the content

// src/components/todo-list.ts
import { customElement, LitElement, html } from 'lit-element';
import './todo-list-placeholder';
import './todo-list-header';
import './todo-list-content';

@customElement('todo-list')
export class TodoList extends LitElement {
    render() {
        return html`
            <div class="todo-list">
                <todo-list-placeholder>
                    <todo-list-header slot="header"></todo-list-header>
                    <todo-list-content slot="content"></todo-list-content>
                </todo-list-placeholder>
            </div>
        `;   
    }
}

Create the todo list

Move the list items to a separate component

// src/components/todo-list-item.ts
import { customElement, LitElement, html } from 'lit-element';

@customElement('todo-list-item')
export class TodoListItem extends LitElement {
    render() {
        return html`
            <div class="todo-list-item">
                <input type="checkbox" />
                <span><slot></slot></span>
                <button>Delete</button>
            </div>
        `;
    }
}

Create the todo list

Move the list items to a separate component

// src/components/todo-list-content.ts
import { customElement, LitElement, html } from 'lit-element';
import './todo-list-item';

@customElement('todo-list-content')
export class TodoListContent extends LitElement {
    render() {
        return html`
            <div class="todo-list-content">
                <todo-list-item>Item 1</todo-list-item>
                <todo-list-item>Item 2</todo-list-item>
                <todo-list-item>Item 3</todo-list-item>
            </div>
        `;   
    }
}

Create the todo list

A bit of styling

// src/components/todo-list-placeholder.lit.scss
.todo-list-placeholder {
    border: 1px solid #ddd;
    padding: 10px;
}

Create the todo list

A bit of styling

// src/components/todo-list-placeholder.ts
import { customElement, LitElement, html } from 'lit-element';
import styles from './todo-list-placeholder.lit.scss';

@customElement('todo-list-placeholder')
export class TodoListPlaceholder extends LitElement {
    static styles = styles;
    
    render() {
        return html`
            <div class="todo-list-placeholder">
                <div class="todo-list-header-placeholder">
                    <slot name="header"></slot>
                </div>
                <div class="todo-list-content-placeholder">
                    <slot name="content"></slot>
                </div>
            </div>
        `;   
    }
}

Create the todo list

A bit of styling

// src/components/todo-list-header.lit.scss
.todo-list-header {
    display: flex;
    margin-bottom: 10px;
    
    input {
        flex-grow: 1;
        margin-right: 10px;
        padding: 10px;
    }

    button {
        background: green;
        border: none;
        color: white;
        flex-basis: 100px;
    }
}

Create the todo list

A bit of styling

// src/components/todo-list-header.ts
import { customElement, LitElement, html } from 'lit-element';
import styles from './todo-list-header.lit.scss';

@customElement('todo-list-header')
export class TodoListHeader extends LitElement {
    static styles = styles;
    
    render() {
        return html`
            <div class="todo-list-header">
                <input type="text" placeholder="Add item..." />
                <button>Add</button>
            </div>
        `;   
    }
}

Create the todo list

A bit of styling

// src/components/todo-list-item.lit.scss
:host {
    display: block;
}

.todo-list-item {
    border: 1px solid #ddd;
    padding: 10px;
    display: flex;
    font-family: sans-serif;
    align-items: center;

    span {
        flex-grow: 1;
        margin-left: 10px;
    }

    &.checked span {
        text-decoration: line-through;
    }

    button {
        background: red;
        padding: 5px;
        color: white;
        border: none;
    }
}

Create the todo list

A bit of styling

// src/components/todo-list-item.ts
import { customElement, LitElement, html } from 'lit-element';
import styles from './todo-list-item.lit.scss';

@customElement('todo-list-item')
export class TodoListItem extends LitElement {
    static styles = styles;

    render() {
        return html`
            <div class="todo-list-item">
                <input type="checkbox" />
                <span><slot></slot></span>
                <button>Delete</button>
            </div>
        `;
    }
}

Create the todo list

A bit of styling

// src/components/todo-list-content.lit.scss
.todo-list-content {
    todo-list-item {
        margin-bottom: 10px;

        &:last-of-type {
            margin-bottom: 0;
        }
    }
}

Create the todo list

A bit of styling

// src/components/todo-list-content.ts
import { customElement, LitElement, html } from 'lit-element';
import './todo-list-item';
import styles from './todo-list-content.lit.scss';

@customElement('todo-list-content')
export class TodoListContent extends LitElement {
    static styles = styles;

    render() {
        return html`
            <div class="todo-list-content">
                <todo-list-item>Item 1</todo-list-item>
                <todo-list-item>Item 2</todo-list-item>
                <todo-list-item>Item 3</todo-list-item>
            </div>
        `;   
    }
}

Create the todo list

Submit an event when adding a new item

// src/components/todo-list-header.ts
import { customElement, html, LitElement, query } from "lit-element";
import styles from "./todo-list-header.lit.scss";

@customElement("todo-list-header")
export class TodoListHeader extends LitElement {
    static styles = styles;

    @query("input")
    textfield: HTMLInputElement;

    private addItem = () => {
        const value = this.textfield.value;
        if (!value) return;

        this.dispatchEvent(new CustomEvent("addItem", {
            detail: {
                value: value,
                checked: false,
                id: new Date().getTime() + Math.random()
            }
        }));

        this.textfield.value = "";
    }
    
    render() {
        return html`
            <div class="todo-list-header">
                <input type="text" placeholder="Add item..." />
                <button @click="${this.addItem}">Add</button>
            </div>
        `;
    }
}

Create the todo list

Make the todo-list component listen to the event

// src/components/todo-list.ts
import { customElement, html, internalProperty, LitElement } from "lit-element";
import "./todo-list-placeholder";
import "./todo-list-header";
import "./todo-list-content";

@customElement("todo-list")
export class TodoList extends LitElement {
    @internalProperty()
    private items = [];

    private addItem = (e: CustomEvent) => {
        this.items = [...this.items, e.detail];
        
    };

    render() {
        return html`
            <div class="todo-list">
                <todo-list-placeholder>
                    <todo-list-header slot="header" @addItem="${this.addItem}"></todo-list-header>
                    <todo-list-content slot="content" .items="${this.items}"></todo-list-content>
                </todo-list-placeholder>
            </div>
        `;
    }
}

Create the todo list

Show the items in the todo-list-content component

// src/components/todo-list-content.ts
import { customElement, html, LitElement, property } from "lit-element";
import "./todo-list-item";
import styles from "./todo-list-content.lit.scss";

@customElement("todo-list-content")
export class TodoListContent extends LitElement {
    static styles = styles;

    @property({type: Object})
    items = [];
    
    render() {
        return html`
            <div class="todo-list-content">
                ${this.items.map(item => (
                    html`<todo-list-item>${item.value}</todo-list-item>`
                ))}
            </div>
        `;
    }
}

Create the todo list

Show items as checked

// src/components/todo-list-content.ts
import { customElement, html, LitElement, property } from "lit-element";
import "./todo-list-item";
import styles from "./todo-list-content.lit.scss";

@customElement("todo-list-content")
export class TodoListContent extends LitElement {
    static styles = styles;

    @property({type: Object})
    items = [];
    
    render() {
        return html`
            <div class="todo-list-content">
                ${this.items.map(item => (
                    html`<todo-list-item ?checked="${item.checked}">${item.value}</todo-list-item>`
                ))}
            </div>
        `;
    }
}

Create the todo list

Show items as checked

// src/components/todo-list-item.ts
import { customElement, LitElement, html, property } from 'lit-element';
import styles from './todo-list-item.lit.scss';

@customElement('todo-list-item')
export class TodoListItem extends LitElement {
    static styles = styles;

    @property({type: Boolean})
    checked = false;

    render() {
        return html`
            <div class="todo-list-item">
                <input type="checkbox" ?checked="${this.checked}" />
                <span><slot></slot></span>
                <button>Delete</button>
            </div>
        `;
    }
}

Create the todo list

Check and uncheck items

// src/components/todo-list-item.ts
import { customElement, LitElement, html, property } from 'lit-element';
import { classMap } from 'lit-html/directives/class-map';
import styles from './todo-list-item.lit.scss';

@customElement('todo-list-item')
export class TodoListItem extends LitElement {
    static styles = styles;

    @property({type: Boolean})
    checked = false;

    private checkItem = () => {
        this.checked = !this.checked;
    };

    render() {
        return html`
            <div @click="${this.checkItem}" class="${classMap({"todo-list-item": true, "checked": this.checked})}">
                <input type="checkbox" ?checked="${this.checked}" />
                <span><slot></slot></span>
                <button>Delete</button>
            </div>
        `;
    }
}

Create the todo list

Delete items

// src/components/todo-list-content.ts
import { customElement, html, LitElement, property } from "lit-element";
import "./todo-list-item";
import styles from "./todo-list-content.lit.scss";

@customElement("todo-list-content")
export class TodoListContent extends LitElement {
    static styles = styles;

    @property({type: Object})
    items = [];
    
    render() {
        return html`
            <div class="todo-list-content">
                ${this.items.map(item => (
                    html`<todo-list-item ?checked="${item.checked}" itemId="${item.id}">${item.value}</todo-list-item>`
                ))}
            </div>
        `;
    }
}

Create the todo list

Delete items

// src/components/todo-list-item.ts
import { customElement, html, LitElement, property } from "lit-element";
import { classMap } from 'lit-html/directives/class-map';
import styles from "./todo-list-item.lit.scss";

@customElement("todo-list-item")
class TodoListItem extends LitElement {
    static styles = styles;

    @property({type: Boolean})
    checked = false;

    @property({type: Number})
    itemId = 0;

    private checkItem = () => {
        this.checked = !this.checked;
    };

    private deleteItem = (e: Event) => {
        e.stopImmediatePropagation();
        
        this.dispatchEvent(new CustomEvent("deleteItem", {
            detail: {
                id: this.itemId
            },
            bubbles: true,
            composed: true
        }));
    };
    
    render() {
        return html`
            <div class="${classMap({"todo-list-item": true, "checked": this.checked})}" @click="${this.checkItem}">
                <input type="checkbox" ?checked="${this.checked}" />
                <span><slot></slot></span>
                <button @click="${this.deleteItem}">Delete</button>
            </div>
        `;
    }
}

Create the todo list

Delete items

// src/components/todo-list.ts
import { customElement, html, internalProperty, LitElement } from "lit-element";
import "./todo-list-placeholder";
import "./todo-list-header";
import "./todo-list-content";

@customElement("todo-list")
export class TodoList extends LitElement {
    @internalProperty()
    private items = [];

    private addItem = (e: CustomEvent) => {
        this.items = [...this.items, e.detail];
    };

    private deleteItem = (e: CustomEvent) => {
        this.items = this.items.filter(item => item.id !== e.detail.id);
    };

    render() {
        return html`
            <div class="todo-list">
                <todo-list-placeholder>
                    <todo-list-header slot="header" @addItem="${this.addItem}"></todo-list-header>
                    <todo-list-content slot="content" .items="${this.items}" @deleteItem="${this.deleteItem}"></todo-list-content>
                </todo-list-placeholder>
            </div>
        `;
    }
}

Create the todo list

Done!

Intro to Lit Element

By alininayeh

Intro to Lit Element

  • 534