🎯 Recommended Samples
Balanced sample collections from various categories for you to explore
Lit Samples
Lit web components examples including reactive properties, lifecycle hooks, templates, and modern component patterns
💻 Lit Hello World typescript
🟢 simple
Basic Lit component examples and Hello World applications with decorators and reactive properties
// Lit Hello World Examples
import { LitElement, html, css } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
// 1. Basic functional component
@customElement('hello-world')
export class HelloWorld extends LitElement {
static styles = css`
:host {
display: block;
padding: 16px;
border: 1px solid #ccc;
border-radius: 8px;
margin: 16px 0;
font-family: Arial, sans-serif;
}
h1 {
color: #2c3e50;
margin: 0 0 8px 0;
}
`;
render() {
return html`<h1>Hello, World!</h1>`;
}
}
// 2. Component with properties
@customElement('greeting-component')
export class GreetingComponent extends LitElement {
static styles = css`
:host {
display: block;
padding: 16px;
background: #f8f9fa;
border-radius: 8px;
margin: 8px 0;
}
h1 {
color: #3498db;
margin: 0;
}
`;
@property({ type: String })
name: string = 'World';
@property({ type: String })
greeting: string = 'Hello';
render() {
return html`
<h1>${this.greeting}, ${this.name}!</h1>
`;
}
}
// 3. Component with reactive state
@customElement('counter-component')
export class CounterComponent extends LitElement {
static styles = css`
:host {
display: block;
padding: 20px;
border: 2px solid #3498db;
border-radius: 8px;
text-align: center;
max-width: 300px;
margin: 16px auto;
}
.count {
font-size: 2em;
font-weight: bold;
color: #2c3e50;
margin: 16px 0;
}
button {
background: #3498db;
color: white;
border: none;
padding: 8px 16px;
margin: 0 4px;
border-radius: 4px;
cursor: pointer;
font-size: 1em;
}
button:hover {
background: #2980b9;
}
button:active {
transform: scale(0.98);
}
`;
@state()
private count: number = 0;
private increment() {
this.count++;
}
private decrement() {
this.count--;
}
private reset() {
this.count = 0;
}
render() {
return html`
<h2>Counter Component</h2>
<div class="count">${this.count}</div>
<div>
<button @click=${this.increment}>+</button>
<button @click=${this.reset}>Reset</button>
<button @click=${this.decrement}>-</button>
</div>
`;
}
}
// 4. Component with complex state
interface Todo {
id: number;
text: string;
completed: boolean;
}
@customElement('todo-app')
export class TodoApp extends LitElement {
static styles = css`
:host {
display: block;
padding: 20px;
max-width: 500px;
margin: 16px auto;
font-family: Arial, sans-serif;
}
.todo-input {
width: 100%;
padding: 8px;
font-size: 16px;
border: 2px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
margin-bottom: 8px;
}
.todo-input:focus {
outline: none;
border-color: #3498db;
}
.todo-list {
list-style: none;
padding: 0;
margin: 0;
}
.todo-item {
display: flex;
align-items: center;
padding: 8px;
border: 1px solid #ddd;
margin-bottom: 4px;
border-radius: 4px;
background: white;
}
.todo-item.completed {
background: #f8f9fa;
opacity: 0.7;
}
.todo-item.completed .todo-text {
text-decoration: line-through;
}
.todo-checkbox {
margin-right: 8px;
}
.todo-text {
flex: 1;
font-size: 16px;
}
.delete-btn {
background: #e74c3c;
color: white;
border: none;
padding: 4px 8px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
.delete-btn:hover {
background: #c0392b;
}
.stats {
margin-top: 16px;
padding: 8px;
background: #f8f9fa;
border-radius: 4px;
font-size: 14px;
color: #7f8c8d;
}
`;
@state()
private todos: Todo[] = [
{ id: 1, text: 'Learn Lit', completed: false },
{ id: 2, text: 'Build web components', completed: false }
];
@state()
private newTodoText: string = '';
private handleInput(event: Event) {
const target = event.target as HTMLInputElement;
this.newTodoText = target.value;
}
private handleKeyPress(event: KeyboardEvent) {
if (event.key === 'Enter' && this.newTodoText.trim()) {
this.addTodo();
}
}
private addTodo() {
if (this.newTodoText.trim()) {
const newTodo: Todo = {
id: Date.now(),
text: this.newTodoText.trim(),
completed: false
};
this.todos = [...this.todos, newTodo];
this.newTodoText = '';
}
}
private toggleTodo(id: number) {
this.todos = this.todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
);
}
private deleteTodo(id: number) {
this.todos = this.todos.filter(todo => todo.id !== id);
}
private get completedCount() {
return this.todos.filter(todo => todo.completed).length;
}
private get activeCount() {
return this.todos.filter(todo => !todo.completed).length;
}
render() {
return html`
<h2>Todo App</h2>
<input
class="todo-input"
type="text"
placeholder="What needs to be done?"
.value=${this.newTodoText}
@input=${this.handleInput}
@keypress=${this.handleKeyPress}
/>
<ul class="todo-list">
${this.todos.map(todo => html`
<li class="todo-item ${todo.completed ? 'completed' : ''}">
<input
class="todo-checkbox"
type="checkbox"
.checked=${todo.completed}
@change=${() => this.toggleTodo(todo.id)}
/>
<span class="todo-text">${todo.text}</span>
<button
class="delete-btn"
@click=${() => this.deleteTodo(todo.id)}
>
Delete
</button>
</li>
`)}
</ul>
<div class="stats">
${this.activeCount} active, ${this.completedCount} completed
</div>
`;
}
}
// 5. Component with computed properties
@customElement('price-calculator')
export class PriceCalculator extends LitElement {
static styles = css`
:host {
display: block;
padding: 20px;
border: 2px solid #27ae60;
border-radius: 8px;
max-width: 400px;
margin: 16px auto;
}
h2 {
color: #27ae60;
text-align: center;
margin-top: 0;
}
.input-group {
margin-bottom: 16px;
}
label {
display: block;
margin-bottom: 4px;
font-weight: bold;
color: #2c3e50;
}
input {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
font-size: 16px;
}
.summary {
background: #f8f9fa;
padding: 16px;
border-radius: 4px;
border-left: 4px solid #27ae60;
}
.summary-row {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
}
.summary-row.total {
font-weight: bold;
font-size: 1.2em;
border-top: 1px solid #ddd;
padding-top: 8px;
}
.total {
color: #27ae60;
}
`;
@property({ type: Number })
price: number = 100;
@property({ type: Number })
quantity: number = 1;
@property({ type: Number })
taxRate: number = 0.08;
private get subtotal() {
return this.price * this.quantity;
}
private get tax() {
return this.subtotal * this.taxRate;
}
private get total() {
return this.subtotal + this.tax;
}
private handlePriceChange(event: Event) {
const target = event.target as HTMLInputElement;
this.price = parseFloat(target.value) || 0;
}
private handleQuantityChange(event: Event) {
const target = event.target as HTMLInputElement;
this.quantity = parseInt(target.value) || 0;
}
private handleTaxRateChange(event: Event) {
const target = event.target as HTMLInputElement;
this.taxRate = parseFloat(target.value) || 0;
}
render() {
return html`
<h2>Price Calculator</h2>
<div class="input-group">
<label for="price">Price ($)</label>
<input
id="price"
type="number"
.value=${this.price}
@input=${this.handlePriceChange}
step="0.01"
min="0"
/>
</div>
<div class="input-group">
<label for="quantity">Quantity</label>
<input
id="quantity"
type="number"
.value=${this.quantity}
@input=${this.handleQuantityChange}
min="0"
/>
</div>
<div class="input-group">
<label for="taxRate">Tax Rate (${(this.taxRate * 100).toFixed(1)}%)</label>
<input
id="taxRate"
type="number"
.value=${this.taxRate}
@input=${this.handleTaxRateChange}
step="0.01"
min="0"
max="1"
/>
</div>
<div class="summary">
<div class="summary-row">
<span>Subtotal:</span>
<span>$${this.subtotal.toFixed(2)}</span>
</div>
<div class="summary-row">
<span>Tax:</span>
<span>$${this.tax.toFixed(2)}</span>
</div>
<div class="summary-row total">
<span>Total:</span>
<span class="total">$${this.total.toFixed(2)}</span>
</div>
</div>
`;
}
}
// 6. Demo app showing all components
@customElement('lit-demo-app')
export class LitDemoApp extends LitElement {
static styles = css`
:host {
display: block;
padding: 20px;
max-width: 1200px;
margin: 0 auto;
font-family: Arial, sans-serif;
}
.demo-section {
margin: 32px 0;
padding: 20px;
border: 1px solid #e0e0e0;
border-radius: 8px;
background: #fafafa;
}
.demo-title {
color: #2c3e50;
border-bottom: 2px solid #3498db;
padding-bottom: 8px;
margin-bottom: 16px;
}
.demo-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
}
`;
render() {
return html`
<h1>Lit Components Demo</h1>
<p>Explore various Lit web component patterns and features:</p>
<div class="demo-section">
<h2 class="demo-title">Basic Components</h2>
<div class="demo-grid">
<hello-world></hello-world>
<greeting-component name="Lit" greeting="Welcome to"></greeting-component>
</div>
</div>
<div class="demo-section">
<h2 class="demo-title">Interactive Components</h2>
<div class="demo-grid">
<counter-component></counter-component>
<price-calculator></price-calculator>
</div>
</div>
<div class="demo-section">
<h2 class="demo-title">Complex Application</h2>
<todo-app></todo-app>
</div>
`;
}
}
export { HelloWorld, GreetingComponent, CounterComponent, TodoApp, PriceCalculator, LitDemoApp };
💻 Lit Properties and Reactivity typescript
🟡 intermediate
Working with reactive properties, state management, and data flow in Lit components
// Lit Properties and Reactivity Examples
import { LitElement, html, css } from 'lit';
import { customElement, property, state, query, queryAll } from 'lit/decorators.js';
// 1. Basic properties with type conversion
@customElement('property-basics')
export class PropertyBasics extends LitElement {
static styles = css`
:host {
display: block;
padding: 20px;
border: 2px solid #3498db;
border-radius: 8px;
margin: 16px 0;
}
.property-display {
background: #f8f9fa;
padding: 12px;
border-radius: 4px;
margin: 8px 0;
font-family: monospace;
}
input, select {
margin: 8px;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
`;
// Different property types
@property({ type: String })
name: string = 'World';
@property({ type: Number })
count: number = 0;
@property({ type: Boolean })
isActive: boolean = true;
@property({ type: Array })
items: string[] = ['apple', 'banana', 'orange'];
@property({ type: Object })
config: { theme: string; version: string } = { theme: 'light', version: '1.0.0' };
private handleNameChange(event: Event) {
const target = event.target as HTMLInputElement;
this.name = target.value;
}
private handleCountChange(event: Event) {
const target = event.target as HTMLInputElement;
this.count = parseInt(target.value) || 0;
}
private toggleActive() {
this.isActive = !this.isActive;
}
private addItem() {
const newItem = `item-${Date.now()}`;
this.items = [...this.items, newItem];
}
private removeItem(index: number) {
this.items = this.items.filter((_, i) => i !== index);
}
private updateTheme() {
this.config = { ...this.config, theme: this.config.theme === 'light' ? 'dark' : 'light' };
}
render() {
return html`
<h3>Property Types Demo</h3>
<div>
<label>Name: <input .value=${this.name} @input=${this.handleNameChange}></label>
<div class="property-display">name: ${this.name}</div>
</div>
<div>
<label>Count: <input type="number" .value=${this.count} @input=${this.handleCountChange}></label>
<div class="property-display">count: ${this.count}</div>
</div>
<div>
<button @click=${this.toggleActive}>
Active: ${this.isActive ? 'Yes' : 'No'}
</button>
<div class="property-display">isActive: ${this.isActive}</div>
</div>
<div>
<button @click=${this.addItem}>Add Item</button>
<div class="property-display">items: ${JSON.stringify(this.items)}</div>
${this.items.map((item, index) => html`
<div>
<span>${item}</span>
<button @click=${() => this.removeItem(index)}>Remove</button>
</div>
`)}
</div>
<div>
<button @click=${this.updateTheme}>Toggle Theme</button>
<div class="property-display">config: ${JSON.stringify(this.config)}</div>
</div>
`;
}
}
// 2. Computed properties and derived state
@customElement('computed-properties')
export class ComputedProperties extends LitElement {
static styles = css`
:host {
display: block;
padding: 20px;
border: 2px solid #9b59b6;
border-radius: 8px;
margin: 16px 0;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 16px;
margin: 16px 0;
}
.stat-card {
background: #f8f9fa;
padding: 16px;
border-radius: 8px;
text-align: center;
border-left: 4px solid #9b59b6;
}
.stat-value {
font-size: 2em;
font-weight: bold;
color: #9b59b6;
}
.stat-label {
font-size: 0.9em;
color: #7f8c8d;
margin-top: 4px;
}
.controls {
margin: 20px 0;
}
input {
margin: 8px;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
`;
@property({ type: Number })
baseValue: number = 10;
@property({ type: Number })
multiplier: number = 2;
@property({ type: Number })
offset: number = 5;
@property({ type: Array })
data: number[] = [1, 2, 3, 4, 5];
private get doubled() {
return this.baseValue * this.multiplier;
}
private get adjusted() {
return this.doubled + this.offset;
}
private get sum() {
return this.data.reduce((sum, val) => sum + val, 0);
}
private get average() {
return this.data.length > 0 ? this.sum / this.data.length : 0;
}
private get max() {
return Math.max(...this.data);
}
private get min() {
return Math.min(...this.data);
}
private handleBaseValueChange(event: Event) {
const target = event.target as HTMLInputElement;
this.baseValue = parseFloat(target.value) || 0;
}
private handleMultiplierChange(event: Event) {
const target = event.target as HTMLInputElement;
this.multiplier = parseFloat(target.value) || 1;
}
private handleOffsetChange(event: Event) {
const target = event.target as HTMLInputElement;
this.offset = parseFloat(target.value) || 0;
}
private addDataPoint() {
const newPoint = Math.floor(Math.random() * 20) + 1;
this.data = [...this.data, newPoint];
}
private clearData() {
this.data = [];
}
render() {
return html`
<h3>Computed Properties Demo</h3>
<div class="controls">
<label>Base: <input type="number" .value=${this.baseValue} @input=${this.handleBaseValueChange}></label>
<label>Multiplier: <input type="number" .value=${this.multiplier} @input=${this.handleMultiplierChange}></label>
<label>Offset: <input type="number" .value=${this.offset} @input=${this.handleOffsetChange}></label>
<button @click=${this.addDataPoint}>Add Data Point</button>
<button @click=${this.clearData}>Clear Data</button>
</div>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value">${this.baseValue}</div>
<div class="stat-label">Base Value</div>
</div>
<div class="stat-card">
<div class="stat-value">${this.doubled}</div>
<div class="stat-label">Doubled</div>
</div>
<div class="stat-card">
<div class="stat-value">${this.adjusted}</div>
<div class="stat-label">Adjusted</div>
</div>
<div class="stat-card">
<div class="stat-value">${this.sum}</div>
<div class="stat-label">Sum</div>
</div>
<div class="stat-card">
<div class="stat-value">${this.average.toFixed(1)}</div>
<div class="stat-label">Average</div>
</div>
<div class="stat-card">
<div class="stat-value">${this.max}</div>
<div class="stat-label">Max</div>
</div>
<div class="stat-card">
<div class="stat-value">${this.min}</div>
<div class="stat-label">Min</div>
</div>
<div class="stat-card">
<div class="stat-value">${this.data.length}</div>
<div class="stat-label">Count</div>
</div>
</div>
<div class="property-display">
data: [${this.data.join(', ')}]
</div>
`;
}
}
// 3. State management with controlled components
@customElement('form-controller')
export class FormController extends LitElement {
static styles = css`
:host {
display: block;
padding: 20px;
border: 2px solid #e67e22;
border-radius: 8px;
margin: 16px 0;
}
.form-group {
margin-bottom: 16px;
}
label {
display: block;
margin-bottom: 4px;
font-weight: bold;
color: #2c3e50;
}
input, select, textarea {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
font-size: 16px;
}
input:focus, select:focus, textarea:focus {
outline: none;
border-color: #e67e22;
}
.error {
color: #e74c3c;
font-size: 0.9em;
margin-top: 4px;
}
button {
background: #e67e22;
color: white;
border: none;
padding: 12px 24px;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
margin-right: 8px;
}
button:hover {
background: #d35400;
}
button:disabled {
background: #bdc3c7;
cursor: not-allowed;
}
.form-summary {
background: #f8f9fa;
padding: 16px;
border-radius: 4px;
border-left: 4px solid #e67e22;
margin-top: 20px;
}
.checkbox-group {
display: flex;
gap: 16px;
margin-top: 8px;
}
.checkbox-item {
display: flex;
align-items: center;
}
.checkbox-item input {
width: auto;
margin-right: 8px;
}
`;
@state()
private formData = {
name: '',
email: '',
age: '',
country: '',
interests: [] as string[],
message: ''
};
@state()
private errors: Record<string, string> = {};
@state()
private isSubmitting: boolean = false;
@state()
private isSubmitted: boolean = false;
private countries = ['USA', 'Canada', 'UK', 'Germany', 'France', 'Japan', 'Australia'];
private availableInterests = ['Technology', 'Sports', 'Music', 'Reading', 'Travel', 'Cooking'];
private validateForm(): boolean {
const newErrors: Record<string, string> = {};
if (!this.formData.name.trim()) {
newErrors.name = 'Name is required';
}
if (!this.formData.email.trim()) {
newErrors.email = 'Email is required';
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(this.formData.email)) {
newErrors.email = 'Please enter a valid email';
}
if (!this.formData.age) {
newErrors.age = 'Age is required';
} else if (parseInt(this.formData.age) < 18) {
newErrors.age = 'You must be at least 18 years old';
}
if (!this.formData.country) {
newErrors.country = 'Please select a country';
}
this.errors = newErrors;
return Object.keys(newErrors).length === 0;
}
private handleInputChange(field: string, event: Event) {
const target = event.target as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
this.formData = { ...this.formData, [field]: target.value };
// Clear error for this field
if (this.errors[field]) {
this.errors = { ...this.errors, [field]: '' };
}
}
private handleInterestChange(interest: string, event: Event) {
const target = event.target as HTMLInputElement;
const currentInterests = [...this.formData.interests];
if (target.checked) {
if (!currentInterests.includes(interest)) {
this.formData.interests = [...currentInterests, interest];
}
} else {
this.formData.interests = currentInterests.filter(i => i !== interest);
}
}
private async handleSubmit() {
if (!this.validateForm()) {
return;
}
this.isSubmitting = true;
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 2000));
this.isSubmitting = false;
this.isSubmitted = true;
// Reset form after 3 seconds
setTimeout(() => {
this.formData = {
name: '',
email: '',
age: '',
country: '',
interests: [],
message: ''
};
this.isSubmitted = false;
}, 3000);
}
private handleReset() {
this.formData = {
name: '',
email: '',
age: '',
country: '',
interests: [],
message: ''
};
this.errors = {};
this.isSubmitted = false;
}
render() {
return html`
<h3>Controlled Form Demo</h3>
<form @submit=${(e: Event) => { e.preventDefault(); this.handleSubmit(); }}>
<div class="form-group">
<label for="name">Name *</label>
<input
id="name"
type="text"
.value=${this.formData.name}
@input=${(e: Event) => this.handleInputChange('name', e)}
?disabled=${this.isSubmitting}
/>
<div class="error">${this.errors.name || ''}</div>
</div>
<div class="form-group">
<label for="email">Email *</label>
<input
id="email"
type="email"
.value=${this.formData.email}
@input=${(e: Event) => this.handleInputChange('email', e)}
?disabled=${this.isSubmitting}
/>
<div class="error">${this.errors.email || ''}</div>
</div>
<div class="form-group">
<label for="age">Age *</label>
<input
id="age"
type="number"
.value=${this.formData.age}
@input=${(e: Event) => this.handleInputChange('age', e)}
?disabled=${this.isSubmitting}
/>
<div class="error">${this.errors.age || ''}</div>
</div>
<div class="form-group">
<label for="country">Country *</label>
<select
id="country"
.value=${this.formData.country}
@change=${(e: Event) => this.handleInputChange('country', e)}
?disabled=${this.isSubmitting}
>
<option value="">Select a country</option>
${this.countries.map(country => html`
<option value=${country}>${country}</option>
`)}
</select>
<div class="error">${this.errors.country || ''}</div>
</div>
<div class="form-group">
<label>Interests</label>
<div class="checkbox-group">
${this.availableInterests.map(interest => html`
<div class="checkbox-item">
<input
type="checkbox"
id=${interest}
.checked=${this.formData.interests.includes(interest)}
@change=${(e: Event) => this.handleInterestChange(interest, e)}
?disabled=${this.isSubmitting}
/>
<label for=${interest}>${interest}</label>
</div>
`)}
</div>
</div>
<div class="form-group">
<label for="message">Message</label>
<textarea
id="message"
rows="4"
.value=${this.formData.message}
@input=${(e: Event) => this.handleInputChange('message', e)}
?disabled=${this.isSubmitting}
></textarea>
</div>
<button type="submit" ?disabled=${this.isSubmitting}>
${this.isSubmitting ? 'Submitting...' : 'Submit'}
</button>
<button type="button" @click=${this.handleReset} ?disabled=${this.isSubmitting}>
Reset
</button>
</form>
${this.isSubmitted ? html`
<div class="form-summary">
<h4>✅ Form Submitted Successfully!</h4>
<pre>${JSON.stringify(this.formData, null, 2)}</pre>
</div>
` : ''}
<div class="form-summary">
<h4>Current Form State:</h4>
<pre>${JSON.stringify(this.formData, null, 2)}</pre>
</div>
`;
}
}
export { PropertyBasics, ComputedProperties, FormController };
💻 Lit Lifecycle and Events typescript
🟡 intermediate
Lifecycle hooks, event handling, custom events, and component communication in Lit
// Lit Lifecycle and Events Examples
import { LitElement, html, css } from 'lit';
import { customElement, property, state, query } from 'lit/decorators.js';
// 1. Lifecycle hooks demo
@customElement('lifecycle-demo')
export class LifecycleDemo extends LitElement {
static styles = css`
:host {
display: block;
padding: 20px;
border: 2px solid #e74c3c;
border-radius: 8px;
margin: 16px 0;
}
.log {
background: #2c3e50;
color: #ecf0f1;
padding: 12px;
border-radius: 4px;
font-family: monospace;
font-size: 12px;
max-height: 200px;
overflow-y: auto;
margin: 16px 0;
}
.log-entry {
margin: 2px 0;
padding: 2px 0;
}
button {
background: #e74c3c;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
margin: 4px;
}
button:hover {
background: #c0392b;
}
.status {
background: #f8f9fa;
padding: 8px;
border-radius: 4px;
margin: 8px 0;
border-left: 4px solid #e74c3c;
}
`;
@state()
private logs: string[] = [];
@state()
private updateCount: number = 0;
@state()
private renderCount: number = 0;
private addLog(message: string) {
const timestamp = new Date().toLocaleTimeString();
this.logs = [...this.logs, `[${timestamp}] ${message}`].slice(-10); // Keep last 10 logs
}
// Lifecycle: Called when the element is added to the DOM
connectedCallback() {
super.connectedCallback();
this.addLog('connectedCallback: Element added to DOM');
}
// Lifecycle: Called when the element is removed from the DOM
disconnectedCallback() {
super.disconnectedCallback();
this.addLog('disconnectedCallback: Element removed from DOM');
}
// Lifecycle: Called before the first update
protected willUpdate(changedProperties: Map<string, any>) {
this.addLog(`willUpdate: Properties changed - ${Array.from(changedProperties.keys()).join(', ')}`);
this.updateCount++;
}
// Lifecycle: Called after the first update
protected firstUpdated() {
this.addLog('firstUpdated: First render completed');
}
// Lifecycle: Called after every update
protected updated(changedProperties: Map<string, any>) {
this.addLog(`updated: Properties updated - ${Array.from(changedProperties.keys()).join(', ')}`);
this.renderCount++;
}
// Lifecycle: Called when the element is about to be updated
protected shouldUpdate(changedProperties: Map<string, any>): boolean {
const shouldUpdate = changedProperties.size > 0;
this.addLog(`shouldUpdate: ${shouldUpdate ? 'Will update' : 'Skipping update'}`);
return shouldUpdate;
}
private triggerUpdate() {
this.requestUpdate();
}
private clearLogs() {
this.logs = [];
}
render() {
return html`
<h3>Lifecycle Hooks Demo</h3>
<div class="status">
Updates: ${this.updateCount} | Renders: ${this.renderCount}
</div>
<div>
<button @click=${this.triggerUpdate}>Force Update</button>
<button @click=${this.clearLogs}>Clear Logs</button>
</div>
<div class="log">
${this.logs.map(log => html`<div class="log-entry">${log}</div>`)}
</div>
<p>Open browser console to see additional lifecycle logs when this component is added/removed from the DOM.</p>
`;
}
}
// 2. Event handling demo
@customElement('event-demo')
export class EventDemo extends LitElement {
static styles = css`
:host {
display: block;
padding: 20px;
border: 2px solid #f39c12;
border-radius: 8px;
margin: 16px 0;
}
.button-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin: 16px 0;
}
button {
padding: 12px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
}
.primary {
background: #3498db;
color: white;
}
.success {
background: #27ae60;
color: white;
}
.warning {
background: #f39c12;
color: white;
}
.danger {
background: #e74c3c;
color: white;
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
}
button:active {
transform: translateY(0);
}
.input-group {
margin: 16px 0;
}
label {
display: block;
margin-bottom: 4px;
font-weight: bold;
}
input, select {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}
.event-log {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 4px;
padding: 12px;
margin: 16px 0;
max-height: 150px;
overflow-y: auto;
}
.event-entry {
padding: 4px 0;
border-bottom: 1px solid #e9ecef;
font-size: 14px;
}
.interactive-area {
background: #ecf0f1;
padding: 20px;
border-radius: 4px;
margin: 16px 0;
text-align: center;
cursor: pointer;
user-select: none;
}
.coordinates {
background: #34495e;
color: white;
padding: 8px;
border-radius: 4px;
font-family: monospace;
margin: 8px 0;
}
`;
@state()
private eventLog: string[] = [];
@state()
private clickCount: number = 0;
@state()
private mousePosition = { x: 0, y: 0 };
@state()
private inputValue: string = '';
@state()
private selectValue: string = '';
private addLog(event: string, detail?: any) {
const timestamp = new Date().toLocaleTimeString();
const message = detail ? `${event}: ${JSON.stringify(detail)}` : event;
this.eventLog = [...this.eventLog, `[${timestamp}] ${message}`].slice(-15);
}
// Click event handlers
private handleRegularClick(event: Event) {
this.clickCount++;
this.addLog('Regular click', { count: this.clickCount });
}
private handleClickWithModifier(event: MouseEvent) {
const modifiers = [];
if (event.ctrlKey) modifiers.push('Ctrl');
if (event.shiftKey) modifiers.push('Shift');
if (event.altKey) modifiers.push('Alt');
if (event.metaKey) modifiers.push('Meta');
this.addLog('Modified click', {
button: event.button,
modifiers: modifiers.length ? modifiers : 'none'
});
}
// Keyboard event handlers
private handleKeyDown(event: KeyboardEvent) {
this.addLog('Key down', {
key: event.key,
code: event.code,
ctrlKey: event.ctrlKey,
shiftKey: event.shiftKey
});
}
private handleKeyPress(event: KeyboardEvent) {
this.addLog('Key press', { key: event.key });
}
// Mouse event handlers
private handleMouseMove(event: MouseEvent) {
this.mousePosition = { x: event.offsetX, y: event.offsetY };
}
private handleMouseEnter() {
this.addLog('Mouse entered interactive area');
}
private handleMouseLeave() {
this.addLog('Mouse left interactive area');
}
// Form event handlers
private handleInput(event: Event) {
const target = event.target as HTMLInputElement;
this.inputValue = target.value;
this.addLog('Input changed', { value: this.inputValue });
}
private handleSelectChange(event: Event) {
const target = event.target as HTMLSelectElement;
this.selectValue = target.value;
this.addLog('Select changed', { value: this.selectValue });
}
// Event delegation
private handleButtonGroupClick(event: MouseEvent) {
const target = event.target as HTMLElement;
if (target.tagName === 'BUTTON') {
this.addLog('Delegated click', { button: target.textContent });
}
}
private clearLog() {
this.eventLog = [];
}
render() {
return html`
<h3>Event Handling Demo</h3>
<div class="button-grid" @click=${this.handleButtonGroupClick}>
<button class="primary" @click=${this.handleRegularClick}>
Click Count: ${this.clickCount}
</button>
<button class="success" @click=${this.handleClickWithModifier}>
Click with Modifiers
</button>
<button class="warning" @dblclick=${() => this.addLog('Double clicked')}>
Double Click Me
</button>
<button class="danger" @contextmenu=${(e: Event) => { e.preventDefault(); this.addLog('Right clicked'); }}>
Right Click Me
</button>
</div>
<div class="input-group">
<label for="input-field">Type here:</label>
<input
id="input-field"
.value=${this.inputValue}
@input=${this.handleInput}
@keydown=${this.handleKeyDown}
@keypress=${this.handleKeyPress}
placeholder="Type to see keyboard events"
/>
</div>
<div class="input-group">
<label for="select-field">Select an option:</label>
<select
id="select-field"
.value=${this.selectValue}
@change=${this.handleSelectChange}
>
<option value="">Choose...</option>
<option value="option1">Option 1</option>
<option value="option2">Option 2</option>
<option value="option3">Option 3</option>
</select>
</div>
<div
class="interactive-area"
@mousemove=${this.handleMouseMove}
@mouseenter=${this.handleMouseEnter}
@mouseleave=${this.handleMouseLeave}
>
<h4>Interactive Area</h4>
<p>Move your mouse over this area</p>
<div class="coordinates">
Position: (${this.mousePosition.x}, ${this.mousePosition.y})
</div>
</div>
<div style="margin: 16px 0;">
<button @click=${this.clearLog}>Clear Event Log</button>
</div>
<div class="event-log">
<strong>Event Log:</strong>
${this.eventLog.map(log => html`<div class="event-entry">${log}</div>`)}
</div>
`;
}
}
// 3. Custom events demo
@customElement('custom-event-demo')
export class CustomEventDemo extends LitElement {
static styles = css`
:host {
display: block;
padding: 20px;
border: 2px solid #16a085;
border-radius: 8px;
margin: 16px 0;
}
.event-sender {
background: #ecf0f1;
padding: 16px;
border-radius: 4px;
margin: 16px 0;
}
.event-receiver {
background: #e8f8f5;
border: 2px solid #16a085;
padding: 16px;
border-radius: 4px;
margin: 16px 0;
min-height: 100px;
}
button {
background: #16a085;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
margin: 4px;
}
button:hover {
background: #138d75;
}
input {
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
margin: 4px;
}
.message-display {
background: #2c3e50;
color: white;
padding: 12px;
border-radius: 4px;
margin: 8px 0;
font-family: monospace;
}
.event-counter {
background: #f39c12;
color: white;
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
margin-left: 8px;
}
`;
@state()
private messages: string[] = [];
@state()
private eventCounter: number = 0;
@state()
private lastEventData: any = null;
// Custom event handlers
private handleCustomMessage(event: CustomEvent) {
this.eventCounter++;
this.lastEventData = event.detail;
const message = `Message: ${event.detail.message} from ${event.detail.source}`;
this.messages = [...this.messages, message].slice(-5);
this.requestUpdate();
}
private handleUserAction(event: CustomEvent) {
this.eventCounter++;
const action = event.detail.action;
const data = event.detail.data;
const message = `User action: ${action} with data: ${JSON.stringify(data)}`;
this.messages = [...this.messages, message].slice(-5);
this.requestUpdate();
}
private handleDataUpdate(event: CustomEvent) {
this.eventCounter++;
const { field, oldValue, newValue } = event.detail;
const message = `Data update: ${field} changed from ${oldValue} to ${newValue}`;
this.messages = [...this.messages, message].slice(-5);
this.requestUpdate();
}
private clearMessages() {
this.messages = [];
this.eventCounter = 0;
this.lastEventData = null;
}
render() {
return html`
<h3>Custom Events Demo</h3>
<div class="event-sender">
<h4>Event Senders</h4>
<event-sender-component
@custom-message=${this.handleCustomMessage}
@user-action=${this.handleUserAction}
@data-update=${this.handleDataUpdate}
></event-sender-component>
</div>
<div class="event-receiver">
<h4>
Event Receiver
<span class="event-counter">${this.eventCounter} events</span>
</h4>
${this.lastEventData ? html`
<div class="message-display">
Last Event: ${JSON.stringify(this.lastEventData, null, 2)}
</div>
` : ''}
<div>
<strong>Recent Messages:</strong>
${this.messages.map(msg => html`<div style="margin: 4px 0;">• ${msg}</div>`)}
${this.messages.length === 0 ? html`<div style="color: #7f8c8d;">No events received yet</div>` : ''}
</div>
<button @click=${this.clearMessages}>Clear Messages</button>
</div>
`;
}
}
// Component that sends custom events
@customElement('event-sender-component')
export class EventSenderComponent extends LitElement {
static styles = css`
:host {
display: block;
}
.controls {
display: flex;
flex-direction: column;
gap: 12px;
}
.control-group {
display: flex;
gap: 8px;
align-items: center;
}
button {
background: #3498db;
color: white;
border: none;
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
button:hover {
background: #2980b9;
}
input {
padding: 6px;
border: 1px solid #ddd;
border-radius: 4px;
}
select {
padding: 6px;
border: 1px solid #ddd;
border-radius: 4px;
}
`;
@state()
private messageText: string = 'Hello from sender!';
@state()
private userData: string = 'John Doe';
@state()
private userRole: string = 'user';
private sendMessage() {
this.dispatchEvent(new CustomEvent('custom-message', {
detail: {
message: this.messageText,
source: 'EventSenderComponent',
timestamp: Date.now()
},
bubbles: true,
composed: true
}));
}
private sendUserAction() {
const actions = ['login', 'logout', 'update-profile', 'delete-account'];
const randomAction = actions[Math.floor(Math.random() * actions.length)];
this.dispatchEvent(new CustomEvent('user-action', {
detail: {
action: randomAction,
data: {
user: this.userData,
role: this.userRole,
timestamp: Date.now()
}
},
bubbles: true,
composed: true
}));
}
private sendGenericEvent(type: string) {
this.dispatchEvent(new CustomEvent(type, {
detail: { type, value: Math.random() },
bubbles: true,
composed: true
}));
}
private handleInputChange(event: Event) {
const target = event.target as HTMLInputElement;
const fieldName = target.name;
const oldValue = fieldName === 'userData' ? this.userData : this.messageText;
const newValue = target.value;
// Send data update event
this.dispatchEvent(new CustomEvent('data-update', {
detail: {
field: fieldName,
oldValue,
newValue
},
bubbles: true,
composed: true
}));
// Update the state
if (fieldName === 'userData') {
this.userData = newValue;
} else {
this.messageText = newValue;
}
}
render() {
return html`
<div class="controls">
<div class="control-group">
<input
name="messageText"
.value=${this.messageText}
@input=${this.handleInputChange}
placeholder="Enter message"
/>
<button @click=${this.sendMessage}>Send Message</button>
</div>
<div class="control-group">
<input
name="userData"
.value=${this.userData}
@input=${this.handleInputChange}
placeholder="User name"
/>
<select .value=${this.userRole} @change=${(e: Event) => { this.userRole = (e.target as HTMLSelectElement).value; }}>
<option value="user">User</option>
<option value="admin">Admin</option>
<option value="guest">Guest</option>
</select>
</div>
<div class="control-group">
<button @click=${this.sendUserAction}>Send User Action</button>
<button @click=${() => this.sendGenericEvent('custom-event-1')}>Send Event 1</button>
<button @click=${() => this.sendGenericEvent('custom-event-2')}>Send Event 2</button>
</div>
</div>
`;
}
}
export { LifecycleDemo, EventDemo, CustomEventDemo, EventSenderComponent };
💻 Lit Advanced Patterns typescript
🔴 complex
Advanced Lit patterns including composition, theming, testing strategies, and performance optimization
// Lit Advanced Patterns and Best Practices
import { LitElement, html, css, unsafeCSS, CSSResultGroup } from 'lit';
import { customElement, property, state, query } from 'lit/decorators.js';
// 1. Component composition with slots
@customElement('card-component')
export class CardComponent extends LitElement {
static styles = css`
:host {
display: block;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
overflow: hidden;
margin: 16px 0;
max-width: 400px;
transition: transform 0.2s, box-shadow 0.2s;
}
:host(:hover) {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0,0,0,0.15);
}
::slotted(header) {
background: #3498db;
color: white;
padding: 16px;
margin: 0;
font-size: 1.2em;
font-weight: bold;
}
::slotted(main) {
padding: 16px;
display: block;
}
::slotted(footer) {
background: #f8f9fa;
padding: 12px 16px;
border-top: 1px solid #dee2e6;
display: block;
font-size: 0.9em;
color: #6c757d;
}
.card-actions {
padding: 16px;
border-top: 1px solid #dee2e6;
display: flex;
gap: 8px;
justify-content: flex-end;
}
.slot-placeholder {
padding: 16px;
color: #6c757d;
font-style: italic;
text-align: center;
}
`;
@property({ type: String })
variant: 'default' | 'elevated' | 'outlined' = 'default';
@property({ type: Boolean })
interactive: boolean = false;
@state()
private isExpanded: boolean = false;
render() {
const hasHeader = this.querySelector(':scope > header') !== null;
const hasMain = this.querySelector(':scope > main') !== null;
const hasFooter = this.querySelector(':scope > footer') !== null;
return html`
<div class="card" data-variant="${this.variant}">
${hasHeader ? html`<slot name="header"></slot>` : ''}
${hasMain ? html`<slot name="main"></slot>` : html`
<div class="slot-placeholder">Main content goes here</div>
`}
${this.interactive ? html`
<div class="card-actions">
<button @click=${() => this.isExpanded = !this.isExpanded}>
${this.isExpanded ? 'Collapse' : 'Expand'}
</button>
<button @click=${() => this.toggleComplete?.()}>
Complete
</button>
</div>
`: ''}
${hasFooter ? html`<slot name="footer"></slot>` : ''}
${this.interactive && this.isExpanded ? html`
<div class="slot-placeholder">Expanded content area</div>
`: ''}
</div>
`;
}
}
// 2. Themeable component with CSS variables
@customElement('themeable-button')
export class ThemeableButton extends LitElement {
static styles = css`
:host {
--button-bg: #3498db;
--button-color: white;
--button-border: none;
--button-padding: 12px 24px;
--button-font-size: 16px;
--button-border-radius: 4px;
--button-transition: all 0.2s ease;
--button-hover-bg: #2980b9;
--button-active-bg: #21618c;
--button-disabled-bg: #bdc3c7;
--button-disabled-color: #7f8c8d;
display: inline-block;
}
button {
background: var(--button-bg);
color: var(--button-color);
border: var(--button-border);
padding: var(--button-padding);
font-size: var(--button-font-size);
border-radius: var(--button-border-radius);
transition: var(--button-transition);
cursor: pointer;
font-family: inherit;
font-weight: 500;
outline: none;
}
button:hover:not(:disabled) {
background: var(--button-hover-bg);
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
}
button:active:not(:disabled) {
background: var(--button-active-bg);
transform: translateY(0);
}
button:disabled {
background: var(--button-disabled-bg);
color: var(--button-disabled-color);
cursor: not-allowed;
transform: none;
box-shadow: none;
}
/* Size variants */
:host([size="small"]) {
--button-padding: 8px 16px;
--button-font-size: 14px;
}
:host([size="large"]) {
--button-padding: 16px 32px;
--button-font-size: 18px;
}
/* Variant styles */
:host([variant="success"]) {
--button-bg: #27ae60;
--button-hover-bg: #229954;
--button-active-bg: #1e8449;
}
:host([variant="warning"]) {
--button-bg: #f39c12;
--button-hover-bg: #e67e22;
--button-active-bg: #d35400;
}
:host([variant="danger"]) {
--button-bg: #e74c3c;
--button-hover-bg: #c0392b;
--button-active-bg: #a93226;
}
:host([variant="outline"]) {
--button-bg: transparent;
--button-border: 2px solid var(--button-bg, #3498db);
--button-color: var(--button-bg, #3498db);
button:hover:not(:disabled) {
background: var(--button-bg, #3498db);
color: white;
}
}
/* Loading state */
.loading {
display: inline-flex;
align-items: center;
gap: 8px;
}
.spinner {
width: 16px;
height: 16px;
border: 2px solid transparent;
border-top: 2px solid currentColor;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`;
@property()
size: 'small' | 'medium' | 'large' = 'medium';
@property()
variant: 'default' | 'success' | 'warning' | 'danger' | 'outline' = 'default';
@property({ type: Boolean })
disabled: boolean = false;
@property({ type: Boolean })
loading: boolean = false;
@property({ type: String })
type: 'button' | 'submit' | 'reset' = 'button';
private handleClick(event: MouseEvent) {
if (this.disabled || this.loading) {
event.preventDefault();
return;
}
this.dispatchEvent(new CustomEvent('button-click', {
detail: {
type: this.type,
variant: this.variant,
timestamp: Date.now()
},
bubbles: true,
composed: true
}));
}
render() {
return html`
<button
?disabled=${this.disabled || this.loading}
type=${this.type}
@click=${this.handleClick}
>
${this.loading ? html`
<span class="loading">
<span class="spinner"></span>
<slot name="loading-text">Loading...</slot>
</span>
` : html`
<slot></slot>
`}
</button>
`;
}
}
// 3. Virtual list component for performance
@customElement('virtual-list')
export class VirtualList extends LitElement {
static styles = css`
:host {
display: block;
height: 400px;
border: 1px solid #ddd;
border-radius: 4px;
overflow: hidden;
position: relative;
}
.viewport {
height: 100%;
overflow-y: auto;
position: relative;
}
.spacer {
position: absolute;
top: 0;
left: 0;
right: 0;
pointer-events: none;
}
.item {
position: absolute;
left: 0;
right: 0;
padding: 12px;
border-bottom: 1px solid #eee;
box-sizing: border-box;
background: white;
}
.item:hover {
background: #f8f9fa;
}
.loading {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
background: #f8f9fa;
color: #6c757d;
}
.no-items {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: #6c757d;
font-style: italic;
}
`;
@property({ type: Array })
items: any[] = [];
@property({ type: Number })
itemHeight: number = 40;
@property({ type: Function })
renderItem: (item: any, index: number) => any = (item, index) => html`<div>Item ${index}: ${JSON.stringify(item)}</div>`;
@state()
private scrollTop: number = 0;
@state()
private containerHeight: number = 400;
@query('.viewport')
private viewport!: HTMLElement;
@query('.spacer')
private spacer!: HTMLElement;
connectedCallback() {
super.connectedCallback();
this.addEventListener('scroll', this.handleScroll);
}
disconnectedCallback() {
super.disconnectedCallback();
this.removeEventListener('scroll', this.handleScroll);
}
private handleScroll = (event: Event) => {
const target = event.target as HTMLElement;
this.scrollTop = target.scrollTop;
};
private get visibleItems() {
const startIdx = Math.floor(this.scrollTop / this.itemHeight);
const visibleCount = Math.ceil(this.containerHeight / this.itemHeight);
const endIdx = Math.min(startIdx + visibleCount + 1, this.items.length);
return { start: startIdx, end: endIdx };
}
private updateSpacerHeight() {
if (this.spacer) {
this.spacer.style.height = `${this.items.length * this.itemHeight}px`;
}
}
protected updated() {
this.updateSpacerHeight();
}
render() {
if (this.items.length === 0) {
return html`
<div class="viewport">
<div class="no-items">No items to display</div>
</div>
`;
}
const { start, end } = this.visibleItems();
const visibleItems = this.items.slice(start, end);
return html`
<div class="viewport">
<div class="spacer"></div>
${visibleItems.map((item, index) => {
const actualIndex = start + index;
return html`
<div
class="item"
style="top: ${actualIndex * this.itemHeight}px; height: ${this.itemHeight}px;"
data-index="${actualIndex}"
>
${this.renderItem(item, actualIndex)}
</div>
`;
})}
</div>
`;
}
}
// 4. Form validation with async validation
@customElement('validated-form')
export class ValidatedForm extends LitElement {
static styles = css`
:host {
display: block;
padding: 20px;
border: 2px solid #8e44ad;
border-radius: 8px;
max-width: 500px;
margin: 16px auto;
}
.form-group {
margin-bottom: 16px;
}
label {
display: block;
margin-bottom: 4px;
font-weight: bold;
color: #2c3e50;
}
input, select, textarea {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
font-size: 16px;
}
input:focus, select:focus, textarea:focus {
outline: none;
border-color: #8e44ad;
}
input.error, select.error, textarea.error {
border-color: #e74c3c;
}
.error-message {
color: #e74c3c;
font-size: 0.9em;
margin-top: 4px;
}
.validation-status {
display: flex;
align-items: center;
gap: 8px;
margin-top: 4px;
}
.spinner {
width: 16px;
height: 16px;
border: 2px solid transparent;
border-top: 2px solid #8e44ad;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.valid {
color: #27ae60;
}
.invalid {
color: #e74c3c;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
button {
background: #8e44ad;
color: white;
border: none;
padding: 12px 24px;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
button:hover:not(:disabled) {
background: #7d3c98;
}
button:disabled {
background: #bdc3c7;
cursor: not-allowed;
}
.form-summary {
background: #f8f9fa;
padding: 16px;
border-radius: 4px;
margin-top: 20px;
border-left: 4px solid #8e44ad;
}
`;
@state()
private formData = {
username: '',
email: '',
password: '',
confirmPassword: ''
};
@state()
private validationStatus: Record<string, 'idle' | 'validating' | 'valid' | 'invalid'> = {};
@state()
private errors: Record<string, string> = {};
@state()
private isSubmitting: boolean = false;
@state()
private isSubmitted: boolean = false;
// Simulate async validation
private async validateUsername(username: string): Promise<boolean> {
await new Promise(resolve => setTimeout(resolve, 1000));
// Simulate API call to check if username exists
const existingUsernames = ['admin', 'user', 'test', 'john'];
return !existingUsernames.includes(username.toLowerCase());
}
private async validateEmail(email: string): Promise<boolean> {
await new Promise(resolve => setTimeout(resolve, 500));
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
private validatePassword(password: string): boolean {
return password.length >= 8;
}
private validatePasswordsMatch(password: string, confirmPassword: string): boolean {
return password === confirmPassword;
}
private async validateField(field: string) {
const value = this.formData[field as keyof typeof this.formData];
this.validationStatus = { ...this.validationStatus, [field]: 'validating' };
this.errors = { ...this.errors, [field]: '' };
try {
let isValid = false;
switch (field) {
case 'username':
if (!value.trim()) {
throw new Error('Username is required');
}
isValid = await this.validateUsername(value);
if (!isValid) {
throw new Error('Username is already taken');
}
break;
case 'email':
if (!value.trim()) {
throw new Error('Email is required');
}
isValid = await this.validateEmail(value);
if (!isValid) {
throw new Error('Please enter a valid email address');
}
break;
case 'password':
if (!value) {
throw new Error('Password is required');
}
isValid = this.validatePassword(value);
if (!isValid) {
throw new Error('Password must be at least 8 characters long');
}
break;
case 'confirmPassword':
if (!value) {
throw new Error('Please confirm your password');
}
isValid = this.validatePasswordsMatch(this.formData.password, value);
if (!isValid) {
throw new Error('Passwords do not match');
}
break;
}
this.validationStatus = { ...this.validationStatus, [field]: 'valid' };
} catch (error) {
this.validationStatus = { ...this.validationStatus, [field]: 'invalid' };
this.errors = { ...this.errors, [field]: error instanceof Error ? error.message : 'Validation failed' };
}
}
private handleInput(field: string, event: Event) {
const target = event.target as HTMLInputElement;
this.formData = { ...this.formData, [field]: target.value };
// Trigger validation after a short delay
setTimeout(() => this.validateField(field), 300);
}
private async handleSubmit() {
// Validate all fields
await Promise.all([
this.validateField('username'),
this.validateField('email'),
this.validateField('password'),
this.validateField('confirmPassword')
]);
// Check if all fields are valid
const allValid = Object.values(this.validationStatus).every(status => status === 'valid');
if (!allValid) {
return;
}
this.isSubmitting = true;
// Simulate form submission
await new Promise(resolve => setTimeout(resolve, 2000));
this.isSubmitting = false;
this.isSubmitted = true;
// Reset after 3 seconds
setTimeout(() => {
this.isSubmitted = false;
this.formData = {
username: '',
email: '',
password: '',
confirmPassword: ''
};
this.validationStatus = {};
this.errors = {};
}, 3000);
}
private getValidationIcon(field: string) {
const status = this.validationStatus[field];
switch (status) {
case 'validating':
return html`<div class="spinner"></div>`;
case 'valid':
return html`<span class="valid">✓</span>`;
case 'invalid':
return html`<span class="invalid">✗</span>`;
default:
return '';
}
}
private isFormValid() {
return Object.values(this.validationStatus).every(status => status === 'valid');
}
render() {
return html`
<h3>Advanced Form with Async Validation</h3>
<form @submit=${(e: Event) => { e.preventDefault(); this.handleSubmit(); }}>
<div class="form-group">
<label for="username">Username *</label>
<input
id="username"
type="text"
.value=${this.formData.username}
@input=${(e: Event) => this.handleInput('username', e)}
class=${this.validationStatus.username === 'invalid' ? 'error' : ''}
placeholder="Choose a unique username"
?disabled=${this.isSubmitting}
/>
<div class="validation-status">
${this.getValidationIcon('username')}
<span class="error-message">${this.errors.username || ''}</span>
</div>
</div>
<div class="form-group">
<label for="email">Email *</label>
<input
id="email"
type="email"
.value=${this.formData.email}
@input=${(e: Event) => this.handleInput('email', e)}
class=${this.validationStatus.email === 'invalid' ? 'error' : ''}
placeholder="[email protected]"
?disabled=${this.isSubmitting}
/>
<div class="validation-status">
${this.getValidationIcon('email')}
<span class="error-message">${this.errors.email || ''}</span>
</div>
</div>
<div class="form-group">
<label for="password">Password *</label>
<input
id="password"
type="password"
.value=${this.formData.password}
@input=${(e: Event) => this.handleInput('password', e)}
class=${this.validationStatus.password === 'invalid' ? 'error' : ''}
placeholder="Min 8 characters"
?disabled=${this.isSubmitting}
/>
<div class="validation-status">
${this.getValidationIcon('password')}
<span class="error-message">${this.errors.password || ''}</span>
</div>
</div>
<div class="form-group">
<label for="confirmPassword">Confirm Password *</label>
<input
id="confirmPassword"
type="password"
.value=${this.formData.confirmPassword}
@input=${(e: Event) => this.handleInput('confirmPassword', e)}
class=${this.validationStatus.confirmPassword === 'invalid' ? 'error' : ''}
placeholder="Re-enter your password"
?disabled=${this.isSubmitting}
/>
<div class="validation-status">
${this.getValidationIcon('confirmPassword')}
<span class="error-message">${this.errors.confirmPassword || ''}</span>
</div>
</div>
<button type="submit" ?disabled=${this.isSubmitting || !this.isFormValid()}>
${this.isSubmitting ? 'Submitting...' : 'Create Account'}
</button>
</form>
${this.isSubmitted ? html`
<div class="form-summary">
<h4>✅ Account Created Successfully!</h4>
<pre>${JSON.stringify(this.formData, null, 2)}</pre>
</div>
` : ''}
`;
}
}
export { CardComponent, ThemeableButton, VirtualList, ValidatedForm };