I have a Vue 2 pattern I was using for a common scenario: programmatically creating an instance to open a Modal/Dialog/Lightbox on dynamic content outside of a template.
In Vue 2, I found this pattern:
// DialogService.js
export default {
alert(text) {
const DialogClass = Vue.extend(DialogComponentDef);
let dialog = new DialogClass({ propsData: { text } });
dialog.$on('close', () => {
dialog.$destroy();
dialog.$el.remove();
dialog = null;
});
// mount the dynamic dialog component in the page
const mountEl = document.createElement('div');
document.body.appendChild(mountEl);
dialog.$mount(mountEl);
},
};
How can I acheive this in Vue 3, knowing Vue.extends
, $on
& $destroy
do not exist anymore?
You can see a full example of the DialogService.js by clicking here.
I have a Vue 2 pattern I was using for a common scenario: programmatically creating an instance to open a Modal/Dialog/Lightbox on dynamic content outside of a template.
In Vue 2, I found this pattern:
// DialogService.js
export default {
alert(text) {
const DialogClass = Vue.extend(DialogComponentDef);
let dialog = new DialogClass({ propsData: { text } });
dialog.$on('close', () => {
dialog.$destroy();
dialog.$el.remove();
dialog = null;
});
// mount the dynamic dialog component in the page
const mountEl = document.createElement('div');
document.body.appendChild(mountEl);
dialog.$mount(mountEl);
},
};
How can I acheive this in Vue 3, knowing Vue.extends
, $on
& $destroy
do not exist anymore?
You can see a full example of the DialogService.js by clicking here.
- Not a duplicate but related, stackoverflow.com/questions/63471824/vue-js-3-event-bus – Estus Flask Commented Oct 27, 2021 at 6:21
- Actually not related at all. The event bus thing isn't the issue here. – darkylmnx Commented Oct 28, 2021 at 11:32
- It is. You create event bus that can be used programmatically (on and emit methods). Vue 3 doesn't provide such bus, so it needs to be provided externally. The rest could be the same, more or less. 'new Vue' is replaced with 'createApp'. Think of it not as of extended comp, but as of sub-app, because it really is one – Estus Flask Commented Oct 28, 2021 at 14:55
- 2 Well, createApp doesn't keep the context of the previous app, while Vue.extend did, so createApp isn't the solution anyway here. I changed the title so that it's more explicit. – darkylmnx Commented Oct 29, 2021 at 17:52
4 Answers
Reset to default 10Here's how to do with createApp
in Vue 3, but the context (stores, plugins, directives...) will not be kept.
// DialogService.js
import { createApp } from 'vue';
export default {
alert(text) {
const mountEl = document.createElement('div');
document.body.appendChild(mountEl);
const dialog = createApp({ extends: DialogComponentDef }, {
// props
text,
// events are passed as props here with on[EventName]
onClose() {
mountEl.parentNode.removeChild(mountEl);
dialog.unmount();
dialog = null;
},
});
dialog.mount(mountEl);
},
};
To keep the context, there's something more complicated that can be seen here with h
and render
Vue methods : https://github.com/vuejs/vue-next/issues/2097#issuecomment-709860132
I would recommend using the mount-vue-component. It's lightweight and easy to use. Code example:
import MyComponent1 from './MyComponent1.vue'
import MyComponent2 from './MyComponent2.vue'
import { mount } from 'mount-vue-component'
let dynamicComponent = mount(someCondition ? MyComponent1 : MyComponent2, { props: { <someProperties...> }, app: MyVueApp })
Here is the simple way to call and run a component programmatically
/* DialogService.js */
import DialogVue from './Dialog.vue';
import { createApp } from 'vue';
const Dialog = (options = {}) => {
const onClose = options.onClose;
const tempDiv = document.createElement('div');
const instance = createApp(DialogVue).mount(tempDiv);
instance.title = options.title;
instance.text = options.text;
instance.onClose = options.onClose;
instance.show = true;
document.body.appendChild(instance.$el);
}
export default Dialog;
<!-- Dialog.vue -->
<template>
<transition name="fade-bottom">
<h3 v-if="title">{{ title }}</h3>
{{ text }}
<button @click="show = false; onClose()">Cancel</button>
</transition>
</template>
<script setup>
import { ref, defineExpose } from 'vue'
const show = ref(false)
const title = ref('')
const text = ref('')
const onClose = () => {}
defineExpose({
title,
text,
show,
onClose
})
</script>
Vue 3 doesn't provide a generic event bus. It can be replaced with lightweight third-party alternatives like mitt
or eventemitter3
.
A component can be mounted outside application element hierarchy with a teleport. This has been previously available in Vue 2 with third-party portal-vue
library. Modals and other screen UI elements are common use cases for it
<teleport to="body">
<DialogComponent ref="dialog" @close="console.log('just a notification')">
Some markup that cannot be easily passed as dialog.value.show('text')
</DialogComponent>
</teleport>
Where DialogComponent
controls its own visibility and doesn't need to be explicitly unmounted like in original snippet. A cleanup is performed automatically on parent unmount:
<teleport to="body">
<div v-if="dialogState">
<slot>{{dialogText}}</slot>
</div>
</teleport>
and
let dialogState = ref(false);
let dialogText = ref('');
let show = (text) => {
dialogText.value = text;
dialogState.value = true;
} ;
...
return { show };
For more complex scenarios that require to manage multiple instances, or access show
outside components in business logic, a teleport needs to be mounted at the top of component hierarchy. In this case an instance of event bus that can be passed through the application can be used for interaction.