tutoriales.com

Svelte y Web Components: Encapsulando Componentes Reutilizables y el DOM Sombra 🎩

Este tutorial te guiará a través de la integración de Svelte con Web Components, explorando cómo encapsular la lógica y el estilo de tus componentes Svelte utilizando el DOM Sombra para una máxima reutilización y aislamiento. Descubre las ventajas de esta poderosa combinación para construir aplicaciones web robustas y modulares.

Intermedio15 min de lectura4 views
Reportar error

La modularidad y la reutilización son pilares fundamentales en el desarrollo web moderno. Svelte, con su enfoque en la compilación a JavaScript vainilla, y Web Components, con sus estándares nativos del navegador, ofrecen una combinación poderosa para lograr estos objetivos. En este tutorial, exploraremos cómo puedes aprovechar ambos para construir componentes altamente encapsulados y reutilizables.

¿Qué Son los Web Components? 🤔

Los Web Components son un conjunto de estándares W3C que permiten crear etiquetas HTML personalizadas y reutilizables con su propia funcionalidad y estilo encapsulados. Son una característica nativa de los navegadores, lo que significa que no requieren bibliotecas o frameworks adicionales para funcionar (aunque frameworks como Svelte pueden ayudar en su creación).

Los pilares de los Web Components son:

  • Custom Elements: Permiten definir nuevas etiquetas HTML (por ejemplo, <mi-boton>, <mi-galeria>).
  • Shadow DOM: Proporciona un subárbol DOM encapsulado para un elemento, aislado del resto del documento. Esto garantiza que el estilo y la estructura interna del componente no sean afectados por estilos externos ni afecten a elementos externos.
  • HTML Templates: Las etiquetas <template> y <slot> permiten definir fragmentos de marcado HTML reutilizables que no se renderizan inmediatamente, pero pueden ser clonados y utilizados por Custom Elements.
📌 Nota: Aunque `HTML Templates` son parte de los estándares, Svelte maneja su propia reactividad y estructura, por lo que nos centraremos más en `Custom Elements` y `Shadow DOM` al integrarlos.

¿Por Qué Combinar Svelte y Web Components? ✨

Svelte es un compilador que produce JavaScript ligero y eficiente. Los Web Components son estándares nativos. Esta combinación ofrece varias ventajas:

  1. Aislamiento de Estilos y Comportamiento: El Shadow DOM de Web Components garantiza que los estilos y la lógica de tu componente Svelte encapsulado no "goteen" fuera ni sean afectados por el CSS global, eliminando conflictos.
  2. Reutilización Universal: Un componente Svelte exportado como Web Component puede ser utilizado en cualquier proyecto web, independientemente de si usa Svelte, React, Vue, Angular o incluso vanilla JavaScript. Son agnósticos al framework.
  3. Encapsulación Robusta: La combinación proporciona un nivel de encapsulación superior, ideal para construir bibliotecas de componentes o sistemas de diseño.
  4. Interopbilidad Sencilla: Facilita la integración de componentes desarrollados en Svelte con aplicaciones existentes que no usan Svelte.
🔥 Importante: Aunque Svelte ya compila a JavaScript vainilla, convertir un componente Svelte en un Web Component añade una capa extra de encapsulación (gracias al Shadow DOM) que Svelte por sí solo no provee de forma nativa para la integración externa.

Creando un Componente Svelte para Exportar como Web Component 🛠️

Vamos a crear un componente Svelte simple y luego lo exportaremos como un Custom Element.

1. Inicializar un Proyecto Svelte

Si aún no tienes un proyecto Svelte, puedes crear uno rápidamente:

npm create vite@latest svelte-web-components -- --template svelte
cd svelte-web-components
npm install
npm run dev

2. Crear el Componente Svelte (MyButton.svelte)

Crearemos un componente de botón interactivo.

Crea un archivo src/lib/MyButton.svelte con el siguiente contenido:

<script>
  export let label = 'Haz clic';
  export let color = 'rebeccapurple';
  export let count = 0;

  function handleClick() {
    count += 1;
    console.log(`Botón clicado: ${count} veces`);
    // Podemos emitir un evento personalizado si queremos que el exterior lo escuche
    const event = new CustomEvent('my-button-click', {
      detail: { count, label },
      bubbles: true,
      composed: true
    });
    window.dispatchEvent(event);
  }
</script>

<button on:click={handleClick} style="background-color: {color}; color: white; padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer;">
  {label} ({count})
</button>

<style>
  /* Estos estilos se aplicarán dentro del Shadow DOM cuando se exporte */
  button {
    font-family: sans-serif;
    font-size: 1.2em;
    transition: background-color 0.2s ease-in-out;
  }

  button:hover {
    filter: brightness(1.2);
  }

  button:active {
    transform: translateY(1px);
  }
</style>

Aquí tenemos:

  • Propiedades (export let): label, color, count para configurar el botón desde el exterior.
  • Estado interno: count para llevar la cuenta de clics.
  • Manejador de eventos: handleClick incrementa el contador y emite un CustomEvent para que el mundo exterior pueda reaccionar a los clics.
  • Estilos: Estilos específicos para el botón que se encapsularán.

3. Exportar el Componente Svelte como Custom Element 🚀

Para que Svelte compile nuestro componente como un Custom Element, necesitamos modificar su configuración de compilación. Svelte ofrece una opción customElement: true.

Modifica src/main.js (o crea uno nuevo si lo prefieres) para registrar el componente como un Custom Element:

// src/main.js
import MyButton from './lib/MyButton.svelte';

// Registramos el componente Svelte como un Custom Element
// El segundo argumento ({ tag: 'my-svelte-button' }) le indica a Svelte que compile
// este componente como un Custom Element y le asigna el nombre de etiqueta.
// Si no se especifica 'tag', el nombre del archivo (MyButton) se usa en kebab-case (my-button).
// Sin embargo, por convención, los Custom Elements deben contener un guion.

// Para usar Shadow DOM, necesitamos decirle a Svelte que lo haga.
// Svelte tiene una opción `customElement: true` en la configuración de la compilación,
// pero cuando se usa `svelte/register` o `create-custom-element` (en versiones anteriores de Svelte),
// o al pasar opciones a la función de montaje, podemos controlar esto.
// Con Svelte 3/4, la forma más sencilla es configurar `customElement: true` en la configuración de rollup/vite
// o usar una envoltura para definir el Custom Element.

// Svelte 3/4 tiene una forma nativa de exportar un componente como Custom Element.
// No necesitas un wrapper externo. Solo necesitas importar el componente y usarlo.
// Pero si quieres que el Shadow DOM esté habilitado, necesitas compilarlo con esa opción.

// Si tu proyecto Svelte usa Vite y quieres compilar a Custom Element directamente,
// necesitas configurar el plugin de Svelte en `vite.config.js`:
// import { svelte } from '@sveltejs/vite-plugin-svelte';
// svelte({ 
//   compilerOptions: { 
//     customElement: true 
//   } 
// })

// Sin embargo, para un control más granular, podemos definirlo manualmente o usar un paquete.
// En Svelte 3/4, los componentes se pueden registrar directamente como Custom Elements si están
// configurados en el compilador. Aquí, vamos a simularlo o mostrar cómo Svelte lo haría.

// Una forma común de registrar componentes Svelte como Custom Elements es usar la sintaxis:
// `MyButton.element = true;` o configurarlo en el plugin de compilación.

// Para este tutorial, asumiremos que nuestro entorno de compilación (Vite + Svelte plugin)
// está configurado para producir Custom Elements.
// Si estás usando Vite, asegúrate de que tu `vite.config.js` tenga algo así:

// vite.config.js
// import { defineConfig } from 'vite'
// import { svelte } from '@sveltejs/vite-plugin-svelte'

// export default defineConfig({
//   plugins: [svelte({
//     compilerOptions: {
//       customElement: true // Esto activa la compilación como Custom Element
//     }
//   })]
// })

// Con esta configuración, Svelte transforma tu componente en una clase de Custom Element
// que puedes registrar globalmente.

// Para nuestro ejemplo, vamos a simular el registro de un Custom Element que usa Shadow DOM.
// En un escenario real con la configuración de `customElement: true` en Vite/Svelte,
// cada componente `.svelte` que quieras registrar como Custom Element debe ser importado
// y automáticamente estará disponible para ser definido globalmente.

// Para garantizar que el Custom Element usa Shadow DOM, la opción `customElement: true`
// del compilador de Svelte lo habilita por defecto. Si quieres cambiar esto, puedes usar
// `customElement: { shadow: 'none' }` para Shadow DOM abierto o deshabilitarlo.

// Ejemplo de cómo registrar un Custom Element (Svelte automáticamente generaría algo similar):
class SvelteMyButton extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' }); // Usar Shadow DOM 'open' para accesibilidad externa
    this.component = null; // Instancia del componente Svelte
  }

  static get observedAttributes() {
    return ['label', 'color', 'count'];
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (this.component) {
      // Actualizar props del componente Svelte cuando cambian los atributos del CE
      if (name === 'count' || name === 'label' || name === 'color') {
        this.component.$set({ [name]: name === 'count' ? parseInt(newValue) : newValue });
      }
    }
  }

  connectedCallback() {
    const props = {};
    for (const attr of SvelteMyButton.observedAttributes) {
      if (this.hasAttribute(attr)) {
        props[attr] = attr === 'count' ? parseInt(this.getAttribute(attr)) : this.getAttribute(attr);
      }
    }

    // Montar el componente Svelte dentro del Shadow DOM
    this.component = new MyButton({
      target: this.shadowRoot, // Renderiza dentro del Shadow DOM
      props: props
    });

    // Escuchar el evento personalizado desde el componente Svelte
    // Los eventos que hacen `bubbles: true` y `composed: true` pueden salir del Shadow DOM
    window.addEventListener('my-button-click', (e) => {
      console.log('Evento de botón Svelte escuchado fuera del Shadow DOM:', e.detail);
      // O re-emitir el evento desde el Custom Element si se necesita un evento nativo
      this.dispatchEvent(new CustomEvent('button-clicked', {
        detail: e.detail,
        bubbles: true,
        composed: true
      }));
    });
  }

  disconnectedCallback() {
    if (this.component) {
      this.component.$destroy();
    }
  }
}

// Definir el Custom Element
if (!customElements.get('my-svelte-button')) {
  customElements.define('my-svelte-button', SvelteMyButton);
}

// Para una aplicación Svelte normal (no como Custom Element), podrías hacer:
// const app = new MyButton({
//   target: document.body,
//   props: {
//     label: 'Botón Svelte Directo',
//     color: 'darkgreen'
//   }
// });

// export default app;
💡 Consejo: Para que este proceso sea más automático con Svelte 3/4 y Vite, la opción `compilerOptions.customElement: true` en el plugin de Svelte se encarga de gran parte de la lógica de envoltura y registro del Custom Element, incluyendo el Shadow DOM por defecto. El código anterior es una simulación para entender el proceso manual. En un proyecto real, solo necesitarías configurar `vite.config.js` y el componente Svelte se convertiría automáticamente en un Custom Element disponible para el navegador.

4. Consumir el Web Component 🌍

Ahora que nuestro componente Svelte ha sido compilado y registrado como un Custom Element, podemos usarlo en cualquier parte de nuestra aplicación HTML, ¡o incluso en otros frameworks!

Modifica src/App.svelte (o cualquier archivo HTML estático) para usar nuestro nuevo Custom Element:

<!-- src/App.svelte (o un archivo index.html simple) -->
<script>
  import './main.js'; // Asegúrate de que el archivo que registra el Custom Element se importe
  let svelteAppClickCount = 0;

  function handleGlobalButtonClick(event) {
    console.log('Evento global de Custom Element capturado:', event.detail);
    svelteAppClickCount = event.detail.count;
  }
</script>

<main>
  <h1>Usando Svelte con Web Components</h1>

  <p>Este es un componente Svelte exportado como Web Component:</p>

  <my-svelte-button
    label="Clic aquí desde HTML"
    color="#28a745"
    count="0"
    on:button-clicked={handleGlobalButtonClick} <!-- Escuchar el evento re-emitido por el Custom Element -->
  ></my-svelte-button>

  <my-svelte-button
    label="Otro botón"
    color="#007bff"
    count="5"
  ></my-svelte-button>

  <p>Último conteo de clics del botón principal desde la App Svelte: {svelteAppClickCount}</p>

  <style>
    /* Estos estilos no deberían afectar a los botones dentro del Shadow DOM */
    p {
      font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
      color: #333;
    }

    my-svelte-button {
      display: block;
      margin: 15px 0;
    }
  </style>
</main>

Si abres tu aplicación en el navegador, verás los botones renderizados. Intenta inspeccionar uno de los botones con las herramientas de desarrollador. Verás el Shadow Root y cómo los estilos y el contenido de tu componente Svelte están encapsulados dentro de él.

Aplicación Host Custom Element (<my-svelte-button>) Shadow DOM Svelte Component (Lógica y Estilos) Props (Datos) Eventos

Comunicación entre el Web Component y la Aplicación Externa 🌐

La comunicación es crucial cuando se trabaja con componentes encapsulados.

1. Propiedades (Props) ✨

Las propiedades en un Custom Element se pasan como atributos HTML. En Svelte, estas propiedades se definen con export let.

En nuestro ejemplo, label, color y count se pasan como atributos:

<my-svelte-button label="Mi Etiqueta" color="red" count="10"></my-svelte-button>
📌 Nota: Los atributos HTML son siempre cadenas de texto. Si necesitas pasar números o booleanos, tendrás que convertirlos dentro de tu Custom Element (como hicimos con `parseInt(newValue)` para `count`). Para objetos complejos, considera pasar un ID y que el componente lo resuelva o usar propiedades JavaScript directamente si el Custom Element es accesible vía JS.

2. Eventos Personalizados (Custom Events) 💬

Para que un componente encapsulado notifique a la aplicación host sobre cambios o interacciones, usamos CustomEvent.

En MyButton.svelte:

const event = new CustomEvent('my-button-click', {
  detail: { count, label },
  bubbles: true,
  composed: true
});
window.dispatchEvent(event); // O this.dispatchEvent(event) si el CE lo re-emite

Las propiedades bubbles: true y composed: true son cruciales:

  • bubbles: true: Permite que el evento "suba" por el DOM y pueda ser capturado por un ancestro (document, window, o el elemento padre del Custom Element).
  • composed: true: Permite que el evento atraviese los límites del Shadow DOM. Sin esto, el evento se detendría en el Shadow Root.

En la aplicación host (o en cualquier script), puedes escuchar estos eventos:

window.addEventListener('my-button-click', (e) => {
  console.log('Botón clicado:', e.detail);
});
// Si el Custom Element re-emite un evento específico
document.querySelector('my-svelte-button').addEventListener('button-clicked', (e) => {
  console.log('Evento de Custom Element re-emitido:', e.detail);
});

Slots y Composición de Contenido 🧩

Los slots son una característica fundamental de los Web Components que permite a los usuarios de tu componente inyectar su propio contenido HTML dentro de él. Svelte tiene su propia implementación de slots que funciona perfectamente con Custom Elements.

1. Modificar MyButton.svelte para usar Slots

Vamos a modificar nuestro botón para que acepte contenido arbitrario en lugar de solo una etiqueta.

<!-- src/lib/MyButtonWithSlot.svelte -->
<script>
  export let color = 'rebeccapurple';
  export let count = 0;

  function handleClick() {
    count += 1;
    console.log(`Botón clicado: ${count} veces`);
    const event = new CustomEvent('my-button-click', {
      detail: { count },
      bubbles: true,
      composed: true
    });
    window.dispatchEvent(event);
  }
</script>

<button on:click={handleClick} style="background-color: {color}; color: white; padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer;">
  <slot>Contenido por defecto</slot> <!-- Aquí se inyectará el contenido -->
  <span class="counter">({count})</span>
</button>

<style>
  button {
    font-family: sans-serif;
    font-size: 1.2em;
    transition: background-color 0.2s ease-in-out;
    display: flex;
    align-items: center;
    gap: 5px;
  }

  button:hover {
    filter: brightness(1.2);
  }

  button:active {
    transform: translateY(1px);
  }

  .counter {
    font-size: 0.9em;
    opacity: 0.8;
  }
</style>

Ahora, el <slot> actuará como un placeholder para el contenido que se le pase al Custom Element.

2. Actualizar main.js para registrar el nuevo componente

// ... (código anterior de SvelteMyButton si lo estás usando) ...

import MyButtonWithSlot from './lib/MyButtonWithSlot.svelte';

class SvelteButtonWithSlot extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.component = null;
  }

  static get observedAttributes() {
    return ['color', 'count'];
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (this.component) {
      if (name === 'count' || name === 'color') {
        this.component.$set({ [name]: name === 'count' ? parseInt(newValue) : newValue });
      }
    }
  }

  connectedCallback() {
    const props = {};
    for (const attr of SvelteButtonWithSlot.observedAttributes) {
      if (this.hasAttribute(attr)) {
        props[attr] = attr === 'count' ? parseInt(this.getAttribute(attr)) : this.getAttribute(attr);
      }
    }

    this.component = new MyButtonWithSlot({
      target: this.shadowRoot,
      props: props,
      // Svelte maneja los slots automáticamente con customElement: true
      // Si estamos simulando, necesitaríamos una forma de pasar el contenido del slot
      // al componente Svelte, lo cual es más complejo manualmente.
      // Por eso `customElement: true` en el compilador es el camino recomendado.
    });

    // Si necesitamos pasar contenido de slot manualmente al Shadow DOM de un componente Svelte
    // que no fue compilado con `customElement: true`, sería complejo y no es el enfoque ideal.
    // La clave es que la compilación con `customElement: true` se encarga de esto.

    window.addEventListener('my-button-click', (e) => {
      console.log('Evento de botón con slot escuchado fuera del Shadow DOM:', e.detail);
      this.dispatchEvent(new CustomEvent('slot-button-clicked', {
        detail: e.detail,
        bubbles: true,
        composed: true
      }));
    });

    // Clonar los nodos hijos del Custom Element y moverlos al Shadow DOM
    // Esto solo es necesario si Svelte NO gestiona directamente el Shadow DOM
    // a través de `customElement: true`. Con `customElement: true`, Svelte lo hace por ti.
    // Para nuestro ejemplo manual, estamos asumiendo que el componente Svelte se monta en el shadowRoot.
    // Los slots se gestionarán internamente por Svelte una vez montado.
    while (this.firstChild) {
      this.shadowRoot.appendChild(this.firstChild);
    }
  }

  disconnectedCallback() {
    if (this.component) {
      this.component.$destroy();
    }
  }
}

if (!customElements.get('my-svelte-slot-button')) {
  customElements.define('my-svelte-slot-button', SvelteButtonWithSlot);
}

3. Consumir el Web Component con Slots en HTML

<!-- src/App.svelte (o index.html) -->
<script>
  // ... import './main.js';
  // ... (código anterior)

  let slotButtonCount = 0;
  function handleSlotButtonClick(event) {
    console.log('Evento de botón con slot capturado:', event.detail);
    slotButtonCount = event.detail.count;
  }
</script>

<main>
  <!-- ... (contenido anterior) ... -->

  <h2>Componente Svelte con Slots como Web Component</h2>
  <my-svelte-slot-button
    color="#ffc107"
    count="0"
    on:slot-button-clicked={handleSlotButtonClick}
  >
    <span>Haz clic aquí <mark>¡Ahora con slot!</mark></span>
    <img src="/favicon.png" alt="icono" style="height: 1.2em; vertical-align: middle; margin-left: 5px;"/>
  </my-svelte-slot-button>

  <my-svelte-slot-button color="#dc3545" count="3">
    <strong>Botón de Peligro</strong>
  </my-svelte-slot-button>

  <p>Último conteo de clics del botón con slot: {slotButtonCount}</p>

  <!-- ... (estilos y resto de contenido) ... -->
</main>

Ahora, el contenido HTML entre las etiquetas <my-svelte-slot-button> se proyectará en el <slot> definido en tu componente Svelte. Esto permite una flexibilidad increíble para la composición.

Aplicación Host <my-svelte-slot-button> Contenido de Slot (e.g., <span>) (e.g., <img>) Shadow DOM Svelte Component <slot> Proyección

Consideraciones y Mejores Prácticas 💡

1. Nomenclatura de Custom Elements

Los nombres de los Custom Elements deben:

  • Contener siempre un guion (-) (por ejemplo, mi-componente, no micomponente).
  • Ser únicos globalmente para evitar conflictos. Usa prefijos específicos de tu organización o proyecto (por ejemplo, mi-org-button).

2. Gestión de Estilos con Shadow DOM

  • Estilos dentro del Shadow DOM: Los estilos definidos en la sección <style> de tu componente Svelte se encapsularán dentro del Shadow DOM. Esto significa que los estilos globales de tu aplicación no los afectarán, y viceversa.
  • Estilos globales para Web Components: Si necesitas aplicar estilos desde el exterior a un Web Component (por ejemplo, margin o width), puedes hacerlo si el Custom Element en sí no tiene un Shadow DOM o si usas la pseudo-clase CSS ::part() o variables CSS personalizadas.
  • Variables CSS: Son una excelente manera de permitir la personalización de estilos a través de los límites del Shadow DOM. Tu componente Svelte puede usar variables CSS, y la aplicación host puede definirlas:
/* En tu componente Svelte (Shadow DOM) */
button {
background-color: var(--button-bg-color, rebeccapurple);
color: var(--button-text-color, white);
}
/* En tu aplicación host (global) */
my-svelte-button {
--button-bg-color: steelblue;
--button-text-color: lightgray;
}

3. Rendimiento y Tamaño de Archivo

Svelte ya produce un bundle muy pequeño. Cuando se compila como Web Component, el overhead es mínimo, ya que solo se incluye el runtime necesario para el componente.

4. Accesibilidad

El Shadow DOM puede complicar ligeramente la accesibilidad si no se maneja correctamente. Asegúrate de que tus Custom Elements sean semánticamente correctos o que implementen los roles y atributos ARIA adecuados.

Conclusión ✅

Combinar Svelte con Web Components es una estrategia poderosa para construir una arquitectura de componentes web robusta, modular y extremadamente reutilizable. Svelte se encarga de la reactividad y la eficiencia, mientras que los Web Components proporcionan el encapsulamiento estándar del navegador y la interoperabilidad universal. Ya sea que estés construyendo una biblioteca de UI, integrando componentes en una aplicación existente o simplemente buscando las mejores prácticas para la modularidad, esta combinación te ofrece una solución elegante y eficiente.

¡Experimenta con esta combinación y lleva tus habilidades de desarrollo web al siguiente nivel!

Tutoriales relacionados

Comentarios (0)

Aún no hay comentarios. ¡Sé el primero!