Introducing Teleport (aka Portal)
This article is available as a screencast!
Let’s take a look at the new <Teleport>
feature, recently renamed from <Portal>
, in Vue.js 3, and some of the interesting things you might do with it.
To get a basic of idea what <Teleport>
does, read the RFC. Basically, it allows you to render a component at an alternative location on the page by wrapping it in <Teleport>
.
A first look with render functions
The first interesting thing I noticed was when writing a render function in TypeScript. For example:
import { createApp, h, defineComponent, Teleport } from 'vue'
const App = defineComponent({
render() {
return h(() => [
h('div', { id: 'dest' }),
h('div', 'Hello world'),
h(Teleport, { to: '#dest' })
])
}
})
createApp(App).mount('#app')
The idea here is the final h
will be teleported to the first h
, specified by the to
prop. This will actually give us a compile time error - <Teleport>
needs some content to, well, teleport, and we haven’t given it any. This is the first time I’ve actually seen a component give a compile time error when the third argument to h
, the children to render, was not provided. We can make this into a working example by adding children:
h(Teleport, { to: '#dest' }, 'This will be teleported')
Another caveat, that may well be a bug, is that <Teleport>
and the destination must be in components without a root node - also known as a fragment. That is why I am using the h() => [...])
syntax. I am guessing this is a bug that will be fixed in a future alpha (at the time of this article, the latest version of Vue 3 is alpha-12.
Binding to Teleport
Let’s move away from the world of render functions, back into good old .vue
files. We are going to build a small app that let’s us teleport some elements around using Vue’s reactivity. Start with the following:
<template>
<label for="top">Top</label>
<input id="top" type="radio" value="top" v-model="selected" />
<label for="middle">Middle</label>
<input id="middle" type="radio" value="middle" v-model="selected" />
<label for="bottom">Bottom</label>
<input id="bottom" type="radio" value="bottom" v-model="selected" />
<br />
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
export default defineComponent({
setup() {
const selected = ref<'top' | 'middle' | 'bottom'>('top')
return {
selected,
}
}
})
</script>
So far this doesn’t do much - we have three radio buttons bound to the selected
variable. Notice there is no root node - this is the bug I was mentioning earlier.
Next, we will add three destinations - yep, a <Teleport>
doesn’t have to be locked to a specific destination:
<template>
<!-- ... -->
<h1>Top</h1>
<div id="top-teleport"></div>
<h1>Middle</h1>
<div id="middle-teleport"></div>
<h1>Bottom</h1>
<div id="bottom-teleport"></div>
</template>
You might see what’s coming next - now we have 3 radio buttons and 3 destinations, let’s finally add in the <Teleport>
:
<template>
<!-- ... -->
<teleport :to="destination">
This will teleport
</teleport>
</template>
We are binding to a destination
variable. Let’s create that and make sure it updates when the selected
does using a computed
property:
<script lang="ts">
import { defineComponent, ref, computed } from 'vue'
export default defineComponent({
setup() {
const selected = ref<'top' | 'middle' | 'bottom'>('top')
const destination = computed(() => `#${selected.value}-teleport`)
return {
destination,
selected,
}
}
})
</script>
This is enough to get everything working! It’s hard to appreciate what’s going on - best watch the accompanying sreencast to really get an idea, but as you change the selected <Teleport>
using the radio buttons, the content is reactively teleported!
The main use case for this is probably a modal, or something that needs to be rendered at the top level of the application, but is triggered from somewhere else.
Disabling a <Teleport>
It is also possible to disable a <Teleport>
. A good use case for this would be when you want to close a modal, potentially. Let’s add a checkbox to support this:
<template>
<!-- ... -->
<label for="top-disabled">Disabled</label>
<input id="top-disabled" type="checkbox" v-model="disabled" />
<teleport :to="destination" :disabled="disabled">
</teleport>
</template>
<script lang="ts">
import { defineComponent, ref, computed } from 'vue'
export default defineComponent({
setup() {
//...
const disabled = ref(false)
return {
disabled,
destination,
selected,
}
}
})
</script>
Now if you try disabling the <Teleport>
, you will notice the original content is rendered back where it started - this this case, at the bottom of the page.
<Teleport>
maintains the state of the DOM
When content is moved around, it preserves the state of the DOM. A good example of this is using a <video>
element. If you move a <video
> that is playing using a <Teleport>
, the video will keep on playing! <Teleport>
isn’t rerendering the DOM element - it’s really moving the actual element as is, without breaking the state or rerendering it. Very cool.
Conclusion
We learned some of the neat features of <Teleport>
, how to bind to it’s to
prop, about it’s disabled
prop, and how it preseves the state of the DOM elements it is moving.
Absolutely no unsolicted spam. Unsubscribe anytime.