Skip to content

Componentes con Solts

Los slots son una de las características de lenguaje más poderosas en Vue. Con la capacidad de definir contenido alternativo, slots con nombre y slots con alcance. Permiten que los componentes principales inyecten su marcado, estilos y comportamiento.

Al igual que las props y los events, los slots son parte de la API pública del componente.

Los componentes comunes, como los Buttons y los Inputs, a menudo usan slots como "prefijo" y "sufijo" para permitirle definir la ubicación de los íconos y usar SVG o componentes de íconos completos.

Los componentes de diseño a nivel de página, como el Sidebar o el Footer, también suelen utilizar slots.

Por último, los componentes sin representación, como un componente Loading o un componente ApolloQuery, hacen un uso intensivo de los slots para definir qué renderizar en varios estados como: error, loading, y success.

El Stot Más Simple

Mostraremos cómo probar un Modal que usa un <slot/> predeterminado. Al igual que en las secciones anteriores, comenzaremos de manera simple.

📃Modal.vue

vue
<script setup>
import { ref } from 'vue'

const show = ref(true)
</script>

<template>
  <div class="overlay">
    <div class="modal" v-if="show">
      <button @click="show = !show">Close</button>
      <slot  />
    </div>
  </div>
</template>

<style scoped>
.overlay {
  position: fixed;
  display: flex;
  padding-top: 120px;
  justify-content: center;
  background: rgba(100, 100, 100, 30%);
  left: 0;
  right: 0;
  top: 0;
  bottom: 0;
}

.modal {
  position: absolute;
  min-height: 350px;
  min-width: 400px;
  background: white;
  color: black;
}
</style>
Modal's body content (passed in via slot)

📃Modal.cy.js

js
import Modal from '../Modal.vue'

const modalSelector = '.overlay'
const closeButtonSelector = 'button'

describe('<Modal>', () => { 
  it('renders the modal content', () => {
    cy.mount(Modal, { slots: { default: () => 'Content' } })
      .get(modalSelector)
      .should('have.contain', 'Content')
  })

  it('can be closed', () => {
    cy.mount(Modal, { slots: { default: () => 'Content' } })
      .get(modalSelector)
      .should('have.contain', 'Content')
      .get(closeButtonSelector)
      .should('have.contain', 'Close')
      .click()
      // Repeat the assertion to make sure the text
      // is no longer visible
      .get(modalSelector)      
      .should('not.have.contain', 'Content')
  })
})

INFO

Si desea consultar el mismo ejemplo de esta prueba con JSX o del mismo componente con la Options API, puede buscar en la Documentación Oficial de Cypress.

Slots Nombrados

Las ranuras con nombre en Vue le dan al componente principal la capacidad de inyectar diferentes marcas y lógica del padre en los componentes del contenedor.

En el caso de nuestro modal, el modal podría definir un encabezado, un pie de página y un cuerpo denominado slot.

Todo esto es parte de la API del componente y ejercerlo a fondo es responsabilidad de la prueba.

Header Content

Modal's body content (passed in via slot)

📃Modal.vue

vue
<script setup>
  // omitted for brevity 
</script>

<template>  
  <div class="overlay">
    <div class="modal" v-if="show">
      <div class="header">
        <slot name="header" />
      </div>
      <hr/>
      <div class="content">
        <slot/>
      </div>
      <hr/>
      <div class="footer">
        <slot name="footer">
          <button @click="show = !show">Close</button>
        </slot>
      </div>
    </div>
  </div>
</template>

<style scoped>
  /* omitted for brevity */
</style>

📃Modal.cy.js

js
import Modal from '../Modal.vue'

const modalSelector = '.overlay'
const closeButtonSelector = 'button'
  
const footerText = 'My Custom Footer'
const headerText = 'My Custom Header'

const slots = {
  default: () => 'Content',
  footer: () => footerText,
  header: () => headerText
}
  
describe('<Modal>', () => {
  it('renders the default modal content', () => {
    cy.mount(Modal, { slots })
      .get(modalSelector).should('have.contain', 'Content')
  })

  it('renders a custom footer', () => {
    const footerText = 'My Custom Footer'  
    cy.mount(Modal, { slots })
      .get(modalSelector).should('have.contain', 'Content')
      .and('have.contain', footerText)
  })

  it('renders a custom header', () => {
    const headerText = 'My Custom Header'
    cy.mount(Modal, { slots })
      .get(modalSelector).should('have.contain', 'Content')
      .and('have.contain', headerText)
  })

  it('renders the fallback "Close" button when no footer is provided', () => {
    cy.mount(Modal, {
      slots: {
        default: () => 'Content',
        header: () => headerText
      }
    })
      .get(modalSelector).should('have.contain', 'Content')
      .get(closeButtonSelector)
      .should('have.contain', 'Close').click()
      // Repeat the assertion to make sure the text
      // is no longer visible
      .get(modalSelector).should('not.have.contain', 'Content')
  })
})

INFO

Si desea consultar el mismo ejemplo de esta prueba con JSX o del mismo componente con la Options API, puede buscar en la Documentación Oficial de Cypress.

Alcance del Slot

Ahora, ¿qué pasa si queremos permitir que el padre controle cuándo cerrar el modal? Podemos proporcionar una propiedad de slot, una función llamada cerca de cualquiera de los slots que queramos.

La implementación de nuestro modal cambiará ligeramente y solo tenemos que mostrar la plantilla para demostrar el cambio.

📃Modal.vue

vue
<script setup>
// omitted for brevity
const onClose = () => show.value = !show.value
</script>

<template>
  <div class="overlay">
    <div class="modal" v-if="show">
      <div class="header">
        <slot name="header" :close="onClose" />
      </div>
      <hr/>
      <div class="content">
        <slot :close="onClose"/>
      </div>
      <hr/>
      <div class="footer">
        <slot name="footer" :close="onClose" />
      </div>
    </div>
  </div>
</template>

<style scoped>
  /* omitted for brevity */
</style>

¡Ahora aquí, podemos escribir algunas pruebas nuevas! Cada uno de nuestros componentes principales debería poder utilizar el método y asegurarse de que esté conectado correctamente. Importaremos h desde Vue para crear nodos virtuales reales para que podamos interactuar con ellos desde fuera de la prueba.

📃Modal.cy.js

js
import Modal from '../Modal.vue'
import { h } from 'vue'

const modalSelector = '.modal'
const footerSelector = '[data-testid=footer-close]'
const headerSelector = '[data-testid=header-close]'
const contentSelector = '[data-testid=content-close]'
const text = 'Close me!'

const slots = {
  footer: ({ close }) => h('div', { onClick: close , 'data-testid': 'footer-close' }, text ),
  header: ({ close }) => h('div', { onClick: close, 'data-testid': 'header-close' }, text ),
  default: ({ close }) => h('div', { onClick: close, 'data-testid': 'content-close' }, text ),
}

describe('<Modal>', () => {
  it('The footer slot binds the close method', () => {
    cy.mount(Modal, { slots })
      .get(footerSelector).should('have.text', text)
      .click()
      .get(modalSelector).should('not.exist')
  })

  it('The header slot binds the close method', () => {
    cy.mount(Modal, { slots })
      .get(headerSelector).should('have.text', text)
      .click()
      .get(modalSelector).should('not.exist')
  })

  it('The default slot binds the close method', () => {
    cy.mount(Modal, { slots })
      .get(contentSelector).should('have.text', text)
      .click()
      .get(modalSelector).should('not.exist')
  })  
})

INFO

Si desea consultar el mismo ejemplo de esta prueba con JSX o del mismo componente con la Options API, puede buscar en la Documentación Oficial de Cypress.

¿Que Sigue?

Ahora que se siente cómodo montando componentes y afirmando sus slots, ¡debe estar listo para probar la mayoría de los componentes con scoped slots y fallbacks!

Trabajemos en la configuración de un comando de montaje personalizado para manejar aplicaciones como Vuetify y complementos como Vue Router.