🚀 Web Components senza Shadow DOM

I Web Components sono una tecnologia web standard che ti permette di creare elementi HTML personalizzati e riutilizzabili. In questa guida vedremo come creare custom tag senza utilizzare il Shadow DOM, mantenendo la semplicità ma ottenendo comunque funzionalità avanzate.

📚 Cos'è un Web Component?

Un Web Component è un elemento HTML personalizzato che puoi definire usando JavaScript. È composto principalmente da:

🛠️ Struttura Base di un Custom Element

Ogni Web Component estende la classe HTMLElement e deve essere registrato con customElements.define():

class MioElemento extends HTMLElement {
    constructor() {
        super();
        // Inizializzazione dell'elemento
    }
    
    connectedCallback() {
        // Chiamato quando l'elemento viene aggiunto al DOM
        this.render();
    }
    
    disconnectedCallback() {
        // Chiamato quando l'elemento viene rimosso dal DOM
    }
    
    attributeChangedCallback(name, oldValue, newValue) {
        // Chiamato quando un attributo osservato cambia
    }
    
    static get observedAttributes() {
        // Lista degli attributi da osservare
        return ['attributo1', 'attributo2'];
    }
    
    render() {
        // Logica per renderizzare il contenuto
        this.innerHTML = `<div>Contenuto del mio elemento</div>`;
    }
}

// Registrazione del custom element
customElements.define('mio-elemento', MioElemento);

🎯 Esempio 1: Counter Interattivo

Creiamo un contatore che può essere incrementato cliccandoci sopra:

class MyCounter extends HTMLElement {
    constructor() {
        super();
        this.count = parseInt(this.getAttribute('start') || '0');
    }
    
    connectedCallback() {
        this.render();
        this.addEventListener('click', this.increment.bind(this));
    }
    
    increment() {
        this.count++;
        this.render();
        
        // Emette un evento personalizzato
        this.dispatchEvent(new CustomEvent('counterChanged', {
            detail: { value: this.count },
            bubbles: true
        }));
    }
    
    render() {
        this.innerHTML = `
            <div class="count">${this.count}</div>
            <div class="label">Clicca per incrementare</div>
        `;
    }
}

customElements.define('my-counter', MyCounter);
🔥 Demo Counter:
Eventi: Clicca sui counter per vedere gli eventi

📊 Esempio 2: Barra di Progresso Animata

Una barra di progresso che si aggiorna automaticamente e risponde ai cambiamenti degli attributi:

class MyProgress extends HTMLElement {
    constructor() {
        super();
        this.value = parseInt(this.getAttribute('value') || '0');
        this.max = parseInt(this.getAttribute('max') || '100');
    }
    
    static get observedAttributes() {
        return ['value', 'max'];
    }
    
    connectedCallback() {
        this.render();
        this.startAutoIncrement();
    }
    
    attributeChangedCallback(name, oldValue, newValue) {
        if (name === 'value') {
            this.value = parseInt(newValue || '0');
        } else if (name === 'max') {
            this.max = parseInt(newValue || '100');
        }
        this.render();
    }
    
    startAutoIncrement() {
        setInterval(() => {
            if (this.value < this.max) {
                this.value += Math.random() * 3;
                if (this.value > this.max) this.value = this.max;
                this.setAttribute('value', Math.floor(this.value));
            }
        }, 200);
    }
    
    render() {
        const percentage = Math.min((this.value / this.max) * 100, 100);
        this.innerHTML = `
            <div class="progress-bar" style="width: ${percentage}%"></div>
            <div class="progress-text">${Math.floor(this.value)}/${this.max}</div>
        `;
    }
}

customElements.define('my-progress', MyProgress);
📊 Demo Progress Bar:

🔘 Esempio 3: Toggle Switch con Eventi

Un interruttore che cambia stato e comunica con altri elementi:

class MyToggle extends HTMLElement {
    constructor() {
        super();
        this.active = this.hasAttribute('active');
        this.label = this.getAttribute('label') || 'Toggle';
    }
    
    connectedCallback() {
        this.render();
        this.querySelector('.toggle-container').addEventListener('click', this.toggle.bind(this));
    }
    
    toggle() {
        this.active = !this.active;
        
        if (this.active) {
            this.setAttribute('active', '');
            this.classList.add('active');
        } else {
            this.removeAttribute('active');
            this.classList.remove('active');
        }
        
        // Emette evento di cambio stato
        this.dispatchEvent(new CustomEvent('toggleChanged', {
            detail: { 
                active: this.active,
                label: this.label
            },
            bubbles: true
        }));
    }
    
    render() {
        this.innerHTML = `
            <div class="toggle-container">
                <div class="toggle-switch">
                    <div class="toggle-knob"></div>
                </div>
                <span class="toggle-label">${this.label}</span>
            </div>
        `;
        
        if (this.active) {
            this.classList.add('active');
        }
    }
}

customElements.define('my-toggle', MyToggle);
🔘 Demo Toggle:
Stato: Clicca sui toggle per vedere i cambiamenti

🎪 Comunicazione tra Components

I Web Components possono comunicare tra loro attraverso eventi personalizzati. Ecco come gestire la comunicazione:

// Ascoltare eventi da altri components
document.addEventListener('counterChanged', (event) => {
    console.log('Counter cambiato:', event.detail.value);
});

// Inviare dati a un component specifico
const myCounter = document.querySelector('my-counter');
myCounter.setAttribute('start', '50');

// Comunicazione parent-child
class ParentComponent extends HTMLElement {
    connectedCallback() {
        this.innerHTML = `
            <my-counter start="0"></my-counter>
            <div id="display">Valore: 0</div>
        `;
        
        this.addEventListener('counterChanged', (e) => {
            this.querySelector('#display').textContent = `Valore: ${e.detail.value}`;
        });
    }
}
💡 Tip: Gli eventi personalizzati con bubbles: true si propagano attraverso il DOM, permettendo una comunicazione efficace tra components.

🔧 Lifecycle Methods

I Web Components hanno diversi metodi del ciclo di vita che puoi utilizzare:

⚡ Best Practices

✅ Da Fare:

❌ Da Evitare:

🌟 Vantaggi senza Shadow DOM

Scegliere di non usare Shadow DOM offre diversi vantaggi:

⚠️ Nota: Senza Shadow DOM non hai l'incapsulamento degli stili. I CSS globali influenzeranno i tuoi components e viceversa.

🎯 Conclusioni

I Web Components senza Shadow DOM offrono un ottimo compromesso tra funzionalità e semplicità. Sono perfetti per creare elementi riutilizzabili che si integrano naturalmente con il resto della pagina, mantenendo la possibilità di gestire stati complessi e interazioni avanzate.

Con gli esempi mostrati hai visto come creare components interattivi che:

Ora puoi iniziare a creare i tuoi Web Components personalizzati! 🚀