Breaking out of the component tree
with portal-vue
Thorsten Lünborg
github.com/linusborg // @linus_borg
portal-vue is a Portal Component for Vue.js, for rendering DOM outside of a component, anywhere in the document.
<portal name="source" to="destination">
<p>Hi, I'm {{ name }}!</p>
</portal>
Sending slot content from source to destination
<portal-target name="destination" />
<div class="v-portal"/>
<div class="v-portal-target">
<p>Hi, I'm Thorsten!</p>
</div>
renders
renders
<portal name="source" to="destination">
<template slot-scope="{ items }">
<li v-for="item in items">{{ item }}</li>
</template>
</portal>
Sending scoped slot content from source to destination
<!-- Imagine "items" to be an array of fruit names -->
<portal-target
tag="ul"
name="destination"
slot-props="items"
/>
<div class="v-portal"/>
<ul class="v-portal-target">
<li>Strawberry</li>
<li>Banana</li>
<li>Tomato</li>
</ul>
renders
renders
<portal name="sourceTwo" to="destination"
order="2"
>
<p>And I'm {{ someOtherName }}!</p>
</portal>
<portal name="sourceOne" to="destination"
order="1"
>
<p>Hi, I'm {{ name }}!</p>
</portal>
Multiple portals!!
<portal-target
name="destination"
multiple
/>
<div class="v-portal"/>
<div class="v-portal-target">
<p>Hi, I'm Thorsten!</p>
<p>and I'm Donald!</p>
</div>
renders
renders
<portal
to="destination"
target-el="#outside-of-app"
>
<p>And I'm {{ someOtherName }}!</p>
</portal>
especially useful for non-SPA apps
<div id="#outside-of-app">
</div>
<div class="v-portal"/>
<div class="v-portal-target">
<p>Hi, I'm Thorsten!</p>
</div>
renders
renders
Will change in 2.0!!
<portal
name="source"
to="destination"
slim
:disabled="toggle"
>
<p>Hi, I'm {{ name }}!</p>
</portal>
i.e. disabling a portal, rendering without a wrapper el ...
<portal-target name="destination" />
<!-- disabled === true -->
<p>Hi, I'm Thorsten</p>
<div class="v-portal-target" />
renders
renders
Portal 1
Portal 2
Portal 3
The Wormhole
Target 1
Target 2
Target 3
Content 1
Content 2
Content 3
for Target 2
for Target 1
import Wormhole from './wormhole'
export default {
created() {
wormhole.open(...)
},
updated() {
wormhole.open(...)
},
beforeDestroy() {
wormhole.close()
},
render(h) {
return h('div', {
class: 'v-portal'
})
}
}
// What we send to the Wormhole
{
to: 'destination',
from: 'source',
// we send the virtualDOM nodes
// to the wormhole
passengers: this.$slots.default,
order: Infinity
}
import Vue from 'vue'
export new Vue({
data: {
transports: {}
},
methods: {
open(content) { ...},
close(name) { ... },
// some helper functions
// and secondary features...
}
})
orders contents of a target in multiple mode
import Wormhole from './wormhole'
export default {
props: ['name'],
data: {
transports: wormhole.transports
},
computed: {
ownTransports: {
return this.transports[this.name]
},
},
render(h) {
return h('div', {}, [
// here we return the vnodes from
// the portal
...this.ownTransports.passengers
])
}
}
// portal.js
export default {
abstract: true
}
// portal.js
export default {
abstract: false,
render(h) {
this.$options.abstract = true
return h('div', { class: 'v-portal' }, [
// if "disabled", render scopedSlot locally
])
},
updated() {
this.sendUpdate() // wormhole.open()
if ( this.$options.abstract ) {
this.$options.abstract = false
}
}
}
something similar was necessary for portal-target
Things to be aware of
<template>
<portal>
<p slot-scope="{ name }">
{{ name }}
</p>
</portal>
</template>
<script>
export default {
provide: {
name: 'Tom'
}
}
</script>
<template>
<portal-target
slot-props="{ name: name }"
/>
</template>
<script>
export default {
// will be `undefined`
inject: ['name']
}
</script>
<template>
<portal>
<p ref="portalP">
{{ name }}
</p>
</portal>
</template>
<script>
export default {
data: {
name: 'Tom'
}
},
mounted() {
this.$nextTick(() => {
// may be undefined, depending
// if the portal renders
// before the target, or asynchrnously
this.$refs.portalP
})
}
</script>
<template>
<portal-target
slot-props="{ name: name }"
/>
</template>
<script>
export default {
// will be `undefined`
inject: ['name']
}
</script>
<template>
<portal>
<div>
<slot>
</div>
</portal>
</template>
<script>
export default {
name: 'reuseablePortal'
}
</script>
<resuable-portal class="for-the-div">
<p>some content</p>
</reusable-portal>
<!--
- this will not be applied to the <div>
- it will be applied to the <portal>
- because of the `abstract` hack (would work for
a truly abstract component like <transition>
- `class` can't be passsed on with $attrs
-->
so?
<!-- PortalWithTarget.js -->
<template functional>
<TargetProvider target-el="targetEl">
<portal
slot-props="{ name }" :to="name" v-bind="$attrs">
<slot />
</portal>
</TargetRpovider>
</template>
<script>
export default {
name: 'PortalWithTarget',
props: ['targetEl']
}
</script>
Logic for mounting portal-target etc will be wrapped in internal TargetProvider component
Add the 100th star and win a beer!