Web Components - Custom Elements

Сложность: Продвинутый

Custom Elements

Создание собственных HTML элементов с кастомным поведением.

Примеры пользовательских элементов:

<!-- Использование кастомных элементов -->
<user-rating value="4" max="5"></user-rating>
<collapse-panel title="Важная информация">
    <p>Это содержимое будет скрыто/показано при клике на заголовок.</p>
</collapse-panel>
<live-timer format="HH:mm:ss"></live-timer>

<script>
// Кастомный элемент для рейтинга
class UserRating extends HTMLElement {
    constructor() {
        super();
        this.value = parseInt(this.getAttribute("value")) || 0;
        this.max = parseInt(this.getAttribute("max")) || 5;
    }
    
    connectedCallback() {
        this.render();
        this.addEventListener("click", this.handleClick.bind(this));
    }
    
    render() {
        this.innerHTML = `
            <div class="rating">
                ${Array.from({length: this.max}, (_, i) => 
                    `<span class="star ${i < this.value ? "filled" : ""}" data-value="${i + 1}">★</span>`
                ).join("")}
            </div>
            <style>
                .rating { display: inline-flex; gap: 2px; }
                .star { 
                    cursor: pointer; 
                    font-size: 24px; 
                    color: #ddd; 
                    transition: color 0.2s; 
                }
                .star.filled { color: #ffd700; }
                .star:hover { color: #ffed85; }
            </style>
        `;
    }
    
    handleClick(event) {
        if (event.target.classList.contains("star")) {
            this.value = parseInt(event.target.dataset.value);
            this.setAttribute("value", this.value);
            this.render();
            
            // Генерация кастомного события
            this.dispatchEvent(new CustomEvent("rating-change", {
                detail: { value: this.value },
                bubbles: true
            }));
        }
    }
}

// Кастомный элемент для сворачиваемой панели
class CollapsePanel extends HTMLElement {
    constructor() {
        super();
        this.isExpanded = this.hasAttribute("expanded");
    }
    
    connectedCallback() {
        this.render();
    }
    
    render() {
        const title = this.getAttribute("title") || "Заголовок";
        
        this.innerHTML = `
            <div class="collapse-panel">
                <div class="header">
                    <h3>${title}</h3>
                    <button class="toggle-btn">${this.isExpanded ? "−" : "+"}</button>
                </div>
                <div class="content" style="display: ${this.isExpanded ? "block" : "none"}">
                    <slot></slot>
                </div>
            </div>
            <style>
                .collapse-panel {
                    border: 1px solid #ddd;
                    border-radius: 4px;
                    margin: 10px 0;
                }
                .header {
                    display: flex;
                    justify-content: space-between;
                    align-items: center;
                    padding: 10px 15px;
                    background: #f5f5f5;
                    cursor: pointer;
                }
                .header h3 {
                    margin: 0;
                }
                .toggle-btn {
                    background: none;
                    border: none;
                    font-size: 18px;
                    cursor: pointer;
                    width: 30px;
                    height: 30px;
                    border-radius: 50%;
                    display: flex;
                    align-items: center;
                    justify-content: center;
                }
                .toggle-btn:hover {
                    background: #e0e0e0;
                }
                .content {
                    padding: 15px;
                }
            </style>
        `;
        
        this.querySelector(".header").addEventListener("click", () => {
            this.toggle();
        });
    }
    
    toggle() {
        this.isExpanded = !this.isExpanded;
        const content = this.querySelector(".content");
        const button = this.querySelector(".toggle-btn");
        
        if (this.isExpanded) {
            content.style.display = "block";
            button.textContent = "−";
            this.setAttribute("expanded", "");
        } else {
            content.style.display = "none";
            button.textContent = "+";
            this.removeAttribute("expanded");
        }
    }
}

// Кастомный элемент для живого таймера
class LiveTimer extends HTMLElement {
    constructor() {
        super();
        this.format = this.getAttribute("format") || "HH:mm:ss";
        this.interval = null;
    }
    
    connectedCallback() {
        this.render();
        this.start();
    }
    
    disconnectedCallback() {
        this.stop();
    }
    
    render() {
        this.innerHTML = `
            <div class="live-timer">
                <span class="time">${this.getFormattedTime()}</span>
            </div>
            <style>
                .live-timer {
                    font-family: "Courier New", monospace;
                    font-size: 18px;
                    padding: 10px;
                    background: #2c3e50;
                    color: #ecf0f1;
                    border-radius: 4px;
                    display: inline-block;
                }
            </style>
        `;
    }
    
    getFormattedTime() {
        const now = new Date();
        const hours = now.getHours().toString().padStart(2, "0");
        const minutes = now.getMinutes().toString().padStart(2, "0");
        const seconds = now.getSeconds().toString().padStart(2, "0");
        
        return this.format
            .replace("HH", hours)
            .replace("mm", minutes)
            .replace("ss", seconds);
    }
    
    start() {
        this.interval = setInterval(() => {
            this.querySelector(".time").textContent = this.getFormattedTime();
        }, 1000);
    }
    
    stop() {
        if (this.interval) {
            clearInterval(this.interval);
            this.interval = null;
        }
    }
}

// Регистрация кастомных элементов
customElements.define("user-rating", UserRating);
customElements.define("collapse-panel", CollapsePanel);
customElements.define("live-timer", LiveTimer);

// Обработка событий от кастомных элементов
document.addEventListener("rating-change", (event) => {
    console.log("Рейтинг изменен:", event.detail.value);
});
</script>