'How do I programmatically render a component in VueJS 3?

I'm using VueJS 3. I'm trying to create a dialog service that can be injected into various components throughout the application and then used to open a dialog. For example:

Ideal State using composition API:

random_component.js

<template>
    <div class="header-controls-layout">
        <div class="left">
            <h2 class="font-semibold text-xl text-gray-800 leading-tight">
                Media Library
            </h2>
        </div>
        <div class="right">
            <button @click="openUploadModal()">Upload</button>
        </div>
    </div>
</template>

<script setup>
import UploadMediaModal from '@/Components/UploadMediaModal.vue'

const dialogService = inject('dialog');// Inject dialog service

function updateTileSize(size){
    this.tileSize = size;
}

function openUploadModal(){
   //Open a dialog and pass a component to render with optional settings
   dialogService.open(UploadMediaModal, {data: {}, width: '800px', height: '80vh'});
}

</script>

As you'll notice, I'm injecting the key 'dialog'. I'm registering this in app.js just before mount the application.

app.js

const vueapp = createApp();
        
VueSingleton.prototype.$vue = vueapp;
const instance = new DialogService("dialog-container");
vueapp.provide('dialog', instance);

vueapp.use(plugin);
vueapp.mixin({ methods: { route } });
vueapp.component('Dialog', Dialog);
vueapp.mount('#app');

dialog.service.js

import { VueSingleton } from "./singleton.vue";
import { ref, onUnmounted, createVNode, render } from 'vue';

export class DialogService extends VueSingleton {
    _container = null;
    _container_selector = '';
    _modals = [];
    

    constructor(selector){
        super();
        this._container_selector = selector;
    }

    open(component, {data, width, height, maxWidth, maxHeight}){

        this._container = window.document.getElementById(this._container_selector);

        let appContext = this.$vue._context.app._instance.appContext; // Seems wrong to me
        let vnode = createVNode('Dialog', {component, data, width, height, maxWidth, maxHeight});

        vnode.appContext = {...appContext };
        render(vnode, this._container);    
    }
}

singleton.vue.js

export class VueSingleton{}

Up until this point, everything works as expected more or less.

Firing the openUploadModal method in method in the random_component.js up above, injects the dialog component into the DOM. I can see the dialog component in the document tree inspector.

However this is where the problem begins. As you can see, in the random_component.js above I'm passing a component(UploadMediaModal) to the open method.

The goal here is that I would like to render this component(UploadMediaModal) in the dialog component. But the Dialog component isn't rendering it's template or even firing it's lifecycle hooks.

Why isn't the dialog component rendering? Additionally, how do I programmatically render another component inside the dialog component?

Here is the dialog component: Dialog.vue

<script setup>
    import { ref, onUnmounted, getCurrentInstance,h, createVNode, render, onMounted } from 'vue';

    const props = defineProps(["component", "data", "width", "max-width", "height", "max-height"]);
    const container = ref();
    const { appContext } = getCurrentInstance();
    
    onMounted(() => {
        // Not Firing
        renderDialog();
    })

    // Destory Comp on unmount
    onUnmounted(() => {
        render(null, container);
    });

    function renderDialog(){
        
        let vnode = createVNode(props.component, props.data || {});
        vnode.appContext = {...appContext };
        render(vnode, container);
    }
</script>

<template>
  <transition name="modal-fade">
    <div class="modal-backdrop">
      <div class="modal" role="dialog" aria-labelledby="modalTitle" aria-describedby="modalContent" :style="{'width': props.width || '600px', 'max-width': props.maxWidth || '100%', 'height': props.height || '400px', 'max-height': props.maxHeight || '80vh'}">
        <!-- Modal Injecttion Container -->
        <div ref="container" class="modal-body" id="modalContent"></div>
      </div>
    </div>
  </transition>
</template>

<style>
  .modal-backdrop {
    position: fixed;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    background-color: rgba(0, 0, 0, 0.3);
    display: flex;
    justify-content: center;
    align-items: center;
  }

  .modal {
    background: #FFFFFF;
    box-shadow: 2px 2px 20px 1px;
    overflow-x: auto;
    display: flex;
    flex-direction: column;
  }


  .modal-body {
    position: relative;
    padding: 20px 10px;
  }

  .btn-close {
    position: absolute;
    top: 0;
    right: 0;
    border: none;
    font-size: 20px;
    padding: 10px;
    cursor: pointer;
    font-weight: bold;
    color: #4AAE9B;
    background: transparent;
  }

  .btn-green {
    color: white;
    background: #4AAE9B;
    border: 1px solid #4AAE9B;
    border-radius: 2px;
  }

  .modal-fade-enter,
  .modal-fade-leave-to {
    opacity: 0;
  }

  .modal-fade-enter-active,
  .modal-fade-leave-active {
    transition: opacity .5s ease;
  }
</style>

UploadMediaModal.vue

<script setup>

</script>

<template>
    <p>Upload Media Modal component working!</p>
</template>

<style lang="scss" scoped>

</style>


Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source