I'm trying to create a BaseOverlay
component that basically teleports its content to a certain area of my application. It works just fine except there's an issue when using it with v-show
... I think because my component's root is a Teleport
that v-show won't work because Teleport is a template.
I figured I could then use inheritAttrs: false
and v-bind="$attrs"
on the inner content... this throws a warning from Vue saying Runtime directive used on component with non-element root node. The directives will not function as intended.
It results in v-show
not working on MyComponent
, but v-if
does work.
Any clues as to what I'm doing wrong?
App.vue
<script setup>
import MyComponent from "./MyComponent.vue";
import {ref} from "vue";
const showOverlay = ref(false);
function onClickButton() {
showOverlay.value = !showOverlay.value;
}
</script>
<template>
<button @click="onClickButton">
Toggle Showing
</button>
<div id="overlays" />
<div>
Hello World
</div>
<MyComponent v-show="showOverlay" text="Doesn't work" />
<MyComponent v-if="showOverlay" text="Works" />
</template>
BaseOverlay.vue
<template>
<Teleport to="#overlays">
<div
class="overlay-container"
v-bind="$attrs"
>
<slot />
</div>
</Teleport>
</template>
<script>
export default {
name: "BaseOverlay",
inheritAttrs: false,
};
</script>
MyComponent.vue
<template>
<BaseOverlay>
{{text}}
</BaseOverlay>
</template>
<script>
import BaseOverlay from "./BaseOverlay.vue";
export default {
name: "MyComponent",
components: {
BaseOverlay
},
props: {
text: {
type: String,
default: ""
}
}
}
</script>
Wanted to follow-up on this. I started running into a lot of other issues with Teleport
, like the inability to use it as a root element in a component (like a Dialog component because I want to teleport all dialogs to a certain area of the application), and some other strange issues with KeepAlive
.
I ended up rolling my own WebComponent and using that instead. I have an OverlayManager
WebComponent that is used within the BaseOverlay
component, and every time a BaseOverlay
is mounted, it adds itself to the OverlayManager
.
OverlayManager.js
export class OverlayManager extends HTMLElement {
constructor() {
super();
this.classList.add("absolute", "top-100", "left-0")
document.body.appendChild(this);
}
add(element) {
this.appendChild(element);
}
remove(element) {
this.removeChild(element);
}
}
customElements.define("overlay-manager", OverlayManager);
BaseOverlay.vue
<template>
<div class="overlay-container" ref="rootEl">
<slot />
</div>
</template>
<script>
import {ref, onMounted, onBeforeUnmount, inject} from "vue";
export default {
name: "BaseOverlay",
setup() {
const rootEl = ref(null);
const OverlayManager = inject("OverlayManager");
onMounted(() => {
OverlayManager.add(rootEl.value);
});
onBeforeUnmount(() => {
OverlayManager.remove(rootEl.value);
});
return {
rootEl
}
}
};
</script>
App.vue
<script setup>
import {OverlayManager} from "./OverlayManager.js";
import MyComponent from "./MyComponent.vue";
import {ref, provide} from "vue";
const manager = new OverlayManager();
provide("OverlayManager", manager);
const showOverlay = ref(false);
function onClickButton() {
showOverlay.value = !showOverlay.value;
}
</script>
<template>
<button @click="onClickButton">
Toggle Showing
</button>
<div>
Hello World
</div>
<MyComponent v-show="showOverlay" text="Now Works" />
<MyComponent v-if="showOverlay" text="Works" />
</template>
<style>
.absolute {
position: absolute;
}
.top-100 {
top: 100px;
}
.left-0 {
left: 0;
}
</style>
This behaves exactly how I need it, and I don't have to deal with the quirks that Teleport
introduces, and it allows me to have a singleton that is in charge of all of my overlays. The other benefit is that I have access to the parent of where BaseOverlay
is initially added in the HTML (not where it's moved). Honestly not sure if this is a good practice, but I'm chuffed at how cute this is and how well Vue integrates with it.