Teleporting in Vue 3

1 Part Series

Teleporting is a new feature brought by the release of Vue 3, inspired by React Portals. The Vue team added this feature with the help of a third-party plugin named Portal-Vue.

Teleporting, as its name suggests, allows you to move an object from one place to another. In Vue JS terms, it allows you to define a component in one place, and render it in a different position in the DOM tree, even outside the scope of the Vue app.

In this article, I will introduce Teleporting, explain the need for it, and finally share a running example to see it in action.

What is Teleporting?

A Component in Vue JS is a first-class citizen. It's encouraged to encapsulate UI, and related logic, into smaller components. These can be reused and nested, one inside another, to build a tree of components that make up your app UI.

Whereas some UI components are better placed towards the end of the <body> element, these require fixed or absolute positioning and management of their z-index. Modal Dialog, Notification UI or Popup UI are an example of this.

The motive behind placing them there is to simplify their styling, eliminate the need to write complex styles, and to deal with z-index Stacking Context behavior.

This is where Vue 3 Teleporting comes to the rescue, and reconciles between the two contradicting approaches forementioned. It allows you to maintain a nested tree of UI Components by letting you physically define a nested component inside one another. It works its magic to render the same component in a different position (different from where it was initially defined) in DOM, satisfying the best practices stated in the latter approach.

You can read more on Teleporting Use Cases on the official RFC page for this feature.

Teleporting can also render components in DOM outside the scope of the app. This opens the door to a number of possibilities that it can help with. For instance, Vue is used to build a certain section of the Page. With Teleporting, you can dynamically inject components into the DOM in other sections of the Page not belonging to the jurisdiction of the app. Think of it like Dashboards with widgets!

Now that you have an idea of what Teleporting is, let’s move on.

Teleport Component

Vue 3 provides the <teleport> component, which creates all the magic behind Teleporting. This component has two useful properties:

to: This required property specifies the location in DOM where it renders the content of the <teleport> component. The value can be any of the following variations below:

  • ID Selector: <teleport to="#location-in-dom">...</teleport>
  • Class Selector: <teleport to=".location-in-dom">...</teleport>
  • Data-Attr Selector: <teleport to="[data-location-in-dom]">...</teleport>
  • Reactive Property: <teleport :to="locationInDom">...</teleport>

disabled: This optional property, when assigned a value of true, disables the state of the <teleport> component. In a disabled state, its content is rendered in the same location where it's defined. Only when the disabled property is assigned a value of false, which is the default, the content is rendered in the DOM location specified in the to property.

One important side-effect to mention in this context is when the disabled property is bound to a reactive property. Switching the value between false and true causes the component to render in two different DOM locations. While doing so, the state of the rendered content is preserved. For example, a <teleport> component hosts a Video player. When the <teleport> component is disabled, the video starts playing in its place. When the disabled property is switched to false, the video continues playing in the new DOM location.

Finally, when you want the <teleport> component to be disabled but not rendering anything to the DOM, you may use v-if, the Vue JS Directive, to hide it and prevent it from rendering.

Vue 3 Modal Component

In this section, we will build a Vue Modal component. It will be used in the next section when I demonstrate the Teleporting feature.

There are countless numbers of Modal components out there. Instead of building ourselves a new one, I will make use of a Modal UI built by Chris Coyier, and introduced in his article on Considerations for Styling a Modal. This article is a valuable reference for building Modals as it explains every decision taken when building such a UI.

Here's a running example of the Modal UI on CodePen - Modal

Let's wrap it in a Vue JS component!

First of all, create a new Vue JS app by running these steps:

Step 1: Install @vue/cli NPM package

Open a terminal window and run the following command:

npm install -g @vue/cli

Step 2: Create a new Vue app

vue create vue-teleporting

Select the default preset when prompted.

Step 3: Run the app

Run the app to make sure it's working properly:

cd vue-teleporting
npm run serve

You should have a running app now!

Step 4: Build the Modal Component

Inside the /src/components/ folder, add a new Vue component named Modal.vue. Replace its content with the following:

<template>
  <div>
    <div class="modal-overlay" id="modal-overlay"></div>
    <div
      class="modal"
      id="modal"
      role="dialog"
      aria-labelledby="modal-header"
      aria-describedby="modal-body"
    >
      <button
        class="close-button"
        id="close-button"
        aria-label="Close modal"
        @click.prevent="closeModal"
      >X</button>
      <div class="modal-container">
        <header class="modal-header" id="modal-header">
          <slot name="header">Header goes here ...</slot>
        </header>

        <section class="modal-body" id="modal-body">
          <slot name="body">Body goes here ...</slot>
        </section>

        <footer class="modal-footer">
          <slot name="footer">Footer goes here ...</slot>
        </footer>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: "Modal",
  methods: {
    closeModal() {
      this.$emit("close-modal");
    },
  },
};
</script>

I've made a few changes to convert this UI component to a Vue component. The additions include:

Three new sections representing the header, body, and footer of the Modal component.

The use of <slot> components to add flexibility.

You can read more about Slots in Vue JS by checking my article on Content Distribution in Vue JS.

The use of a few new styling tags to cater for the new sections.

Making the Modal accessible by introducing a few aria attributes. For instance, the use of role="dialog" helps assistive software identify the Modal component as being a Dialog or Window that's separated from the rest of the app UI.

You can read more about page accessibility by checking Daniel Marin's article, Make it Accessible: Better Layout with HTML

The Close button now emits the modal-close event to send a request to the parent component hosting this Modal to eventually close it.

Let's embed this new component inside the App.vue and run the app.

<template>
  <Modal @close-modal="closeModal">
    <template #body>
      <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae expedita corrupti laudantium aperiam, doloremque explicabo ipsum earum dicta saepe delectus totam vitae ipsam doloribus et obcaecati facilis eius assumenda, cumque.</p>
      <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae expedita corrupti laudantium aperiam, doloremque explicabo ipsum earum dicta saepe delectus totam vitae ipsam doloribus et obcaecati facilis eius assumenda, cumque.</p>
    </template>
  </Modal>
</template>

__Figure 1__ below shows the app running with the Modal component open.

vue-teleporting-1

That's it! Now we have a Modal Vue component.

Let's make use of this component in the next section.

Teleporting in Action

So far we have been using Vue 2 to build a Modal component. Now it's time to add the Vue 3 bits into our app and use Teleporting!

Open a terminal window and navigate to the root folder of our app. Then, run the following command to convert our Vue 2 app into a Vue 3 one.

vue add vue-next

Open the `/package.json` file and make sure the `dependencies` and `dev-dependencies` sections are similar to this below:
 "dependencies": {
    "core-js": "^3.6.5",
    "vue": "^3.0.0-rc.5"
  },
  "devDependencies": {
    "@vue/cli-plugin-babel": "~4.4.0",
    "@vue/cli-plugin-eslint": "~4.4.0",
    "@vue/cli-service": "~4.4.0",
    "@vue/compiler-sfc": "^3.0.0-rc.5",
    "babel-eslint": "^10.1.0",
    "eslint": "^6.7.2",
    "eslint-plugin-vue": "^7.0.0-alpha.0",
    "vue-cli-plugin-vue-next": "~0.1.3"
  },

The __vue-next__ package automatically converts it for you.

Switch back to the App.vue and replace its content with:

<template>
  <div>
    <h1>Demonstrating Teleporting in Vue 3</h1>
    <teleport to="#modal-area">
      <Modal @close-modal="closeModal">
        <template #body>
          <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae expedita corrupti laudantium aperiam, doloremque explicabo ipsum earum dicta saepe delectus totam vitae ipsam doloribus et obcaecati facilis eius assumenda, cumque.</p>
          <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae expedita corrupti laudantium aperiam, doloremque explicabo ipsum earum dicta saepe delectus totam vitae ipsam doloribus et obcaecati facilis eius assumenda, cumque.</p>
        </template>
      </Modal>
    </teleport>
  </div>
</template>

<script>
import Modal from "./components/Modal.vue";

export default {
  name: "App",
  components: {
    Modal,
  },
  methods: {
    closeModal() {},
  },
};
</script>

The component now uses a `` component to wrap the Modal component. The `to` property is now pointed to a `
` element located in the `/public/index.html` file.

Replace the contents of the /public/index.html file with the following:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width,initial-scale=1.0">
  <link rel="icon" href="<%= BASE_URL %>favicon.ico">
  <title><%= htmlWebpackPlugin.options.title %></title>
</head>

<body>
  <noscript>
    <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled.
      Please enable it to continue.</strong>
  </noscript>
  <div id="app"></div>
  <div id="modal-area"></div>
  <!-- built files will be auto injected -->
</body>

</html>

Let's run the app and see it in action!

Figure 2 below shows the Vue 3 app running. vue-teleporting-2

The Modal component is now rendered inside the <div id=”modal-area”/> outside the scope of the Vue 3 app and inside the <body> element of the index.html page.

You are nesting the Modal component inside the App.vue and Vue 3 will render it inside the <body> tag. Have your cake and eat it too!

How to close the Modal and remove its content from the DOM?

Let's make use of the disabled property of the <teleport> component and also the v-if Vue directive to close the Modal, and remove the component from the DOM.

<template>
  <div>
    <h1>Demonstrating Teleporting in Vue 3</h1>
    <teleport to="#modal-area" :disabled="shouldClose" v-if="!shouldClose">
      <Modal @close-modal="closeModal">
        <template #body>
          ...
        </template>
      </Modal>
    </teleport>
  </div>
</template>

<script>
import Modal from "./components/Modal.vue";

export default {
  name: "App",
  components: {
    Modal,
  },
  data() {
    return {
      shouldClose: false,
    };
  },
  methods: {
    closeModal() {
      this.shouldClose = true;
    },
  },
};
</script>

The code introduces a new reactive property named `shouldClose`. This property is assigned a value of `true` by default.

The code assigns the shouldClose property to the disabled property on the <teleport> component :disabled=”shouldClose”. As long as the value of shouldClose is false, the <teleport> component won't be disabled, hence it will render its content to the DOM.

When the user clicks the Close button, the button's event handler sets the value of shouldClose property to true. The <teleport> reactively stops rendering inside the <body> tag. However, it tries to render the Modal component in the position where it was originally defined- that is, inside the App.vue component.

Our goal is to prevent the <teleport> component from rendering anything to the DOM. We can achieve that by using the v-if=”!shouldClose” directive expression on the <teleport> component. This instructs the <teleport> component not to render its content anywhere inside the DOM.

That's all!

Conclusion

This article introduced the Teleporting feature coming along with Vue 3.

I hope you appreciate the importance and value of such an addition on the Vue framework. This will allow building app UIs to the standards and best practices of both the Vue framework, as well as UI building and design.

You might also like