tutoriales.com

Svelte y la Magia de los Componentes Slots: Flexibilidad y Reutilización Avanzada ✨

Los Slots de Svelte ofrecen una forma potente de inyectar contenido dinámico en tus componentes, aumentando drásticamente su flexibilidad y reutilización. Este tutorial te guiará a través de los conceptos básicos y avanzados de los slots, incluyendo slots con nombre y slots con props, permitiéndote construir interfaces más modulares y mantenibles. Descubre cómo aprovechar al máximo esta característica fundamental de Svelte.

Intermedio15 min de lectura9 views
Reportar error

Introducción a la Flexibilidad de los Componentes con Slots 📖

En el desarrollo de aplicaciones web modernas, la reutilización de componentes es clave para mantener un código limpio, modular y eficiente. Svelte, con su enfoque en la simplicidad y el rendimiento, nos ofrece una herramienta poderosa para lograr esta reutilización de una manera muy elegante: los slots.

Imagina que tienes un componente Boton que quieres usar en diferentes partes de tu aplicación. A veces quieres que diga "Haz clic", otras "Enviar", y a veces incluso quieres que contenga un icono junto al texto. Sin slots, tendrías que pasar el texto como una prop y quizás tener una prop booleana para el icono, volviendo el componente menos flexible. Los slots resuelven esto permitiéndote "proyectar" o "inyectar" contenido dinámico dentro de un componente.

Este tutorial te sumergirá en el mundo de los slots de Svelte, desde los conceptos más básicos hasta técnicas avanzadas como los slots con nombre y los slots con props, también conocidos como scoped slots. Al final, tendrás las herramientas para construir componentes verdaderamente flexibles y adaptables a cualquier escenario.

🔥 Importante: Los slots son una característica fundamental que todo desarrollador de Svelte debe dominar para escribir código limpio y componentes verdaderamente reutilizables.

¿Qué Son los Slots y Por Qué Son Importantes? 🤔

En esencia, un slot es un marcador de posición dentro de un componente que indica dónde debe renderizarse el contenido hijo que se le pasa desde el componente padre. Piensa en ello como un "agujero" en tu componente donde puedes insertar cualquier cosa: texto, HTML, otros componentes, etc.

La Necesidad de los Slots 🎯

Considera el siguiente ejemplo sin slots:

<!-- Boton.svelte -->
<script>
  export let text = 'Default Button';
</script>

<button>{text}</button>

<!-- App.svelte -->
<Boton text="Enviar Datos" />
<Boton text="Cancelar" />

Esto funciona bien para texto simple. Pero, ¿y si quieres un icono?

<!-- Boton.svelte -->
<script>
  export let text = 'Default Button';
  export let icon = false;
</script>

<button>
  {#if icon}
    <span></span>
  {/if}
  {text}
</button>

<!-- App.svelte -->
<Boton text="Guardar" icon={true} />

Esto ya empieza a ser un poco más complicado. Si el icono necesita ser personalizado (SVG, diferente tamaño, etc.), o si quieres contenido más complejo dentro del botón (un spinner de carga, por ejemplo), este enfoque se vuelve rápidamente insostenible. Aquí es donde los slots brillan.

Cómo Funcionan los Slots Básicos (Default Slots) 📌

El slot más simple se conoce como default slot (slot predeterminado). Se define en el componente hijo usando la etiqueta <slot />.

Veamos cómo reescribir el componente Boton con un slot:

<!-- Boton.svelte -->
<button>
  <slot>Contenido por defecto</slot>
</button>

<!-- App.svelte -->
<Boton>Enviar</Boton>
<Boton><span>Cargar</span> <img src="spinner.gif" alt="loading" style="width: 16px;"></Boton>
<Boton></Boton> <!-- Mostrará 'Contenido por defecto' -->
💡 Consejo: El texto dentro de `` (`Contenido por defecto`) es el *fallback content* (contenido de respaldo). Si el componente padre no proporciona ningún contenido para el slot, se renderizará el contenido de respaldo. Esto es excelente para proporcionar valores predeterminados o ejemplos.

Con este cambio, nuestro componente Boton es infinitamente más flexible. Podemos pasarle texto, iconos, otros componentes, o cualquier fragmento de HTML directamente entre sus etiquetas de apertura y cierre.

25%

Slots Con Nombre: Organizando Contenido Específico 🏷️

A menudo, un componente puede necesitar múltiples "agujeros" para diferentes tipos de contenido. Por ejemplo, una tarjeta podría tener un espacio para el encabezado, otro para el cuerpo y un tercero para el pie de página. Aquí es donde los slots con nombre entran en juego.

Definiendo Slots Con Nombre en el Componente Hijo 🛠️

Para definir un slot con nombre, simplemente añades el atributo name a la etiqueta <slot />.

<!-- Tarjeta.svelte -->
<div class="card">
  <header>
    <slot name="header">Encabezado por defecto</slot>
  </header>
  <main>
    <slot>Contenido principal por defecto</slot>
  </main>
  <footer>
    <slot name="footer">Pie de página por defecto</slot>
  </footer>
</div>

<style>
  .card {
    border: 1px solid #ccc;
    border-radius: 8px;
    padding: 16px;
    margin: 16px;
    box-shadow: 2px 2px 8px rgba(0,0,0,0.1);
  }
  header, footer {
    padding: 8px 0;
    border-bottom: 1px dashed #eee;
    margin-bottom: 8px;
  }
  footer {
    border-top: 1px dashed #eee;
    border-bottom: none;
    margin-top: 8px;
  }
</style>

En Tarjeta.svelte hemos definido tres slots: header, footer y un default slot (sin nombre). Un componente siempre puede tener un único default slot y múltiples slots con nombre.

Proyectando Contenido a Slots Con Nombre desde el Padre 📝

Para pasar contenido a un slot con nombre desde el componente padre, utilizas una etiqueta HTML (generalmente <div> o <span>, pero puede ser cualquier cosa) con el atributo slot="nombre_del_slot".

<!-- App.svelte -->
<script>
  import Tarjeta from './Tarjeta.svelte';
</script>

<Tarjeta>
  <h2 slot="header">Título de mi Tarjeta de Svelte</h2>
  <p>Este es el contenido principal de la tarjeta. Es muy importante y flexible.</p>
  <div slot="footer">
    <button>Leer más</button>
    <span class="badge blue">Nuevo</span>
  </div>
</Tarjeta>

<Tarjeta>
  <h3 slot="header">Otra Tarjeta</h3>
  <p>Aquí hay un contenido diferente.</p>
</Tarjeta>

<style>
  .badge {
    display: inline-block;
    padding: 0.2em 0.6em;
    font-size: 0.75em;
    font-weight: bold;
    line-height: 1;
    text-align: center;
    white-space: nowrap;
    vertical-align: middle;
    border-radius: 0.25rem;
    color: #fff;
    background-color: #007bff;
    margin-left: 8px;
  }
</style>

Como puedes ver, el contenido para el slot header se envuelve en h2 slot="header", y el contenido para el slot footer se envuelve en div slot="footer". Cualquier contenido que no tenga el atributo slot se considerará parte del default slot.

📌 Nota: Si no proporcionas contenido para un slot con nombre, su contenido de respaldo (si lo tiene) se mostrará. Si un slot con nombre no tiene contenido de respaldo y no se le pasa nada, simplemente no se renderizará nada en su lugar.
50%

Slots Con Props (Scoped Slots): Pasando Datos del Hijo al Padre 🚀

Esta es una de las características más avanzadas y poderosas de los slots en Svelte. Los slots con props (también conocidos como scoped slots en otros frameworks) te permiten pasar datos del componente hijo al componente padre a través del slot. Esto es extremadamente útil cuando el componente hijo es quien posee los datos que el padre necesita para renderizar el contenido del slot.

¿Cuándo Usar Slots Con Props? 💡

Imagina que tienes un componente ListaDeItems.svelte que obtiene una lista de elementos (por ejemplo, usuarios, productos). El componente ListaDeItems es responsable de cargar y mantener esa lista, pero no debería decidir cómo se renderiza cada item individual. Eso es tarea del componente padre.

Sin slots con props, tendrías que pasar la lista completa como una prop al padre, o el padre tendría que obtener los datos por sí mismo, rompiendo el encapsulamiento del componente ListaDeItems. Con slots con props, ListaDeItems puede pasar cada item individual al slot, y el padre decide cómo mostrarlo.

Definiendo Slots Con Props en el Hijo 🛠️

Para pasar datos al slot, simplemente defines variables en la etiqueta <slot /> usando la misma sintaxis que las props:

<!-- ListaDeItems.svelte -->
<script>
  import { onMount } from 'svelte';

  export let items = [];

  onMount(() => {
    // Simula una llamada a API
    setTimeout(() => {
      items = [
        { id: 1, name: 'Alice', age: 30, city: 'Nueva York' },
        { id: 2, name: 'Bob', age: 24, city: 'Londres' },
        { id: 3, name: 'Charlie', age: 35, city: 'París' }
      ];
    }, 1000);
  });
</script>

<div class="item-list">
  {#each items as item (item.id)}
    <div class="item-wrapper">
      <slot item={item} index={item.id - 1}> <!-- Aquí pasamos 'item' y 'index' al slot -->
        <!-- Contenido de respaldo si el padre no proporciona un slot -->
        <p>{item.name} ({item.age})</p>
      </slot>
    </div>
  {/each}
  {#if items.length === 0}
    <p>Cargando items...</p>
  {/if}
</div>

<style>
  .item-list {
    border: 1px solid #ddd;
    padding: 10px;
    border-radius: 5px;
    margin: 16px;
  }
  .item-wrapper {
    margin-bottom: 10px;
    padding: 8px;
    background-color: #f9f9f9;
    border-radius: 4px;
  }
</style>

En ListaDeItems.svelte, dentro del bloque {#each}, estamos pasando cada item y su index al slot. Esto se hace con item={item} y index={item.id - 1}.

Consumiendo Slots Con Props en el Padre ✅

Para acceder a las props pasadas por el slot, utilizas la directiva let: en la etiqueta que envuelve el contenido del slot. Puedes desestructurar las props si lo deseas.

<!-- App.svelte -->
<script>
  import ListaDeItems from './ListaDeItems.svelte';
</script>

<h2>Usuarios de la Aplicación</h2>

<ListaDeItems let:item let:index>
  <div class="user-card">
    <h3>{index + 1}. {item.name}</h3>
    <p>Edad: {item.age}</p>
    <p>Ciudad: {item.city}</p>
  </div>
</ListaDeItems>

<hr>

<h2>Solo Nombres de Usuarios</h2>

<ListaDeItems let:item>
  <p>➡️ {item.name}</p>
</ListaDeItems>

<style>
  .user-card {
    border: 1px solid #4CAF50;
    padding: 10px;
    margin: 5px 0;
    border-radius: 5px;
    background-color: #e8f5e9;
  }
</style>

En App.svelte, en el primer uso de ListaDeItems, usamos let:item y let:index para capturar los datos que el slot está proporcionando. Luego, podemos usar item y index directamente en el cuerpo del slot. En el segundo ejemplo, solo necesitamos item.

⚠️ Advertencia: Un slot con props solo puede ser un *default slot* o un *slot con nombre*. No puedes tener un slot con props y contenido de respaldo *dentro de la misma etiqueta ``* que también esté siendo utilizado por el padre. El contenido de respaldo solo se usa si el padre *no* proporciona ningún contenido para el slot. Si el padre proporciona contenido, incluso si es solo para acceder a las props, el contenido de respaldo se ignora. Sin embargo, en el ejemplo de `ListaDeItems`, el contenido de respaldo (`

{item.name} ({item.age})

`) se renderizará si el padre *no* usa `let:item` para renderizar su propio contenido. Es una sutileza importante.
Hijo (ListaDeItems.svelte) {#each items as item} <slot> item={item} Padre (App.svelte) <ListaDeItems let:item> <div class="user-card"> {item.name} </div> Propagación

Desestructuración de Props en Slots 💥

Si el slot pasa múltiples props, puedes desestructurarlas directamente para mayor comodidad:

<!-- App.svelte (usando desestructuración) -->
<script>
  import ListaDeItems from './ListaDeItems.svelte';
</script>

<ListaDeItems let:{
  item,
  index
}>
  <div class="user-card">
    <h3>{index + 1}. {item.name}</h3>
    <p>Edad: {item.age}</p>
    <p>Ciudad: {item.city}</p>
  </div>
</ListaDeItems>

Esto hace que el código sea más legible, especialmente cuando hay muchas props.

75%

Casos de Uso Avanzados y Patrones Comunes 💡

Los slots son increíblemente versátiles y se pueden usar en muchos escenarios para mejorar la flexibilidad de tus componentes Svelte.

Diseño de Layouts Flexibles 🌐

Los slots con nombre son perfectos para diseñar componentes de layout. Por ejemplo, un layout de dos columnas:

<!-- TwoColumnLayout.svelte -->
<div class="layout">
  <div class="left-column">
    <slot name="left"></slot>
  </div>
  <div class="right-column">
    <slot name="right"></slot>
  </div>
</div>

<style>
  .layout {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 20px;
    padding: 20px;
    border: 1px solid #eee;
  }
</style>

<!-- App.svelte -->
<script>
  import TwoColumnLayout from './TwoColumnLayout.svelte';
</script>

<TwoColumnLayout>
  <h2 slot="left">Columna Izquierda</h2>
  <p slot="left">Contenido para la sección izquierda del layout.</p>
  <ul slot="left">
    <li>Item 1</li>
    <li>Item 2</li>
  </ul>

  <h2 slot="right">Columna Derecha</h2>
  <p slot="right">Aquí va otro tipo de contenido.</p>
  <button slot="right">Botón de Acción</button>
</TwoColumnLayout>

Componentes de Formulario Abiertos 📝

Considera un componente FormWrapper que gestiona el estado de envío y validación, pero permite al padre definir los campos exactos del formulario:

<!-- FormWrapper.svelte -->
<script>
  import { createEventDispatcher } from 'svelte';

  const dispatch = createEventDispatcher();
  let submitting = false;

  async function handleSubmit() {
    submitting = true;
    // Simula validación o envío de API
    await new Promise(resolve => setTimeout(resolve, 1500));
    dispatch('submit');
    submitting = false;
  }
</script>

<form on:submit|preventDefault={handleSubmit}>
  <slot loading={submitting}></slot>
  <button type="submit" disabled={submitting}>
    {#if submitting}Enviando...{:else}Enviar{/if}
  </button>
</form>
<!-- App.svelte -->
<script>
  import FormWrapper from './FormWrapper.svelte';

  let username = '';
  let email = '';

  function onSubmit() {
    alert(`Formulario enviado! Usuario: ${username}, Email: ${email}`);
    username = '';
    email = '';
  }
</script>

<FormWrapper on:submit={onSubmit} let:loading>
  <div>
    <label>Usuario:</label>
    <input type="text" bind:value={username} disabled={loading}>
  </div>
  <div>
    <label>Email:</label>
    <input type="email" bind:value={email} disabled={loading}>
  </div>
</FormWrapper>

<style>
  form div {
    margin-bottom: 10px;
  }
  label {
    display: block;
    margin-bottom: 5px;
  }
</style>

Aquí, FormWrapper usa un slot con props (loading) para informar al padre sobre el estado de envío, permitiendo al padre deshabilitar los campos de entrada si es necesario. Esto es muy poderoso para crear componentes de lógica sin acoplamiento visual.

Componentes de Tabla Personalizables 📊

Un componente de tabla genérico que permite personalizar las celdas es un excelente caso de uso para slots con props.

<!-- Table.svelte -->
<script>
  export let data = [];
  export let columns = []; // Ej: [{ key: 'name', label: 'Nombre' }, { key: 'age', label: 'Edad' }]
</script>

<table>
  <thead>
    <tr>
      {#each columns as col}
        <th>{col.label}</th>
      {/each}
    </tr>
  </thead>
  <tbody>
    {#each data as row (row.id || JSON.stringify(row))}
      <tr>
        {#each columns as col}
          <td>
            <slot name="cell" {row} {col} value={row[col.key]}>
              {row[col.key]}
            </slot>
          </td>
        {/each}
      </tr>
    {/each}
  </tbody>
</table>

<style>
  table {
    width: 100%;
    border-collapse: collapse;
    margin: 20px 0;
  }
  th, td {
    border: 1px solid #ddd;
    padding: 8px;
    text-align: left;
  }
  th {
    background-color: #f2f2f2;
  }
</style>
<!-- App.svelte -->
<script>
  import Table from './Table.svelte';

  const users = [
    { id: 1, name: 'Alice', age: 30, status: 'Activo' },
    { id: 2, name: 'Bob', age: 24, status: 'Inactivo' },
    { id: 3, name: 'Charlie', age: 35, status: 'Activo' }
  ];

  const userColumns = [
    { key: 'name', label: 'Nombre Completo' },
    { key: 'age', label: 'Edad' },
    { key: 'status', label: 'Estado' }
  ];
</script>

<Table data={users} columns={userColumns}>
  <svelte:fragment slot="cell" let:row let:col let:value>
    {#if col.key === 'status'}
      <span class="badge {value === 'Activo' ? 'green' : 'red'}">{value}</span>
    {:else if col.key === 'name'}
      <strong>{value.toUpperCase()}</strong>
    {:else}
      {value}
    {/if}
  </svelte:fragment>
</Table>

<style>
  .badge.green { background-color: #28a745; }
  .badge.red { background-color: #dc3545; }
</style>

En este ejemplo, usamos un default slot (implícito, pero con name="cell") y svelte:fragment para personalizar el renderizado de cada celda. El componente Table pasa row, col, y value a este slot, permitiendo que el componente padre decida cómo mostrar cada pieza de información. Si no se usa el slot, se muestra el value por defecto.

¿Qué es ``?

<svelte:fragment> es una etiqueta especial de Svelte que te permite agrupar contenido para un slot sin añadir un elemento HTML adicional al DOM. Es útil cuando quieres aplicar un slot con nombre o un slot con props a un grupo de elementos, pero no quieres que un <div> o <span> extra sea el contenedor de ese grupo. Simplemente actúa como un marcador para el slot.

100%

Mejores Prácticas y Consideraciones ✨

Al trabajar con slots en Svelte, ten en cuenta las siguientes mejores prácticas:

  • Claridad sobre la propiedad de los datos: Decide si el componente hijo o el padre es el dueño de los datos. Si el hijo tiene los datos y el padre necesita formatearlos, usa slots con props. Si el padre tiene los datos y solo necesita insertarlos en el hijo, usa slots simples.
  • Nombres descriptivos para slots: Cuando uses slots con nombre, elige nombres que sean claros y describan el propósito del slot (ej., header, footer, actions, sidebar).
  • Contenido de respaldo: Siempre que sea posible, proporciona contenido de respaldo significativo para los slots. Esto mejora la robustez de tu componente y facilita su uso, ya que proporciona un comportamiento por defecto si el consumidor no especifica nada.
  • Evita el exceso de acoplamiento: Aunque los slots aumentan la flexibilidad, un abuso de slots con props que expongan demasiada lógica interna del componente hijo puede llevar a un acoplamiento excesivo. Encuentra un equilibrio.
  • Documenta tus slots: Si estás creando una biblioteca de componentes o trabajando en un equipo, documenta claramente qué slots ofrece tu componente, qué props pasan (si es un slot con props) y qué tipo de contenido se espera.
💡 Consejo: Piensa en los slots como una forma de inversión de control para la renderización. El componente padre tiene el control sobre cómo se renderizan ciertas partes del componente hijo, mientras que el componente hijo se encarga de la lógica y la estructura general.

Conclusión 🎉

Los slots son una característica esencial y potente en Svelte para construir componentes verdaderamente reutilizables, flexibles y adaptables. Ya sea que necesites inyectar contenido simple, organizar secciones específicas con slots con nombre o pasar datos del componente hijo al padre con slots con props, Svelte te proporciona las herramientas para lograrlo de manera elegante y eficiente.

Al dominar los slots, no solo mejorarás la modularidad y el mantenimiento de tu código, sino que también podrás crear experiencias de desarrollo más agradables para ti y para cualquier persona que utilice tus componentes. ¡Ahora sal y crea componentes Svelte increíbles con la magia de los slots!

Tutoriales relacionados

Comentarios (0)

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