How to build Portals in Vue.js
whoami
- Vue core team member since ~06/2016
- "That guy on the forum" answering your questions
- Programming is not my job 😱
- Twitter: @linus_borg
- Github: linusborg
The what and why of Portals
A naive Implentation
A better implementation
Available Solutions
Caveats
A Portal moves its children from their original location to another location in the DOM
<Layout />
<Profile />
<Settings />
<EditButton />
<EditModal v-if="editButtonClicked" />
How do we do that?
- We want to render this modal here
- position it absolutely, relative to the viewport
-
but some ancestor element is already relatively position
We move that Modal somewhere else in the DOM
...with a Portal
Answer
A naive Implementation
<portal to="#some-element">
<Modal>
<form>
<input v-model="username"/>
<button @click="save">
Save
</button>
</form>
</Modal>
</portal>
const Portal = Vue.extend({
props: ['to'],
template: `
<div ref="root">
<slot />
</div>
`,
mounted() {
const kids = [...this.$refs.root.childNodes]
const target = document.querySelector(this.to)
kids.forEach(node => {
target.append(node)
})
}
})
The Good
- It's simple
- It works (mostly*)
- Changes to the slot content even update!
The Bad
- It works - until it doesn't
- Real DOM and Virtual DOM are out of sync
- Vue's DOM updates may break
- It doesn't feel clean, right?
This solution is inadequate
- Some situations work, others don't
- Vue should be the only one moving stuff around
- We don't want to touch the DOM anway, right?
A better Implementation
Letting Vue do it's thing
The basic idea
Instead of moving the DOM elements...
...we mount a small component...
...and pass it the content of our portal's slot.
<portal to="#some-element">
<Modal>
<form>
<input v-model="username"/>
<button @click="save">
Save
</button>
</form>
</Modal>
</portal>
const Portal = Vue.extend({
props: ['to'],
render(h) { return h() },
mounted() {
const mountEl = document.createElement('div')
document.querySelector(this.to).append(mountEl)
const content = this.$slots.default
this.target = new Wrapper({
el: mountEl,
propsData: {
content,
},
})
}
})
Demo Time!
Codesandbox: https://codesandbox.io/s/m53x34vl08
Live on Netlify:
I've built not one, but two portal libraries
The simple one:
The big one:
Thank you!
How to build Portals in Vuejs
By Thorsten Lünborg
How to build Portals in Vuejs
A short Lightning Talk & Demo about how to build Portals in Vue.js
- 2,010