🎯 Exemples recommandés
Balanced sample collections from various categories for you to explore
Exemples Lit
Exemples de web components Lit incluant les propriétés réactives, les crochets de cycle de vie, les modèles et les patrons modernes de composants
💻 Bonjour Monde Lit typescript
🟢 simple
Exemples de base de composants Lit et applications Bonjour Monde avec décorateurs
// 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 };
💻 Propriétés et Réactivité de Lit typescript
🟡 intermediate
Travailler avec les propriétés réactives, la gestion d'état et le flux de données dans les composants Lit
// 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 };
💻 Cycle de Vie et Événements de Lit typescript
🟡 intermediate
Crochets de cycle de vie, gestion d'événements, événements personnalisés et communication de composants dans 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 };
💻 Patrons Avancés de Lit typescript
🔴 complex
Patrons avancés de Lit incluant la composition, la tematización, les stratégies de test et l'optimisation des performances
// 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 };