yarn add lit
example code:
import {LitElement, html} from 'lit';
export class MyElement extends LitElement {
render() {
return html`
<p>Hello world! From my-element.</p>
`;
}
}
customElements.define('my-element', MyElement);
Add properties:
static properties = {
message: {},
};
constructor() {
super();
this.message = 'Hello again.';
}
${this.message}
Event listeners
<button @click=${this.handleClick}>Click me!</button>
changeName(event) {
const input = event.target;
this.name = input.value;
}
More attributes:
?disabled=${!this.checked}
5 types of expressions:
<!-- Child nodes -->
<h1>${this.pageTitle}</h1>
<!-- Attribute -->
<div class=${this.myTheme}></div>
<!-- Boolean attribute -->
<p ?hidden=${this.isHidden}>I may be in hiding.</p>
<!-- Property -->
<input .value=${this.value}>
<!-- Event listener -->
<button @click=${() => {console.log("You clicked a button.")}}>...</button>
Working with lists
${this._listItems.map((item) =>
html`<li>${item.text}</li>`
)}
Example of a task list
${this._listItems.map((item) =>
html`<li>${item.text}</li>`
)}
get input() {
return this.renderRoot?.querySelector('#newitem') ?? null;
}
addToDo() {
this._listItems = [...this._listItems,
{text: this.input.value, completed: false}];
this.input.value = '';
}
Styles Add this as a class field:
static styles = css`
.completed {
text-decoration-line: line-through;
color: #777;
}
`;
They will be scoped to the component, using shadow DOM.
Add classes conditionally:
class=${item.completed ? 'completed' : ''}
or use classmap
Map
import {map} from 'lit/directives/map.js';
and later
<ul>
${map(this.items, (item) => html`<li>${item}</li>`)}
</ul>
Lit map is useful when you are working with anything that's not a js array.
Range
import {map} from 'lit/directives/map.js';
range(10)
Repeat
import {repeat} from 'lit/directives/repeat.js';
Reactive properties You need to add this code:
static properties = {
result: {},
};
Lit automatically adds accessors
Mutating doesn't trigger update!
You can trigger it yourself with requestUpdate(changedProperties)
You can also override shouldUpdate(changedProperties)
- by default, it always returns true
Example:
shouldUpdate(changedProperties) {
return !(changedProperties.size === 1 && changedProperties.has('duration'));
}
calculating stuff for update
willUpdate(changedProperties)
After update
updated(changedProperties)
Attributes
Attribute (html):
<input value="something">
Property (js):
input.value = 15;
Sometimes they have the same names.
You can turn off attribute to property conversion off:
date: {attribute: false},
you can also turn on property to attribute conversion:
reflect: true
Built in converters:
- String
- Bool
- Number
- Array
- Object
Example:
properties:
dateStr: {type: String, attribute: "date-str"}
how to display, the basic way:
willUpdate(changed) {
if (changed.has('dateStr') && this.dateStr) {
this.date = new Date(this.dateStr);
}
}
It's better to define a custom attribute converter example: new file:
'use strict';
export const dateConverter = (locale) => {
return {
toAttribute: (date) => {
return date.toLocaleDateString(locale);
},
fromAttribute: (value) => {
return new Date(value);
}
}
}
then import
import {dateConverter} from "./date-converter.js";
and add converter to property
static properties = {
date: {converter: dateConverter(navigator.language), reflect: true},
};
You won't need dateStr, or willUpdate.
Directive is a function that updates the value they render after the fact.
Make a directive
import {directive, Directive} from 'lit/directive.js';
class TimeAgoDirective extends Directive {
}
export const timeAgo = directive(TimeAgoDirective);
Add a render method with the arguments you want
render(time) {
return time.toDateString();
}
How to use it:
import
import {timeAgo} from './time-ago.js';
Then call the directive:
${timeAgo(timeCreated)}
for working with time, it's useful to get timeago
npm package
Instead of Directive, use AsyncDirective
import {directive, AsyncDirective} from 'lit/async-directive.js';
class TimeAgoDirective extends AsyncDirective {
It adds a few useful methods:
setValue()
- basically rerenders
disconnected()
- unsubscribe
reconnected()
- subscribe again
Example:
timer = undefined;
ensureTimerStarted() {
if (this.timer === undefined) {
this.timer = setInterval(() => {
/* do some periodic work */
}, 3000);
}
}
Then run it in update. Example:
update(part, [time]) {
if (this.isConnected) {
this.ensureTimerStarted();
}
return this.render(time);
}
Update the value: you need to store it in the class. update it whenever needed.
time;
update(part, [time]) {
this.time = time;
// dont change the rest
}
use setValue
where needed.
To avoid memory leaks. Add a method to unsubscribe:
ensureTimerStopped() {
clearInterval(this.timer);
this.timer = undefined;
}
and call it:
disconnected() {
this.ensureTimerStopped();
}
Then add reconnected:
reconnected() {
this.ensureTimerStarted();
}
You can test it: remove and re-add an element, for example on click.
handleClick() {
const parent = this.parentNode;
this.remove();
setTimeout(() => parent.appendChild(this), 1000);
}
add console logs.
You can use them in any expression.
<comment-card user="lit Developer" time=${timeAgo(timeCreated)}
const helloHTML = html`
<svg>
${svg`<text>Hello, SVG!</text>`}
</svg>
`;
const createElement = (chars) => svg`
<text
dominant-baseline="hanging"
font-family="monospace"
font-size="24px">
${chars}
</text>
`;
static properties = {
chars: {type: String},
};
constructor() {
super();
this.chars = 'lit';
}
<def>
contains svg elements without rendering them.
const helloDefs = svg`
<defs>
<text id="chars">Hello defs!</text>
</defs>
`;
Then to use a defined element, <use href="#chars">
const helloDefs = svg`
<defs>
<text id="chars">Hello defs!</text>
</defs>
<use href="#chars"></use>
`;
you can apply effects, like
<use
href="#chars"
transform="rotate(180, 0,0)">
</use>
<g>
applies properties to all the children:
<g transform="translate(50, 50)">
<use
href="#chars"
transform="rotate(${currRotation}, 0,0)">
</use>
</g>
Create a clip path:
const helloClipPath = svg`
<clipPath id="rect-clip">
<rect width="200" height="200"></rect>
</clipPath>
`;
Then refer to it in a rect:
const helloTile = svg`
<rect
clip-path="url(#rect-clip)"
width="300"
height="300"
fill="#000000">
</rect>
<pattern>
repeats an element over 2D space
select <patternUnits>
, like userSpaceOnUse
const helloPattern = svg`
<pattern patternUnits="userSpaceOnUse">
${createTile()}
</pattern>
`;
<pattern>
needs an id, and then add "fill"
const helloPattern = svg`
<pattern
id="hello-pattern"
patternUnits="userSpaceOnUse">
${createTile()}
</pattern>
`;
const helloPatternFill = svg`
<rect fill="url(#hello-pattern)" width="200" height="200"></rect>
`;
Adding CSS to SVG
const helloSvgCss = css`
.background {
fill: #000000;
}
`;
Add a class to an element like this:
const helloCssClasses = html`
<rect class="background"></rect>
`;
You can also add properties:
const helloCssCustomProperties = css`
.background {
fill: var(--background-color, #000000);
}
`;
Insert <slot></slot>
in your component.
It will show it's content (children)
<my-component>This will be displayed</my-component>
It's basically this
scope of an element. Useful for styling.
Make a tooltip component with slot inside. Style it.
render() {
return html`<slot></slot>`;
}
Hide / Show
show = () => {
this.style.cssText = '';
};
hide = () => {
this.style.display = 'none';
};
Hide by default:
connectedCallback() {
super.connectedCallback();
this.hide();
}
Add event listeners: Define an array of relevant events like this:
const enterEvents = ['pointerenter', 'focus'];
const leaveEvents = ['pointerleave', 'blur', 'keydown', 'click'];
add event listeners:
_target = null;
get target() {
return this._target;
}
set target(target) {
// Remove events from existing target
if (this.target) {
enterEvents.forEach((name) =>
this.target.removeEventListener(name, this.show)
);
leaveEvents.forEach((name) =>
this.target.removeEventListener(name, this.hide)
);
}
// Add events to new target
if (target) {
enterEvents.forEach((name) => target.addEventListener(name, this.show));
leaveEvents.forEach((name) => target.addEventListener(name, this.hide));
}
this._target = target;
}
Not sure what this does:
connectedCallback() {
//...
this.target ??= this.previousElementSibling;
}
??=
only sets something if it's not set.
Basic positioning:
static properties = {
offset: {type: Number},
};
constructor() {
super();
this.offset = 4;
}
show = () => {
this.style.cssText = '';
const {x, y, height} = this.target.getBoundingClientRect();
this.style.left = `${x}px`;
this.style.top = `${y + height + this.offset}px`;
};
Improve positioning:
import {
computePosition,
autoPlacement,
offset,
shift
} from '@floating-ui/dom';
change the positioning code:
show = () => {
this.style.cssText = '';
computePosition(this.target, this, {
strategy: 'fixed',
middleware: [
offset(this.offset),
shift(),
autoPlacement({allowedPlacements: ['top', 'bottom']}),
],
}).then(({x, y}) => {
this.style.left = `${x}px`;
this.style.top = `${y}px`;
});
};
Adding animation: Add a state:
static properties = {
offset: {type: Number},
showing: {reflect: true, type: Boolean},
};
constructor() {
super();
this.offset = 4;
this.showing = false;
}
Then add css:
:host {
/* ... */
opacity: 0;
transform: scale(0.75);
transition: opacity, transform;
transition-duration: 0.33s;
}
:host([showing]) {
opacity: 1;
transform: scale(1);
}
Show it:
show = () => {
// ...
this.showing = true;
};
Hide and add method on finishing hiding:
hide = () => {
this.showing = false;
};
finishHide = () => {
if (!this.showing) {
this.style.display = 'none';
}
};
Attach finishHide in the constructor:
this.addEventListener('transitionend', this.finishHide);
Adding lazy loading:
static lazy(target, callback) {
const createTooltip = () => {
const tooltip = document.createElement('simple-tooltip');
callback(tooltip);
target.parentNode!.insertBefore(tooltip, target.nextSibling);
tooltip.show();
// We only need to create the tooltip once, so ignore all future events.
enterEvents.forEach(
(eventName) => target.removeEventListener(eventName, createTooltip));
};
enterEvents.forEach(
(eventName) => target.addEventListener(eventName, createTooltip));
}
Then remove the old tooltip, and add
import {SimpleTooltip} from './simple-tooltip.js';
export class MyContent extends LitElement {
// ...
firstUpdated() {
const greeting = this.shadowRoot.getElementById('greeting');
SimpleTooltip.lazy(greeting, (tooltip) => {
tooltip.textContent = `${this.name}, there's coffee available in the lounge.`;
});
}
Using directives
setupLazy() {
this.didSetupLazy = true;
SimpleTooltip.lazy(this.part.element, (tooltip) => {
this.tooltip = tooltip;
this.renderTooltipContent();
});
}
import {html, css, LitElement, render} from 'lit';
//...
renderTooltipContent() {
render(this.tooltipContent, this.tooltip, this.part.options);
}
<p>
<span ${tooltip(html`${this.name}, there's coffee available in the lounge.`)}>
Hello, ${this.name}!
</span>
</p>
import {html, css, LitElement, render} from 'lit';
import {Directive, directive} from 'lit/directive.js';
/* playground-fold */
import {computePosition, autoPlacement, offset, shift} from '@floating-ui/dom';
const enterEvents = ['pointerenter', 'focus'];
const leaveEvents = ['pointerleave', 'blur', 'keydown', 'click'];
export class SimpleTooltip extends LitElement {
static properties = {
offset: {type: Number},
showing: {reflect: true, type: Boolean},
};
// Lazy creation
static lazy(target, callback) {
const createTooltip = () => {
const tooltip = document.createElement('simple-tooltip');
callback(tooltip);
target.parentNode.insertBefore(tooltip, target.nextSibling);
tooltip.show();
// We only need to create the tooltip once, so ignore all future events.
enterEvents.forEach((eventName) =>
target.removeEventListener(eventName, createTooltip)
);
};
enterEvents.forEach((eventName) =>
target.addEventListener(eventName, createTooltip)
);
}
static styles = css`
:host {
display: inline-block;
position: fixed;
padding: 4px;
border: 1px solid darkgray;
border-radius: 4px;
background: #ccc;
pointer-events: none;
/* Animate in */
opacity: 0;
transform: scale(0.75);
transition: opacity, transform;
transition-duration: 0.33s;
}
:host([showing]) {
opacity: 1;
transform: scale(1);
}
`;
_target = null;
get target() {
return this._target;
}
set target(target) {
// Remove events from existing target
if (this.target) {
enterEvents.forEach((name) =>
this.target.removeEventListener(name, this.show)
);
leaveEvents.forEach((name) =>
this.target.removeEventListener(name, this.hide)
);
}
// Add events to new target
if (target) {
enterEvents.forEach((name) => target.addEventListener(name, this.show));
leaveEvents.forEach((name) => target.addEventListener(name, this.hide));
}
this._target = target;
}
constructor() {
super();
// Finish hiding at end of animation
this.addEventListener('transitionend', this.finishHide);
this.offset = 4;
// Attribute for styling "showing"
this.showing = false;
}
connectedCallback() {
super.connectedCallback();
this.target ??= this.previousElementSibling;
this.finishHide();
}
render() {
return html`<slot></slot>`;
}
show = () => {
this.style.cssText = '';
computePosition(this.target, this, {
strategy: 'fixed',
middleware: [
offset(this.offset),
shift(),
autoPlacement({allowedPlacements: ['top', 'bottom']}),
],
}).then(({x, y}) => {
this.style.left = `${x}px`;
this.style.top = `${y}px`;
});
this.showing = true;
};
hide = () => {
this.showing = false;
};
finishHide = () => {
if (!this.showing) {
this.style.display = 'none';
}
};
}
customElements.define('simple-tooltip', SimpleTooltip);
/* playground-fold-end */
class TooltipDirective extends Directive {
didSetupLazy = false;
tooltipContent;
part;
tooltip;
// A directive must define a render method.
render(tooltipContent = '') {}
update(part, [tooltipContent]) {
this.tooltipContent = tooltipContent;
this.part = part;
if (!this.didSetupLazy) {
this.setupLazy();
}
if (this.tooltip) {
this.renderTooltipContent();
}
}
setupLazy() {
this.didSetupLazy = true;
SimpleTooltip.lazy(this.part.element, (tooltip) => {
this.tooltip = tooltip;
this.renderTooltipContent();
});
}
renderTooltipContent() {
render(this.tooltipContent, this.tooltip, this.part.options);
}
}
export const tooltip = directive(TooltipDirective);
Use this selector:
::slotted(*)
It works same as
:host
but for slotted elements.
Start with the DOM. Add slotted elements:
<div>
<slot></slot>
</div>
and style it.
Added a selected property:
static properties = { selected: {type: Number} };
selectedInternal = 0;
constructor () {
super();
this.selected = 0;
}
get maxSelected() {
return this.childElementCount - 1;
}
hasValidSelected() {
return this.selected >= 0 && this.selected <= this.maxSelected;
}
render() {
if (this.hasValidSelected()) {
this.selectedInternal = this.selected;
}
To only render the selected element:
<div class="fit">
<slot name="selected"></slot>
</div>
previous = 0;
updated(changedProperties) {
if (changedProperties.has('selected') && this.hasValidSelected()) {
this.updateSlots();
this.previous = this.selected;
}
}
updateSlots() {
this.children[this.previous]?.removeAttribute('slot');
this.children[this.selected]?.setAttribute('slot', 'selected');
}
use this in html
<motion-carousel id="carousel" selected="4">
Then allow changing of the element add click event
<div class="fit" @click=${this.clickHandler}>
clickHandler(e) {
const i = this.selected + (Number(!e.shiftKey) || -1);
this.selected = i > this.maxSelected ? 0 : i < 0 ? this.maxSelected : i;
const change = new CustomEvent('change',
{detail: this.selected, bubbles: true, composed: true});
this.dispatchEvent(change);
}
Adding animations: update code like this.
import {styleMap} from 'lit/directives/style-map.js';
// ...
left = 0;
render() {
if (this.hasValidSelected()) {
this.selectedInternal = this.selected;
}
const animateLeft = ``;
const selectedLeft = ``;
const previousLeft = ``;
return html`
<div class="fit"
@click=${this.clickHandler}
style=${styleMap({left: animateLeft})}
>
<div class="fit" style=${styleMap({left: previousLeft})}>
<slot name="previous"></slot>
</div>
<div class="fit selected" style=${styleMap({left: selectedLeft})}>
<slot name="selected"></slot>
</div>
</div>
`;
}
To slot the previous item:
get selectedSlot() {
return (this.__selectedSlot ??=
this.renderRoot?.querySelector('slot[name="selected"]') ?? null);
}
get previousSlot() {
return (this.__previousSlot ??=
this.renderRoot?.querySelector('slot[name="previous"]') ?? null);
}
Then update slots:
updateSlots() {
// unset old slot state
this.selectedSlot.assignedElements()[0]?.removeAttribute('slot');
this.previousSlot.assignedElements()[0]?.removeAttribute('slot');
// set slots
this.children[this.previous]?.setAttribute('slot', 'previous');
this.children[this.selected]?.setAttribute('slot', 'selected');
}
Now position the elements:
New Render:
render() {
const p = this.selectedInternal;
const s = (this.selectedInternal =
this.hasValidSelected() ? this.selected : this.selectedInternal);
const shouldMove = this.hasUpdated && s !== p;
const atStart = p === 0;
const toStart = s === 0;
const atEnd = p === this.maxSelected;
const toEnd = s === this.maxSelected;
const shouldAdvance = shouldMove &&
(atEnd ? toStart : atStart ? !toEnd : s > p);
const delta = (shouldMove ? Number(shouldAdvance) || -1 : 0) * 100;
this.left -= delta;
const animateLeft = `${this.left}%`;
const selectedLeft = `${-this.left}%`;
const previousLeft = `${-this.left - delta}%`;
And add directives:
import {animate} from '@lit-labs/motion';
and
${animate()}
to your html elements
Start with basic element:
import {html, LitElement} from 'lit';
export class WordViewer extends LitElement {
render() {
// TODO: Render something!
return html`<pre>Expressive Template</pre>`;
}
}
customElements.define('word-viewer', WordViewer);
Add reactive properties:
static properties = {
words: {},
};
constructor() {
super();
this.words = 'initial value';
}
and change the words through html:
<word-viewer words="These.are.words"></word-viewer>
Displaying one at a time:
add index:
idx: {state: true},
to properties
state true - means they will not have an attribute.
this.idx = 0;
in constructor
Display only one:
const splitWords = this.words.split('.');
const word = splitWords[this.idx % splitWords.length];
return html`<pre>${word}</pre>`;
Cycling the words:
tickToNextWord = () => { this.idx += 1; };
Add callbacks:
intervalTimer;
connectedCallback() {
super.connectedCallback();
this.intervalTimer = setInterval(this.tickToNextWord, 1000);
}
disconnectedCallback() {
super.disconnectedCallback();
clearInterval(this.intervalTimer);
this.intervalTimer = undefined;
}
Styling: just use static styles css.
Adding events:
add a state:
playDirection: {state: true},
in constructor:
this.playDirection = 1;
fix the index counting:
const idx = ((this.idx % splitWords.length) + splitWords.length) % splitWords.length;
change tick:
tickToNextWord = () => { this.idx += this.playDirection; };
and add a method to switch direction
switchPlayDirection() {
this.playDirection *= -1;
}
and render:
@click=${this.switchPlayDirection}
Dynamically change style:
use classMap directive (it helps change a list of classes)
import {classMap} from 'lit/directives/class-map.js';
and
return html`<pre
class="${classMap({ backwards: this.playDirection === -1 })}"
...
>${word}</pre>`;
```
Then add styles.