A simple base class for creating fast, lightweight web components
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
Built-in directives for advanced templating patterns, conditional rendering, and performance optimization. Directives extend lit-html's templating capabilities with reusable, efficient patterns for common UI scenarios.
Efficiently renders lists with keyed items for optimal performance.
/**
* Renders a list of items with efficient updates using keys
* @param items Iterable of items to render
* @param keyFn Function to generate unique keys for items
* @param template Function to render each item
* @returns DirectiveResult for list rendering
*/
function repeat<T>(
items: Iterable<T>,
keyFn: KeyFn<T>,
template: ItemTemplate<T>
): DirectiveResult<typeof RepeatDirective>;
type KeyFn<T> = (item: T, index: number) => unknown;
type ItemTemplate<T> = (item: T, index: number) => unknown;Usage Examples:
import { LitElement, html, repeat } from "lit-element";
interface User {
id: number;
name: string;
email: string;
}
class UserList extends LitElement {
users: User[] = [
{ id: 1, name: "Alice", email: "alice@example.com" },
{ id: 2, name: "Bob", email: "bob@example.com" },
{ id: 3, name: "Charlie", email: "charlie@example.com" }
];
render() {
return html`
<ul>
${repeat(
this.users,
(user) => user.id, // Key function
(user, index) => html` // Template function
<li>
<strong>${user.name}</strong> (${index + 1})
<br />
<small>${user.email}</small>
</li>
`
)}
</ul>
`;
}
}Conditionally applies CSS classes based on an object map.
/**
* Sets CSS classes on an element based on a map of class names to boolean values
* @param classInfo Object mapping class names to boolean conditions
* @returns DirectiveResult for conditional class application
*/
function classMap(classInfo: ClassInfo): DirectiveResult<typeof ClassMapDirective>;
interface ClassInfo {
[name: string]: string | boolean | number;
}Usage Examples:
import { LitElement, html, classMap } from "lit-element";
class ButtonElement extends LitElement {
disabled = false;
primary = true;
loading = false;
size = "medium";
render() {
const classes = {
'btn': true,
'btn-primary': this.primary,
'btn-secondary': !this.primary,
'btn-disabled': this.disabled,
'btn-loading': this.loading,
[`btn-${this.size}`]: this.size
};
return html`
<button class=${classMap(classes)} ?disabled=${this.disabled}>
${this.loading ? 'Loading...' : 'Click me'}
</button>
`;
}
}Conditionally applies CSS styles based on an object map.
/**
* Sets CSS styles on an element based on a map of style properties to values
* @param styleInfo Object mapping CSS properties to values
* @returns DirectiveResult for conditional style application
*/
function styleMap(styleInfo: StyleInfo): DirectiveResult<typeof StyleMapDirective>;
interface StyleInfo {
[name: string]: string | number | undefined | null;
}Usage Examples:
import { LitElement, html, styleMap } from "lit-element";
class ProgressBar extends LitElement {
progress = 50; // 0-100
color = "#007bff";
height = 20;
render() {
const barStyles = {
width: `${this.progress}%`,
height: `${this.height}px`,
backgroundColor: this.color,
transition: 'width 0.3s ease'
};
const containerStyles = {
width: '100%',
height: `${this.height}px`,
backgroundColor: '#e9ecef',
borderRadius: '4px',
overflow: 'hidden'
};
return html`
<div style=${styleMap(containerStyles)}>
<div style=${styleMap(barStyles)}></div>
</div>
<p>${this.progress}% complete</p>
`;
}
}Conditionally renders content based on a boolean condition.
/**
* Conditionally renders one of two templates based on a condition
* @param condition Boolean condition to evaluate
* @param trueCase Template to render when condition is true
* @param falseCase Template to render when condition is false
* @returns Rendered template or nothing
*/
function when<T, F>(
condition: boolean,
trueCase: () => T,
falseCase?: () => F
): T | F | typeof nothing;Usage Examples:
import { LitElement, html, when } from "lit-element";
class ConditionalElement extends LitElement {
isLoggedIn = false;
userName = "Alice";
hasPermission = true;
render() {
return html`
<div>
${when(
this.isLoggedIn,
() => html`
<div class="user-area">
<h2>Welcome, ${this.userName}!</h2>
${when(
this.hasPermission,
() => html`<button>Admin Panel</button>`,
() => html`<p>Limited access</p>`
)}
</div>
`,
() => html`
<div class="login-area">
<h2>Please log in</h2>
<button>Login</button>
</div>
`
)}
</div>
`;
}
}Renders different templates based on a switch-like pattern.
/**
* Renders different templates based on a value, like a switch statement
* @param value Value to switch on
* @param cases Object mapping values to template functions
* @param defaultCase Default template when no case matches
* @returns Rendered template based on value
*/
function choose<T, V>(
value: T,
cases: Record<string, () => V>,
defaultCase?: () => V
): V | typeof nothing;Usage Examples:
import { LitElement, html, choose } from "lit-element";
type Status = 'loading' | 'success' | 'error' | 'idle';
class StatusElement extends LitElement {
status: Status = 'idle';
message = '';
render() {
return html`
<div class="status-display">
${choose(this.status, {
loading: () => html`
<div class="spinner"></div>
<p>Loading...</p>
`,
success: () => html`
<div class="success-icon">✓</div>
<p>Success! ${this.message}</p>
`,
error: () => html`
<div class="error-icon">✗</div>
<p>Error: ${this.message}</p>
`,
idle: () => html`
<p>Ready to start</p>
<button @click=${this._start}>Start</button>
`
})}
</div>
`;
}
private _start() {
this.status = 'loading';
// Simulate async operation
setTimeout(() => {
this.status = Math.random() > 0.5 ? 'success' : 'error';
this.message = this.status === 'success' ? 'Operation completed' : 'Something went wrong';
}, 2000);
}
}Only sets an attribute if the value is defined (not null or undefined).
/**
* Sets an attribute only if the value is defined (not null or undefined)
* @param value Value to conditionally set
* @returns Value or nothing if undefined/null
*/
function ifDefined(value: unknown): unknown | typeof nothing;Usage Examples:
import { LitElement, html, ifDefined } from "lit-element";
class ImageElement extends LitElement {
src?: string;
alt?: string;
title?: string;
width?: number;
height?: number;
render() {
return html`
<img
src=${ifDefined(this.src)}
alt=${ifDefined(this.alt)}
title=${ifDefined(this.title)}
width=${ifDefined(this.width)}
height=${ifDefined(this.height)}
/>
`;
}
}
// Only defined attributes will be set:
// <img src="image.jpg" alt="Description" /> (title, width, height omitted)Prevents re-evaluation of expensive templates unless dependencies change.
/**
* Guards template re-evaluation by checking if dependencies have changed
* @param value Value or values to watch for changes
* @param fn Function that returns the template to render
* @returns Guarded template result
*/
function guard(value: unknown, fn: () => unknown): DirectiveResult<typeof GuardDirective>;Usage Examples:
import { LitElement, html, guard } from "lit-element";
class ExpensiveList extends LitElement {
data: any[] = [];
sortOrder = 'asc';
filterText = '';
private _processData() {
console.log('Processing data (expensive operation)');
return this.data
.filter(item => item.name.toLowerCase().includes(this.filterText.toLowerCase()))
.sort((a, b) => {
const order = this.sortOrder === 'asc' ? 1 : -1;
return a.name.localeCompare(b.name) * order;
});
}
render() {
return html`
<div>
<input
@input=${(e: any) => this.filterText = e.target.value}
placeholder="Filter..."
/>
<button @click=${() => this.sortOrder = this.sortOrder === 'asc' ? 'desc' : 'asc'}>
Sort ${this.sortOrder === 'asc' ? '↓' : '↑'}
</button>
<!-- Only re-process when data, sortOrder, or filterText change -->
${guard([this.data, this.sortOrder, this.filterText], () => {
const processedData = this._processData();
return html`
<ul>
${processedData.map(item => html`<li>${item.name}</li>`)}
</ul>
`;
})}
</div>
`;
}
}Caches template results based on a key for performance optimization.
/**
* Caches template results based on a value/key
* @param value Key value for caching
* @returns Cached directive result
*/
function cache(value: unknown): DirectiveResult<typeof CacheDirective>;Usage Examples:
import { LitElement, html, cache } from "lit-element";
class TabContainer extends LitElement {
activeTab = 'tab1';
tabs = ['tab1', 'tab2', 'tab3'];
private _renderTabContent(tabId: string) {
// Expensive template rendering
console.log(`Rendering content for ${tabId}`);
return html`
<div class="tab-content">
<h3>Content for ${tabId}</h3>
<p>This is expensive content that benefits from caching.</p>
<ul>
${Array.from({length: 100}, (_, i) => html`<li>Item ${i + 1}</li>`)}
</ul>
</div>
`;
}
render() {
return html`
<div class="tabs">
${this.tabs.map(tab => html`
<button
class=${tab === this.activeTab ? 'active' : ''}
@click=${() => this.activeTab = tab}
>
${tab}
</button>
`)}
</div>
<!-- Cache expensive content for each tab -->
${cache(this.activeTab)}
${this.activeTab === 'tab1' ? this._renderTabContent('tab1') : ''}
${this.activeTab === 'tab2' ? this._renderTabContent('tab2') : ''}
${this.activeTab === 'tab3' ? this._renderTabContent('tab3') : ''}
`;
}
}Forces a property binding to always update, even if the value hasn't changed.
/**
* Forces live binding that always updates regardless of value changes
* @param value Value to bind with live updates
* @returns Live binding directive result
*/
function live(value: unknown): DirectiveResult<typeof LiveDirective>;Usage Examples:
import { LitElement, html, live } from "lit-element";
class InputElement extends LitElement {
value = '';
render() {
return html`
<div>
<!-- Regular binding might not update if user modifies input -->
<input .value=${this.value} @input=${this._updateValue} />
<!-- Live binding ensures input always reflects property value -->
<input .value=${live(this.value)} @input=${this._updateValue} />
<button @click=${this._reset}>Reset</button>
<p>Current value: ${this.value}</p>
</div>
`;
}
private _updateValue(e: Event) {
this.value = (e.target as HTMLInputElement).value;
}
private _reset() {
this.value = '';
// Live binding ensures input is cleared even if user has modified it
}
}Creates references to DOM elements for imperative access.
/**
* Creates a reference to a DOM element
* @param callback Optional callback function called with the element
* @returns Ref directive result
*/
function ref(callback?: (element?: Element) => void): DirectiveResult<typeof RefDirective>;Usage Examples:
import { LitElement, html, ref, createRef } from "lit-element";
class RefElement extends LitElement {
private inputRef = createRef<HTMLInputElement>();
private canvasRef = createRef<HTMLCanvasElement>();
render() {
return html`
<div>
<input ${ref(this.inputRef)} placeholder="Focus me" />
<canvas ${ref(this.canvasRef)} width="200" height="100"></canvas>
<button @click=${this._focusInput}>Focus Input</button>
<button @click=${this._drawOnCanvas}>Draw</button>
</div>
`;
}
private _focusInput() {
this.inputRef.value?.focus();
}
private _drawOnCanvas() {
const canvas = this.canvasRef.value;
if (canvas) {
const ctx = canvas.getContext('2d');
if (ctx) {
ctx.fillStyle = 'blue';
ctx.fillRect(10, 10, 100, 50);
ctx.fillStyle = 'white';
ctx.font = '16px Arial';
ctx.fillText('Hello!', 20, 35);
}
}
}
}Renders placeholder content until a promise resolves.
/**
* Renders default content until a promise resolves, then renders the resolved value
* @param promise Promise to wait for
* @param defaultContent Content to show while waiting
* @returns Until directive result
*/
function until(promise: Promise<unknown>, ...defaultContent: unknown[]): DirectiveResult<typeof UntilDirective>;Usage Examples:
import { LitElement, html, until } from "lit-element";
class AsyncElement extends LitElement {
private async _loadUserData(userId: number) {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 2000));
return { id: userId, name: `User ${userId}`, email: `user${userId}@example.com` };
}
private async _loadPosts() {
await new Promise(resolve => setTimeout(resolve, 1500));
return ['Post 1', 'Post 2', 'Post 3'];
}
render() {
const userData = this._loadUserData(123);
const posts = this._loadPosts();
return html`
<div>
<h2>User Profile</h2>
<!-- Show loading message until user data loads -->
${until(
userData.then(user => html`
<div class="user-info">
<h3>${user.name}</h3>
<p>Email: ${user.email}</p>
<p>ID: ${user.id}</p>
</div>
`),
html`<p>Loading user data...</p>`
)}
<h3>Recent Posts</h3>
<!-- Show spinner until posts load -->
${until(
posts.then(postList => html`
<ul>
${postList.map(post => html`<li>${post}</li>`)}
</ul>
`),
html`<div class="spinner">Loading posts...</div>`
)}
</div>
`;
}
}Renders untrusted HTML/SVG content (use with extreme caution).
/**
* Renders HTML string as actual HTML (bypasses sanitization)
* WARNING: Only use with trusted content to prevent XSS attacks
* @param html HTML string to render
* @returns Unsafe HTML directive result
*/
function unsafeHTML(html: string): DirectiveResult<typeof UnsafeHTMLDirective>;
/**
* Renders SVG string as actual SVG (bypasses sanitization)
* WARNING: Only use with trusted content to prevent XSS attacks
* @param svg SVG string to render
* @returns Unsafe SVG directive result
*/
function unsafeSVG(svg: string): DirectiveResult<typeof UnsafeSVGDirective>;Usage Examples:
import { LitElement, html, unsafeHTML, unsafeSVG } from "lit-element";
class UnsafeContentElement extends LitElement {
// ONLY use with trusted content!
trustedHtml = '<strong>Bold</strong> and <em>italic</em> text';
trustedSvg = '<circle cx="50" cy="50" r="25" fill="red" />';
render() {
return html`
<div>
<h3>Trusted HTML Content:</h3>
<div>${unsafeHTML(this.trustedHtml)}</div>
<h3>Trusted SVG Content:</h3>
<svg width="100" height="100">
${unsafeSVG(this.trustedSvg)}
</svg>
</div>
`;
}
}Renders MathML string as actual MathML (bypasses sanitization).
/**
* Renders MathML string as actual MathML (bypasses sanitization)
* WARNING: Only use with trusted content to prevent XSS attacks
* @param mathml MathML string to render
* @returns Unsafe MathML directive result
*/
function unsafeMathML(mathml: string): DirectiveResult<typeof UnsafeMathMLDirective>;Appends values from an async iterable to the DOM.
/**
* Appends values from an async iterable, preserving previous values
* @param asyncIterable Async iterable of values to append
* @returns Async append directive result
*/
function asyncAppend(asyncIterable: AsyncIterable<unknown>): DirectiveResult<typeof AsyncAppendDirective>;Usage Examples:
import { LitElement, html, asyncAppend } from "lit-element";
class StreamingElement extends LitElement {
async *generateData() {
for (let i = 0; i < 10; i++) {
await new Promise(resolve => setTimeout(resolve, 1000));
yield html`<div>Item ${i + 1} - ${new Date().toLocaleTimeString()}</div>`;
}
}
render() {
return html`
<div>
<h3>Streaming Data (Append):</h3>
<div class="stream">
${asyncAppend(this.generateData())}
</div>
</div>
`;
}
}Replaces content with values from an async iterable.
/**
* Replaces content with values from an async iterable
* @param asyncIterable Async iterable of values to render
* @returns Async replace directive result
*/
function asyncReplace(asyncIterable: AsyncIterable<unknown>): DirectiveResult<typeof AsyncReplaceDirective>;Usage Examples:
import { LitElement, html, asyncReplace } from "lit-element";
class CountdownElement extends LitElement {
async *countdown(from: number) {
for (let i = from; i >= 0; i--) {
yield html`<div class="countdown">${i}</div>`;
if (i > 0) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
yield html`<div class="done">🎉 Done!</div>`;
}
render() {
return html`
<div>
<h3>Countdown Timer:</h3>
${asyncReplace(this.countdown(5))}
</div>
`;
}
}Maps over an iterable with a template function.
/**
* Maps over an iterable, applying a template function to each item
* @param items Iterable to map over
* @param template Function to apply to each item
* @returns Map directive result
*/
function map<T>(
items: Iterable<T>,
template: (item: T, index: number) => unknown
): DirectiveResult<typeof MapDirective>;Usage Examples:
import { LitElement, html, map } from "lit-element";
class MappedList extends LitElement {
items = ['apple', 'banana', 'cherry'];
users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
];
render() {
return html`
<div>
<h3>Simple List:</h3>
<ul>
${map(this.items, (item, index) => html`
<li>${index + 1}. ${item}</li>
`)}
</ul>
<h3>User Cards:</h3>
<div class="users">
${map(this.users, (user) => html`
<div class="user-card">
<h4>${user.name}</h4>
<p>ID: ${user.id}</p>
</div>
`)}
</div>
</div>
`;
}
}Joins rendered items with a separator.
/**
* Joins rendered items with a separator
* @param items Iterable of items to join
* @param joiner Separator to place between items
* @returns Join directive result
*/
function join<T>(
items: Iterable<T>,
joiner: unknown
): DirectiveResult<typeof JoinDirective>;Usage Examples:
import { LitElement, html, join } from "lit-element";
class JoinedList extends LitElement {
tags = ['javascript', 'typescript', 'lit-element'];
breadcrumbs = ['Home', 'Products', 'Electronics', 'Phones'];
render() {
return html`
<div>
<h3>Tags:</h3>
<p>
${join(
this.tags.map(tag => html`<span class="tag">${tag}</span>`),
html`<span class="separator"> • </span>`
)}
</p>
<h3>Breadcrumbs:</h3>
<nav>
${join(
this.breadcrumbs.map(crumb => html`<a href="#">${crumb}</a>`),
html`<span> / </span>`
)}
</nav>
<h3>Simple Join:</h3>
<p>${join(this.breadcrumbs, ' > ')}</p>
</div>
`;
}
}Generates a range of numbers for iteration.
/**
* Generates a range of numbers
* @param startOrEnd Start value (if end provided) or end value (if only parameter)
* @param end End value (exclusive)
* @param step Step size (default: 1)
* @returns Range directive result
*/
function range(startOrEnd: number, end?: number, step?: number): Iterable<number>;Usage Examples:
import { LitElement, html, range, map } from "lit-element";
class RangeElement extends LitElement {
render() {
return html`
<div>
<h3>Simple Range (0 to 5):</h3>
<ul>
${map(range(5), (n) => html`<li>Item ${n}</li>`)}
</ul>
<h3>Custom Range (10 to 20, step 2):</h3>
<ul>
${map(range(10, 20, 2), (n) => html`<li>Number ${n}</li>`)}
</ul>
<h3>Grid (3x3):</h3>
<div class="grid">
${map(range(3), (row) => html`
<div class="row">
${map(range(3), (col) => html`
<div class="cell">${row},${col}</div>
`)}
</div>
`)}
</div>
</div>
`;
}
}Forces re-rendering when a key changes.
/**
* Associates a key with content, forcing re-render when key changes
* @param key Key value to associate with content
* @param value Content to render
* @returns Keyed directive result
*/
function keyed(key: unknown, value: unknown): DirectiveResult<typeof KeyedDirective>;Usage Examples:
import { LitElement, html, keyed } from "lit-element";
class KeyedElement extends LitElement {
currentUser = { id: 1, name: 'Alice' };
refreshKey = 0;
private _switchUser() {
this.currentUser = this.currentUser.id === 1
? { id: 2, name: 'Bob' }
: { id: 1, name: 'Alice' };
}
private _refresh() {
this.refreshKey++;
}
render() {
return html`
<div>
<button @click=${this._switchUser}>Switch User</button>
<button @click=${this._refresh}>Force Refresh</button>
<!-- Force re-render when user changes -->
${keyed(this.currentUser.id, html`
<div class="user-profile">
<h3>${this.currentUser.name}</h3>
<input placeholder="User-specific input" />
<p>Profile loaded at: ${new Date().toLocaleTimeString()}</p>
</div>
`)}
<!-- Force re-render when refresh key changes -->
${keyed(this.refreshKey, html`
<div class="refresh-content">
<p>Refreshed content: ${Math.random()}</p>
</div>
`)}
</div>
`;
}
}Renders content from an HTMLTemplateElement.
/**
* Renders the content of an HTMLTemplateElement
* @param templateElement HTMLTemplateElement to render
* @returns Template content directive result
*/
function templateContent(templateElement: HTMLTemplateElement): DirectiveResult<typeof TemplateContentDirective>;Usage Examples:
import { LitElement, html, templateContent } from "lit-element";
class TemplateContentElement extends LitElement {
private _getTemplate() {
const template = document.createElement('template');
template.innerHTML = `
<div style="border: 1px solid #ccc; padding: 16px;">
<h4>Template Content</h4>
<p>This content came from an HTMLTemplateElement</p>
<button>Template Button</button>
</div>
`;
return template;
}
render() {
return html`
<div>
<h3>Rendered Template:</h3>
${templateContent(this._getTemplate())}
</div>
`;
}
}