Slots
Esta página supone que ya has leído los Fundamentos de los Componentes. Léelo primero si eres nuevo en el tema de componentes.
Contenido y Salida del Slot
Ya aprendimos que los componentes pueden aceptar props, que pueden ser valores de JavaScript de cualquier tipo. Pero, ¿qué pasa con el contenido de las plantillas? Es posible que en algunos casos queramos pasar un fragmento de plantilla a un componente hijo, y dejar que el componente hijo renderice el fragmento dentro de su propia plantilla.
Por ejemplo, podemos tener un componente <FancyButton>
que admite un uso como éste:
template
<FancyButton>
¡Hazme clic! <!-- contenido del slot -->
</FancyButton>
La plantilla de <FancyButton>
tiene el siguiente aspecto:
template
<button class="fancy-btn">
<slot></slot> <!-- salida del slot -->
</button>
El elemento <slot>
es una salida de slot que indica dónde se debe renderizar el contenido del slot proporcionado por el padre.
Y el renderizado final del DOM:
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 interior es proporcionado por el componente padre.
Otra forma de entender los slots es compararlos con las funciones de JavaScript:
js
// el componente padre que pasa el contenido del slot
FancyButton('¡Hazme clic!')
// FancyButton renderiza el contenido del slot en su propia plantilla
function FancyButton(slotContent) {
return `<button class="fancy-btn">
${slotContent}
</button>`
}
El contenido del slot no se limita a texto. Puede ser cualquier contenido válido de una plantilla. 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 utilizarlo en diferentes lugares con diferentes contenidos internos, pero todos con el mismo diseño elegante.
El mecanismo de slots de los componentes de Vue está inspirado en el elemento nativo de Componentes Web <slot>
, 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 {{ mensaje }}
renderizarán el mismo contenido.
El contenido del slot no tiene acceso a los datos del componente hijo. Las expresiones en las plantillas de Vue sólo pueden acceder al ámbito en el que se define, de forma consistente con el léxico de ámbito de JavaScript. En otras palabras:
Las expresiones en la plantilla padre sólo tienen acceso al ámbito padre; las expresiones en la plantilla hijo sólo tienen acceso al ámbito hijo.
Contenido Alternativo
Hay casos en los que es útil especificar el contenido por defecto de un slot, que se mostrará sólo cuando no se proporcione ningún contenido. Por ejemplo, en un componente <SubmitButton>
:
template
<button type="submit">
<slot></slot>
</button>
Podemos querer que el texto "Submit" sea renderizado dentro del <button>
si el padre no proporciona ningún contenido del slot. Para hacer que "Submit" sea el contenido alternativo, podemos colocarlo entre las etiquetas <slot>
:
template
<button type="submit">
<slot>
Enviar <!-- contenido alternativo -->
</slot>
</button>
Ahora cuando usamos <SubmitButton>
en un componente padre, no proporcionando contenido para el slot:
template
<SubmitButton />
Esto renderizará el contenido alternativo, "Enviar":
html
<button type="submit">Enviar</button>
Pero si proporcionamos contenido:
template
<SubmitButton>Guardar</SubmitButton>
En ese caso, el contenido proporcionado se mostrará en su lugar:
html
<button type="submit">Guardar</button>
Slots Asignados
Hay ocasiones en las que es útil tener varias salidas de slots en un mismo componente. Por ejemplo, en un componente <BaseLayout>
con la siguiente plantilla:
template
<div class="container">
<header>
<!-- Queremos el contenido del header aquí -->
</header>
<main>
<!-- Queremos el contenido del main aquí -->
</main>
<footer>
<!-- Queremos el contenido del footer aquí -->
</footer>
</div>
Para estos casos, el elemento <slot>
tiene un atributo especial, name
, que se puede utilizar para asignar un ID único a los diferentes slots y así poder determinar dónde se debe mostrar el contenido:
template
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
Una salida de <slot>
sin nombre
tiene implícitamente el nombre "default".
En un componente padre que utilice <BaseLayout>
, necesitamos una forma de pasar múltiples fragmentos de contenido del slot, cada uno dirigido a una salida de slot diferente. Aquí es donde entran en juego los slots asignados.
Para pasar un slot asignado, 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 del slot header -->
</template>
</BaseLayout>
v-slot
tiene una abreviatura dedicada #
, por lo que <plantilla v-slot:header>
puede acortarse a sólo <plantilla #header>
. Piensa en ello como "renderizar este fragmento de plantilla en el slot 'header' del componente hijo".
Este es el código que pasa el contenido de los tres slots a <BaseLayout>
utilizando la sintaxis abreviada:
template
<BaseLayout>
<template #header>
<h1>Aquí puede haber un título de 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í hay información de contacto</p>
</template>
</BaseLayout>
Cuando un componente acepta tanto un slot por defecto como slots asignados, todos los nodos de nivel superior que no sean <template>
son tratados implícitamente como contenido del slot default. Así que lo anterior también puede escribirse como:
template
<BaseLayout>
<template #header>
<h1>Aquí puede haber un título de página</h1>
</template>
<!-- slot default implícito -->
<p>Un párrafo para el contenido principal.</p>
<p>Y otro más.</p>
<template #footer>
<p>Aquí hay 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 final renderizado será:
html
<div class="container">
<header>
<h1>Aquí puede haber un título de página</h1>
</header>
<main>
<p>Un párrafo para el contenido principal.</p>
<p>Y otro más.</p>
</main>
<footer>
<p>Aquí hay información de contacto</p>
</footer>
</div>
De nuevo, puede ayudarte a entender mejor los slots asignados utilizar la analogía de las funciones de JavaScript:
js
// pasar varios fragmentos de slots con diferentes nombres
BaseLayout({
header: `...`,
default: `...`,
footer: `...`
})
// <BaseLayout> los renderiza en diferentes lugares
function BaseLayout(slots) {
return `<div class="container">
<header>${slots.header}</header>
<main>${slots.default}</main>
<footer>${slots.footer}</footer>
</div>`
}
Nombres de Slots Dinámicos
Los argumentos de la directiva dinámica también funcionan con v-slot
, permitiendo la definición de nombres de slots dinámicos:
template
<base-layout>
<template v-slot:[dynamicSlotName]>
...
</template>
<!-- Con abreviatura -->
<template #[dynamicSlotName]>
...
</template>
</base-layout>
Ten en cuenta que la expresión está sujeta a las restricciones sintácticas de los argumentos de la directiva dinámica.
Slots con Ámbito
Como se discutió en Ámbito de Renderizado, el contenido de los slots no tiene acceso al estado del componente hijo.
Pero hay casos en los que puede ser útil que el contenido de un slot pueda hacer uso de datos tanto del ámbito padre como del ámbito hijo. Para conseguirlo, necesitamos una forma de que el hijo pase datos a un slot cuando lo renderice.
En efecto, podemos hacer exactamente eso: podemos pasar atributos a una salida de slots igual que pasamos props a un componente:
template
<!-- plantilla <MyComponent> -->
<div>
<slot :text="greetingMessage" :count="1"></slot>
</div>
La recepción de los props de los slots es un poco diferente cuando se utiliza un único slot por defecto frente a la utilización de slots asignados. Mostraremos primero cómo recibir props usando un slot default único, usando v-slot
directamente en la etiqueta del componente hijo:
template
<MyComponent v-slot="slotProps">
{{ slotProps.text }} {{ slotProps.count }}
</MyComponent>
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 de ámbito como una función que se pasa al componente hijo. El componente hijo la llama, pasando los accesorios como argumentos:
js
MyComponent({
// pasando el slot default, pero como una función
default: (slotProps) => {
return `${slotProps.text} ${slotProps.count}`
}
})
function MyComponent(slots) {
const greetingMessage = 'hello'
return `<div>${
// ¡llama a la función slot con props!
slots.default({ text: greetingMessage, count: 1 })
}</div>`
}
De hecho, esto es muy parecido a cómo se compilan los slots de ámbito, y cómo se utilizarían los slots de ámbito en las Funciones de Renderizado manuales.
Observa cómo v-slot="slotProps"
coincide con la firma de la función slot. Al igual que con los argumentos de las funciones, podemos utilizar la desestructuración en v-slot
:
template
<MyComponent v-slot="{ text, count }">
{{ text }} {{ count }}
</MyComponent>
Slots de Ámbito Asignado
Los slots de ámbito asignado funcionan de forma similar; las props de los slots son accesibles como el valor de la directiva v-slot
: v-slot:name="slotProps"
. Cuando se utiliza 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 asignado:
template
<slot name="header" message="hola"></slot>
Ten en cuenta que el nombre
de un slot no se incluirá en las props porque está reservado; así que el headerProps
resultante sería { message: 'hola' }
.
Ejemplo de Lista Elegante
Te estarás preguntando cuál sería un buen caso de uso para los slots de ámbito. Aquí tienes un ejemplo: imagina un componente <FancyList>
que muestra una lista de elementos; puede encapsular la lógica para cargar datos remotos, utilizar los datos para mostrar una lista, o incluso características avanzadas como la paginación o el desplazamiento infinito. Sin embargo, queremos que sea flexible con el aspecto de cada elemento y dejar el estilo de cada elemento al componente padre que lo consume. Así que el uso deseado puede ser así:
template
<FancyList :api-url="url" :per-page="10">
<template #item="{ body, username, likes }">
<div class="item">
<p>{{ body }}</p>
<p>por {{ username }} | {{ likes }} me gusta</p>
</div>
</template>
</FancyList>
Dentro de <FancyList>
, podemos renderizar el mismo <slot>
varias veces con diferentes datos de los ítems (fíjate que estamos usando v-bind
para pasar un objeto como props del slot):
template
<ul>
<li v-for="item in items">
<slot name="item" v-bind="item"></slot>
</li>
</ul>
Componentes sin Renderizado
El caso de uso de <FancyList>
que comentamos anteriormente incluye tanto la lógica reutilizable (obtención de datos, paginación, etc.) como la salida visual, mientras que delega parte de la salida visual al componente consumidor a través de slots de ámbito.
Si llevamos este concepto un poco más allá, podemos crear componentes que sólo encapsulen la lógica y no rendericen nada por sí mismos; la salida visual se delega completamente en el componente consumidor con slots de ámbito. Llamamos a este tipo de componentes un Componente sin Renderizado.
Un ejemplo de componente sin renderizado podría ser uno que encapsule la lógica del seguimiento de la posición actual del ratón:
template
<MouseTracker v-slot="{ x, y }">
Mouse is at: {{ x }}, {{ y }}
</MouseTracker>
Aunque se trata de un patrón interesante, la mayor parte de lo que se puede conseguir con los componentes sin renderizado se puede lograr de una manera más eficiente con la Composition API, sin incurrir en la sobrecarga de la anidación de componentes adicionales. Más adelante, veremos cómo podemos implementar la misma funcionalidad de seguimiento del ratón como un [Composable] (/guide/reusability/composables).
Dicho esto, los slots de ámbito siguen siendo útiles en los casos en los que necesitamos encapsular la lógica y componer la salida visual, como en el ejemplo de <FancyList>
.