3

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.

darkylmnx
  • 1,891
  • 4
  • 22
  • 36
  • Not a duplicate but related, https://stackoverflow.com/questions/63471824/vue-js-3-event-bus – Estus Flask Oct 27 '21 at 06:21
  • Actually not related at all. The event bus thing isn't the issue here. – darkylmnx Oct 28 '21 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 Oct 28 '21 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 Oct 29 '21 at 17:52

4 Answers4

7

Here'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

binaryfunt
  • 6,401
  • 5
  • 37
  • 59
darkylmnx
  • 1,891
  • 4
  • 22
  • 36
2

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>
Andres Separ
  • 3,144
  • 1
  • 19
  • 17
1

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.

Estus Flask
  • 206,104
  • 70
  • 425
  • 565
  • Well my question has nothing to do with event buses. Also, the modal here is just an example of the pattern, the whole pattern here is to be able to instantiate a component when you're not in a template. Teleport is meant to solve things done in a template A that has to appear in a template B. Which isn't the same scenario here. – darkylmnx Oct 28 '21 at 11:31
  • Well, it could be worded better then. This is a common way to do what you asked about in V3. V2 didn't have teleports to mount a comp outside the app, so you had to jump through hoops by mounting it manually. – Estus Flask Oct 28 '21 at 15:01
  • I changed the title so that it's more explicit. – darkylmnx Oct 29 '21 at 17:52
1

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 })
Marshal
  • 4,452
  • 1
  • 23
  • 15