How do I create a global component without injection?

Viewed 691

There is a case that we need a component in global to use it like this.$toast('some words') or this.$dialog({title:'title words',contentText:'some words').

In Vue 2.x, we can add Toast.vue's methods to Vue.prototype, and call Toast.vue's methods everywhere. But how do we do this in Vue 3.x?

I read the document of i18n plugin demo in vue-next. But it needs to inject the i18n plugin into every component that needs to use it. It's not convenient.

2 Answers

Vue 3 provides an API for attaching global properties:

import { createApp } from 'vue'

const app = createApp({})
app.config.globalProperties.$toast = () => { /*...*/ }

A way showing the component anywhere in vue3 app without injection

mechanism

mountting the component into dom each time.

implementation

use 'Toast' for example:

step 1: create a SFC (Toast.vue)

<template>
  <transition name="fade">
    <div class="toast" v-html="msg" :style="style" @click="closeHandle"></div>
  </transition>
</template>

<script>
  import {ref,computed,onMounted,onUnmounted} from 'vue'
  export default {
    name: "Toast",
    props:{
      msg:{type:String,required:true},
      backgroundColor:{type:String},
      color:{type:String},
      // closing the Toast when timed out. 0:not closed until to call this.$closeToast()
      timeout:{type:Number,default:2000, validate:function (val){return val >= 0}},
      // closing the Toast immediately by click it, not wait the timed out.
      clickToClose:{type:Boolean, default: true},
      // a function provied by ToastPlugin.js, to unmout the toast. 
      close:{type:Function,required: true}
    },

    setup(props){
      let innerTimeout = ref();
      const style = computed(
          ()=>{return{backgroundColor:props.backgroundColor ? props.backgroundColor : '#696969', color:props.color ? props.color : '#FFFFFF'}}
      );

      onMounted(()=>{
        toClearTimeout();
        if(props.timeout > 0)
          innerTimeout.value = setTimeout(()=>{ props.close(); },props.timeout);
      });

      /**
       * when toast be unmounted, clear the 'innerTimeout'
       */
      onUnmounted(()=>{toClearTimeout()})

      /**
       * unmount the toast
       */
      const closeHandle = () => {
        if(props.clickToClose)
          props.close();
      }

      /**
       * to clear the 'innerTimeout' if it exists.
       */
      const toClearTimeout = ()=>{
        if(innerTimeout.value){
          try{
            clearTimeout(innerTimeout.value);
          }catch (e){
            console.error(e);
          }
        }
      }

      return {style,closeHandle};
    },
  }
</script>

<style scoped>
  .toast{position: fixed; top: 50%; left: 50%; padding: .3rem .8rem .3rem .8rem; transform: translate(-50%,-50%); z-index: 99999;
    border-radius: 2px; text-align: center; font-size: .8rem; letter-spacing: .1rem;}
  .fade-enter-active{transition: opacity .1s;}
  .fade-leave-active {transition: opacity .3s;}
  .fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {opacity: 0;}

</style>

step 2: create a plugin (ToastPlugin.js)

import Toast from "./Toast.vue";
import {createApp} from 'vue'
const install = (app) => {
    // dom container for mount the Toast.vue
    let container;
    // like 'app' just for Toast.vue
    let toastApp;
    // 'props' that Toast.vue required.
    const baseProps = {
        // define a function to close(unmount) the toast used for 
        // case 1: in Toast.vue "click toast appeared and close it"
        // case 2: call 'this.$closeToast()' to close the toast in anywhere outside Toast.vue
        close:()=> {
            if (toastApp)
                toastApp.unmount(container);

            container = document.querySelector('#ToastPlug');
            if(container)
                document.body.removeChild(container);
        }
    };
    
    // show Toast
    const toast = (msg)=>{
        if(typeof msg === 'string')
            msg = {msg};

        const props = {...baseProps,...msg}
        console.log('props:',JSON.stringify(props));

        // assume the toast(previous) was not closed, and try to close it.
        props.close();

        // create a dom container and mount th Toast.vue
        container = document.createElement('div');
        container.setAttribute('id','ToastPlug');
        document.body.appendChild(container);
        toastApp = createApp(Toast, props);
        toastApp.mount(container);
    }
    
    // set 'toast()' and 'close()' globally
    app.config.globalProperties.$toast = toast;
    app.config.globalProperties.$closeToast = baseProps.close;
}

export default install;

step 3: usage

in main.js

import ToastPlugin from 'xxx/ToastPlugin'
import { createApp } from 'vue'

const app = createApp({})
app.use(ToastPlugin)

// then the toast can be used in anywhere like this:
this.$toast('some words')
this.$toast({msg:'some words',timeout:3000})
Related