Saltar al contenido

Teleport

<Teleport> es un componente integrado que nos permite "teletransportar" una parte de el template de un componente a un nodo del DOM que existe fuera de la jerarquía del DOM de ese componente.

Uso Básico

A veces, una parte de el template de un componente le pertenece lógicamente, pero desde un punto de vista visual, debería mostrarse en otro lugar del DOM, quizás incluso fuera de la aplicación de Vue.

El ejemplo más común de esto es al construir un modal de pantalla completa. Idealmente, queremos que el código para el botón del modal y el propio modal estén escritos dentro del mismo componente de archivo único, ya que ambos están relacionados con el estado de abierto/cerrado del modal. Pero eso significa que el modal se renderizará junto al botón, profundamente anidado en la jerarquía del DOM de la aplicación. Esto puede crear algunos problemas complicados al posicionar el modal mediante CSS.

Considera la siguiente estructura HTML.

template
<div class="outer">
  <h3>Ejemplo de Vue Teleport</h3>
  <div>
    <MyModal />
  </div>
</div>

Y aquí está la implementación de <MyModal>:

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

const open = ref(false)
</script>

<template>
  <button @click="open = true">Abrir Modal</button>

  <div v-if="open" class="modal">
    <p>¡Hola desde el modal!</p>
    <button @click="open = false">Cerrar</button>
  </div>
</template>

<style scoped>
.modal {
  position: fixed;
  z-index: 999;
  top: 20%;
  left: 50%;
  width: 300px;
  margin-left: -150px;
}
</style>
vue
<script>
export default {
  data() {
    return {
      open: false
    }
  }
}
</script>

<template>
  <button @click="open = true">Abrir Modal</button>

  <div v-if="open" class="modal">
    <p>¡Hola desde el modal!</p>
    <button @click="open = false">Cerrar</button>
  </div>
</template>

<style scoped>
.modal {
  position: fixed;
  z-index: 999;
  top: 20%;
  left: 50%;
  width: 300px;
  margin-left: -150px;
}
</style>

El componente contiene un <button> para activar la apertura del modal, y un <div> con una class de .modal, que contendrá el contenido del modal y un botón para autocerrarse.

Al usar este componente dentro de la estructura HTML inicial, existen varios problemas potenciales:

  • position: fixed solo posiciona el elemento relativo a la ventana gráfica cuando ningún elemento ancestro tiene la propiedad transform, perspective o filter establecida. Si, por ejemplo, intentamos animar el ancestro <div class="outer"> con una transformación CSS, ¡rompería el diseño del modal!

  • El z-index del modal está restringido por sus elementos contenedores. Si hay otro elemento que se superpone con <div class="outer"> y tiene un z-index más alto, cubriría nuestro modal.

<Teleport> proporciona una manera limpia de solucionar estos problemas, permitiéndonos salir de la estructura del DOM anidada. Modifiquemos <MyModal> para usar <Teleport>:

template
<button @click="open = true">Abrir Modal</button>

<Teleport to="body">
  <div v-if="open" class="modal">
    <p>¡Hola desde el modal!</p>
    <button @click="open = false">Cerrar</button>
  </div>
</Teleport>

El objetivo to de <Teleport> espera una cadena de selector CSS o un nodo del DOM real. Aquí, esencialmente le estamos diciendo a Vue que "teletransporte este fragmento de template a la etiqueta body".

Puedes hacer clic en el botón de abajo e inspeccionar la etiqueta <body> a través de las herramientas de desarrollo de tu navegador:

Puedes combinar <Teleport> con <Transition> para crear modales animados - consulta el Ejemplo aquí.

TIP

El objetivo to de teleport debe estar ya en el DOM cuando el componente <Teleport> se monta. Idealmente, este debería ser un elemento fuera de toda la aplicación de Vue. Si apuntas a otro elemento renderizado por Vue, debes asegurarte de que ese elemento esté montado antes que el <Teleport>.

Uso con Componentes

<Teleport> solo altera la estructura del DOM renderizada; no afecta la jerarquía lógica de los componentes. Es decir, si <Teleport> contiene un componente, ese componente seguirá siendo un hijo lógico del componente padre que contiene el <Teleport>. El paso de props y la emisión de eventos seguirán funcionando de la misma manera.

Esto también significa que las inyecciones de un componente padre funcionan como se espera, y que el componente hijo estará anidado debajo del componente padre en las Vue Devtools, en lugar de ser colocado donde se movió el contenido real.

Desactivar Teleport

En algunos casos, es posible que queramos desactivar condicionalmente <Teleport>. Por ejemplo, podríamos querer renderizar un componente como una superposición para escritorio, pero en línea en móvil. <Teleport> soporta la prop disabled, que puede activarse/desactivarse dinámicamente:

template
<Teleport :disabled="isMobile">
  ...
</Teleport>

Podríamos entonces actualizar dinámicamente isMobile.

Múltiples Teleports en el Mismo Objetivo

Un caso de uso común sería un componente <Modal> reutilizable, con el potencial de tener múltiples instancias activas al mismo tiempo. Para este tipo de escenario, múltiples componentes <Teleport> pueden montar su contenido en el mismo elemento objetivo. El orden será un simple añadido, con los montajes posteriores ubicados después de los anteriores, pero todos dentro del elemento objetivo.

Dado el siguiente uso:

template
<Teleport to="#modals">
  <div>A</div>
</Teleport>
<Teleport to="#modals">
  <div>B</div>
</Teleport>

El resultado renderizado sería:

html
<div id="modals">
  <div>A</div>
  <div>B</div>
</div>

Teleport Diferido

En Vue 3.5 y superior, podemos usar la prop defer para diferir la resolución del objetivo de un Teleport hasta que otras partes de la aplicación se hayan montado. Esto permite que el Teleport apunte a un elemento contenedor que es renderizado por Vue, pero en una parte posterior del árbol de componentes:

template
<Teleport defer to="#late-div">...</Teleport>

<!-- en algún lugar más tarde en el template -->
<div id="late-div"></div>

Ten en cuenta que el elemento objetivo debe renderizarse en el mismo ciclo de montaje / actualización que el Teleport; es decir, si el <div> se monta solo un segundo después, el Teleport seguirá reportando un error. El defer funciona de manera similar al hook del ciclo de vida mounted.


Relacionado

Teleport
FREE WEEKEND NOW LIVE!
NOW LIVE! Unlimited access to ALL Vue School courses
Join Now
02
days
:
06
hours
:
14
minutes
: