Saltar al contenido

Slots

Esta página asume que ya has leído los Fundamentos de Componentes. Léela primero si eres nuevo en los componentes.

Contenido y Salida de Slot

Hemos aprendido que los componentes pueden aceptar props, que pueden ser valores JavaScript de cualquier tipo. Pero, ¿qué pasa con el contenido de el template? En algunos casos, es posible que queramos pasar un fragmento de template a un componente hijo, y dejar que el componente hijo renderice el fragmento dentro de su propia template.

Por ejemplo, podríamos tener un componente <FancyButton> que admita un uso como este:

template
<FancyButton>
  ¡Hazme Clic! <!-- contenido del slot -->
</FancyButton>

El template de <FancyButton> se ve así:

template
<button class="fancy-btn">
  <slot></slot> <!-- slot de salida -->
</button>

El elemento <slot> es una salida de slot que indica dónde debe renderizarse el contenido del slot proporcionado por el padre.

slot diagram

Y el DOM renderizado final:

html
<button class="fancy-btn">¡Hazme Clic!</button>

Con los slots, el <FancyButton> es responsable de renderizar el <button> exterior (y su estilo sofisticado), mientras que el contenido interno es proporcionado por el componente padre.

Otra forma de entender los slots es comparándolos con las funciones de JavaScript:

js
// el componente padre pasa el contenido del slot
FancyButton('¡Hazme Clic!')

// FancyButton renderiza el contenido del slot en su propio template
function FancyButton(slotContent) {
  return `<button class="fancy-btn">
      ${slotContent}
    </button>`
}

El contenido del slot no se limita solo a texto. Puede ser cualquier contenido de template válido. Por ejemplo, podemos pasar múltiples elementos, o incluso otros componentes:

template
<FancyButton>
  <span style="color:red">¡Hazme Clic!</span>
  <AwesomeIcon name="plus" />
</FancyButton>

Al usar slots, nuestro <FancyButton> es más flexible y reutilizable. Ahora podemos usarlo en diferentes lugares con diferente contenido interno, pero todos con el mismo estilo sofisticado.

El mecanismo de slots de los componentes Vue está inspirado en el elemento <slot> nativo de Web Component, pero con capacidades adicionales que veremos más adelante.

Ámbito de Renderizado

El contenido del slot tiene acceso al ámbito de datos del componente padre, porque está definido en el padre. Por ejemplo:

template
<span>{{ message }}</span>
<FancyButton>{{ message }}</FancyButton>

Aquí ambas interpolaciones {{ message }} renderizarán el mismo contenido.

El contenido del slot no tiene acceso a los datos del componente hijo. Las expresiones en los templates de Vue solo pueden acceder al ámbito en el que están definidas, lo que es consistente con el alcance léxico de JavaScript. En otras palabras:

Las expresiones en el template padre solo tienen acceso al ámbito padre; las expresiones en el template hijo solo tienen acceso al ámbito hijo.

Contenido Alternativo

Hay casos en los que es útil especificar contenido de reserva (es decir, por defecto) para un slot, que se renderizará solo cuando no se proporcione ningún contenido. Por ejemplo, en un componente <SubmitButton>:

template
<button type="submit">
  <slot></slot>
</button>

Podríamos querer que el texto "Enviar" se renderice dentro del <button> si el padre no proporcionó ningún contenido de slot. Para hacer que "Enviar" sea el contenido de reserva, podemos colocarlo entre las etiquetas <slot>:

template
<button type="submit">
  <slot>
    Enviar <!-- contenido alternativo -->
  </slot>
</button>

Ahora, cuando usamos <SubmitButton> en un componente padre, sin proporcionar contenido para el slot:

template
<SubmitButton />

Esto renderizará el contenido de reserva, "Enviar":

html
<button type="submit">Enviar</button>

Pero si proporcionamos contenido:

template
<SubmitButton>Guardar</SubmitButton>

Entonces el contenido proporcionado será renderizado en su lugar:

html
<button type="submit">Guardar</button>

Slots con Nombre

Hay ocasiones en las que es útil tener múltiples salidas de slot en un solo componente. Por ejemplo, en un componente <BaseLayout> con el siguiente template:

template
<div class="container">
  <header>
    <!-- Queremos el contenido del header aquí -->
  </header>
  <main>
    <!-- Queremos el contenido principal aquí -->
  </main>
  <footer>
    <!-- Queremos el contenido del footer aquí -->
  </footer>
</div>

Para estos casos, el elemento <slot> tiene un atributo especial, name, que puede usarse para asignar un ID único a diferentes slots para que puedas determinar dónde debe renderizarse el contenido:

template
<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

Una <slot> de salida sin name tiene implícitamente el nombre "default".

En un componente padre que utiliza <BaseLayout>, necesitamos una forma de pasar múltiples fragmentos de contenido de slot, cada uno dirigido a una salida de slot diferente. Aquí es donde entran los slots nombrados.

Para pasar un slot nombrado, necesitamos usar un elemento <template> con la directiva v-slot, y luego pasar el nombre del slot como argumento a v-slot:

template
<BaseLayout>
  <template v-slot:header>
    <!-- contenido para el slot header -->
  </template>
</BaseLayout>

v-slot tiene una abreviatura dedicada #, por lo que <template v-slot:header> puede acortarse a simplemente <template #header>. Piensa en ello como "renderizar este fragmento de template en el slot 'header' del componente hijo".

named slots diagram

Aquí está el código que pasa contenido para los tres slots a <BaseLayout> usando la sintaxis abreviada:

template
<BaseLayout>
  <template #header>
    <h1>Aquí podría estar el título de la página</h1>
  </template>

  <template #default>
    <p>Un párrafo para el contenido principal.</p>
    <p>Y otro más.</p>
  </template>

  <template #footer>
    <p>Aquí está la información de contacto</p>
  </template>
</BaseLayout>

Cuando un componente acepta tanto un slot por defecto como slots nombrados, todos los nodos de nivel superior que no son <template> se tratan implícitamente como contenido para el slot por defecto. Así que lo anterior también se puede escribir como:

template
<BaseLayout>
  <template #header>
    <h1>Aquí podría estar el título de la página</h1>
  </template>

  <!-- slot implícito por defecto -->
  <p>Un párrafo para el contenido principal.</p>
  <p>Y otro más.</p>

  <template #footer>
    <p>Aquí está la información de contacto</p>
  </template>
</BaseLayout>

Ahora, todo lo que esté dentro de los elementos <template> se pasará a los slots correspondientes. El HTML renderizado final será:

html
<div class="container">
  <header>
    <h1>Aquí podría estar el título de la página</h1>
  </header>
  <main>
    <p>Un párrafo para el contenido principal.</p>
    <p>Y otro más.</p>
  </main>
  <footer>
    <p>Aquí está la información de contacto</p>
  </footer>
</div>

De nuevo, puede que te ayude a entender mejor los slots nombrados usando la analogía de la función de JavaScript:

js
// passing multiple slot fragments with different names
BaseLayout({
  header: `...`,
  default: `...`,
  footer: `...`
})

// <BaseLayout> renders them in different places
function BaseLayout(slots) {
  return `<div class="container">
      <header>${slots.header}</header>
      <main>${slots.default}</main>
      <footer>${slots.footer}</footer>
    </div>`
}

Slots Condicionales

A veces, quieres renderizar algo basándote en si se ha pasado contenido a un slot o no.

Puedes usar la propiedad $slots en combinación con un v-if para lograr esto.

En el siguiente ejemplo, definimos un componente Card con tres slots condicionales: header, footer y el default. Cuando el contenido para el header / footer / default está presente, queremos envolverlo para proporcionar un estilo adicional:

template
<template>
  <div class="card">
    <div v-if="$slots.header" class="card-header">
      <slot name="header" />
    </div>

    <div v-if="$slots.default" class="card-content">
      <slot />
    </div>

    <div v-if="$slots.footer" class="card-footer">
      <slot name="footer" />
    </div>
  </div>
</template>

Pruébalo en el Playground

Nombres de Slot Dinámicos

Los argumentos de directiva dinámicos también funcionan en v-slot, permitiendo la definición de nombres de slot dinámicos:

template
<base-layout>
  <template v-slot:[dynamicSlotName]>
    ...
  </template>

  <!-- con abreviación -->
  <template #[dynamicSlotName]>
    ...
  </template>
</base-layout>

Ten en cuenta que la expresión está sujeta a las restricciones de sintaxis de los argumentos de directiva dinámicos.

Slots con Ámbito

Como se discutió en Ámbito de Renderizado, el contenido del slot no tiene acceso al estado del componente hijo.

Sin embargo, hay casos en los que podría ser útil si el contenido de un slot puede utilizar datos tanto del ámbito padre como del ámbito hijo. Para lograr esto, necesitamos una forma de que el hijo pase datos a un slot al renderizarlo.

De hecho, podemos hacer exactamente eso: podemos pasar atributos a una salida de slot como si pasaras props a un componente:

template
<!-- template de <MyComponent> -->
<div>
  <slot :text="greetingMessage" :count="1"></slot>
</div>

Recibir las slot props es un poco diferente cuando se usa un solo slot por defecto en comparación con el uso de slots nombrados. Primero mostraremos cómo recibir props usando un solo slot por defecto, usando v-slot directamente en la etiqueta del componente hijo:

template
<MyComponent v-slot="slotProps">
  {{ slotProps.text }} {{ slotProps.count }}
</MyComponent>

scoped slots diagram

Las props pasadas al slot por el hijo están disponibles como el valor de la directiva v-slot correspondiente, a la que se puede acceder mediante expresiones dentro del slot.

Puedes pensar en un slot con ámbito como una función que se pasa al componente hijo. El componente hijo la llama, pasando props como argumentos:

js
MyComponent({
  // pasando el slot por defecto, pero como una función
  default: (slotProps) => {
    return `${slotProps.text} ${slotProps.count}`
  }
})

function MyComponent(slots) {
  const greetingMessage = 'hola'
  return `<div>${
    // ¡llama a la función del slot con props!
    slots.default({ text: greetingMessage, count: 1 })
  }</div>`
}

De hecho, esto es muy similar a cómo se compilan los slots con ámbito, y cómo usarías los slots con ámbito en render functions manuales.

Observa cómo v-slot="slotProps" coincide con la firma de la función del slot. Al igual que con los argumentos de función, podemos usar la desestructuración en v-slot:

template
<MyComponent v-slot="{ text, count }">
  {{ text }} {{ count }}
</MyComponent>

Slots Nombrados con Ámbito

Los slots nombrados con ámbito funcionan de manera similar: las slot props son accesibles como el valor de la directiva v-slot: v-slot:name="slotProps". Cuando se usa la abreviatura, se ve así:

template
<MyComponent>
  <template #header="headerProps">
    {{ headerProps }}
  </template>

  <template #default="defaultProps">
    {{ defaultProps }}
  </template>

  <template #footer="footerProps">
    {{ footerProps }}
  </template>
</MyComponent>

Pasando props a un slot nombrado:

template
<slot name="header" message="hola"></slot>

Ten en cuenta que el name de un slot no se incluirá en las props porque está reservado, por lo que las headerProps resultantes serían { message: 'hola' }.

Si estás mezclando slots nombrados con el slot por defecto con ámbito, necesitas usar una etiqueta <template> explícita para el slot por defecto. Intentar colocar la directiva v-slot directamente en el componente resultará en un error de compilación. Esto es para evitar cualquier ambigüedad sobre el ámbito de las props del slot por defecto. Por ejemplo:

template
<!-- template de <MyComponent> -->
<div>
  <slot :message="hola"></slot>
  <slot name="footer" />
</div>
template
<!-- Este template no compilará -->
<MyComponent v-slot="{ message }">
  <p>{{ message }}</p>
  <template #footer>
    <!-- el mensaje pertenece al slot por defecto y no está disponible aquí -->
    <p>{{ message }}</p>
  </template>
</MyComponent>

Usar una etiqueta <template> explícita para el slot por defecto ayuda a dejar claro que la prop message no está disponible dentro del otro slot:

template
<MyComponent>
  <!-- Usa el slot explícito por defecto -->
  <template #default="{ message }">
    <p>{{ message }}</p>
  </template>

  <template #footer>
    <p>Aquí tienes alguna información de contacto</p>
  </template>
</MyComponent>

Ejemplo de Lista Sofisticada

Puede que te estés preguntando cuál sería un buen caso de uso para los slots con ámbito. Aquí tienes un ejemplo: imagina un componente <FancyList> que renderiza una lista de ítems; podría encapsular la lógica para cargar datos remotos, usar esos datos para mostrar una lista, o incluso características avanzadas como paginación o desplazamiento infinito. Sin embargo, queremos que sea flexible en cuanto a la apariencia de cada ítem y dejar el estilo de cada ítem al componente padre que lo consume. Así, el uso deseado podría verse así:

template
<FancyList :api-url="url" :per-page="10">
  <template #item="{ body, username, likes }">
    <div class="item">
      <p>{{ body }}</p>
      <p>por {{ username }} | {{ likes }} likes</p>
    </div>
  </template>
</FancyList>

Dentro de <FancyList>, podemos renderizar el mismo <slot> varias veces con diferentes datos de ítem (observa que estamos usando v-bind para pasar un objeto como slot props):

template
<ul>
  <li v-for="item in items">
    <slot name="item" v-bind="item"></slot>
  </li>
</ul>

Componentes Sin Renderización

El caso de uso de <FancyList> que discutimos anteriormente encapsula tanto la lógica reutilizable (obtención de datos, paginación, etc.) como la salida visual, mientras delega parte de la salida visual al componente consumidor a través de slots con ámbito.

Si llevamos este concepto un poco más allá, podemos crear componentes que solo encapsulan lógica y no renderizan nada por sí mismos; la salida visual se delega completamente al componente consumidor con slots con ámbito. A este tipo de componente lo llamamos Componentes Sin Renderización.

Un ejemplo de componentes sin renderización podría ser uno que encapsule la lógica de seguimiento de la posición actual del ratón:

template
<MouseTracker v-slot="{ x, y }">
  El ratón está en: {{ x }}, {{ y }}
</MouseTracker>

Si bien es un patrón interesante, la mayor parte de lo que se puede lograr con los Componentes Renderless se puede conseguir de una manera más eficiente con la Composition API, sin incurrir en la sobrecarga de anidamiento de componentes adicional. Más adelante, veremos cómo podemos implementar la misma funcionalidad de seguimiento del ratón como un Composable.

Dicho esto, los slots con ámbito siguen siendo útiles en casos en los que necesitamos tanto encapsular la lógica como componer la salida visual, como en el ejemplo de <FancyList>.

Slots