[Vue] Teleport

Zhentiw發表於2024-11-29

Vue’s component architecture enables us to build our user interface into components that beautifully organize our business logic and presentation layer. However, there are some instances where one component has some html that needs to get rendered in an alternative location. For example:

  1. Styles that require fixed or absolute positioning and z-index. For example, it’s a common pattern to place UI components (like modals) right before the </body> tag to ensure they are properly placed in front of all other parts of the webpage.
  2. When our Vue application is running on a small part of our webpage (or a widget), sometimes we may want to move components to other locations in the DOM outside of our Vue app.

Solution

The solution Vue 3 provides is the Teleport component. Previously this was named “Portal”, but the name was changed to Teleport so not to conflict with the future <portal> element which might some day be a part of the HTML standard. The Teleport component allows us to specify template html (which may include child components) that we can send to another part of the DOM. I’m going to show you some very basic usage, and then show you how we might use this in something more advanced. Let’s start by adding a div tag outside of our Vue app, in our basic Vue CLI generated app:

/public/index.html

    ...
    <div id="app"></div>
    <div id="end-of-body"></div>
  </body>
</html>

Then let’s try teleporting some text to this #end-of-body div from inside our Vue application to slightly outside the application.

/src/App.vue

<template>
  <teleport to="#end-of-body">
    This should be at the end.
  </teleport>
  <div>
    This should be at the top.
  </div>
</template>

Notice the teleport line where we specify the div we want to move our template code to, and if we did this right, the text at the top should be moved to the bottom. Sure enough, it does:

(width=300)

https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2Fteleport1.jpg?alt=media&token=0e2b4234-8f01-43cb-a303-e56297c53636

Teleport Options for To

Our to attribute simply needs to be a valid DOM query selector. Aside from using the id like I did above, here are three more examples.

Class selector

<teleport to=".someClass">

Data selector

<teleport to="[data-modal]">

Using a data attribute our target div might look like:

Dynamic selector

If you needed you could even bind a dynamic selector, adding the colon.

<teleport :to="reactiveProperty">

Disabled State

Modals and other pop-ups often start hidden until they are displayed on the screen. For that reason, teleport has a disabled state where the content stays inside the original component. It’s not until teleport is enabled that it will be moved to the target positioning. Let’s update the code to be able to toggle showText, like so:

<template>
  <teleport to="#end-of-body" :disabled="!showText">
    This should be at the end.
  </teleport>
  <div>
    This should be at the top.
  </div>
  <button @click="showText = !showText">
     Toggle showText
  </button>
</template>
<script>
export default {
  data() {
    return {
      showText: false
    };
  }
};
</script>

As you can see, the content inside teleport gets moved from inside the component, to outside the component as we toggle:

<01-disable.gif width=250>

https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2Fteleport2.gif?alt=media&token=04d9934b-bead-49d8-b146-79a2e168e851

If we inspect the source realtime, we can see that the content is actually being moved in the DOM from place to place.

<02-devtools.gif width=367>

https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2Fteleport3.gif?alt=media&token=08fd1a88-377c-4397-a6df-8ba561476d25

Automatically Saving the State

When teleport goes from disabled to enabled, the DOM elements are re-used, so they completely retain the existing state. This can be illustrated by teleporting a playing video.

<template>
  <teleport to="#end-of-body" :disabled="!showText">
    <video autoplay="true" loop="true" width="250">
      <source src="flower.webm" type="video/mp4">
    </video>
  </teleport>
  <div>
    This should be at the top.
  </div>
  <button @click="showText = !showText">
      Toggle showText
  </button>
</template>
<script>
export default {
  data() {
    return {
      showText: false
    };
  }
};
</script>

As you can see in the video below, the state of the video as it moves between locations remains the same.

<03-video.gif width=266>

https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2Fteleport4.gif?alt=media&token=d85592f8-4194-4ef8-aec7-f9b5408156dd

Hiding the Text

If the content we had inside teleport was a modal, we probably wouldn’t want to show it until it was active. Right now “This should be at the end.” is displaying inside our component, even when showText is false. We can disable this from showing by simply adding a v-if.

<template>
  <teleport to="#end-of-body" :disabled="!showText" v-if="showText">
      This should be at the end.
  </teleport>
  ...

Now our text only shows up when showText is true, and thus teleported to the bottom of the page.

<04-v-if width=250>

https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2Fteleport5.gif?alt=media&token=0cef8637-db2c-417d-b2b4-c383ec51c84d

Multiple Teleports into the Same Place

This made me wonder, what happens when you teleport two things into the same place? I can can see (especially with modals) how you might want to teleport more than one thing. Let’s give it a try with our overly simple example, simply creating a showText2.

<template>
  <teleport to="#end-of-body" :disabled="!showText" v-if="showText">
    This should be at the end.
  </teleport>
  <teleport to="#end-of-body" :disabled="!showText2" v-if="showText2">
    This should be at the end too.
  </teleport>
  <div>
    This should be at the top.
  </div>
  <button @click="showText = !showText">
      Toggle showText
  </button>
  <button @click="showText2 = !showText2">
      Toggle showText2
  </button>
</template>
<script>
export default {
  data() {
    return {
      showText: false,
      showText2: false
    };
  }
};
</script>

You can see from the video below that it works as you’d expect, adding the content as it’s toggled. It’s interesting to see that it’s simply appending the element based on which one is clicked first.

<width=300px>

https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2Fteleport6.gif?alt=media&token=e45bd9f9-0116-4b69-8857-3e60210f4de0

Conclusion

As you can see, using teleport provides you a way to keep your code in the same component, while moving pieces of it into other parts of your page. Aside from the obvious solution of using this for modals which need to appear on top of the rest of the page, and placed right above your </body> tag, I’m excited to see how else this Vue 3 feature is used in practice.

For a more detailed written description, check out the RFC.

相關文章