Roman Koliada
Roman Koliada

Reputation: 5102

Is it possible to render a component automatically in vue 3?

I'm working on the small library for showing notifications/toasts for vue 3. My idea is to append an invisible container for my notifications during the plugin registration. So end user should not care about rendering this area. Is it possible at all?

My current plugin look like this:

export const plugin = {
    install: (app: App, options?) => {
        options = reactive(options || defaultOptions);

        app.provide(symbol, instance);
        app.component('vue3-notification', Notification);
        app.component('vue3-notifications', Overlay);
        console.log('app', app); // app._component is null at this point
        var test = Overlay.render({ notifications: instance });
        console.log('test', test); // how to attach Overlay component to app?
    }
};

It seems like when a plugin is installed the vue root container is not available yet. I managed to render my component providing needed dependency(at least I hope so, it's logged to the console in the last line) but I don't know how to mount it and integrate with the main app.

My overlay component that I want to render automatically from plugin look like this:

<div class="notifications-overlay">
    <Teleport to="body">
        <vue3-notification
            v-for="(n, index) in notifications.stack.value"
            :key="n.id"
            v-bind="n"
            v-bind:hide="() => hide(n.id)"
        ></vue3-notification>
    </Teleport>
</div>

And it has fixed position:

.notifications-overlay {
    position: fixed;
    top: 0;
    left: 0;
    width: 100vw;
    height: 100vh;
    pointer-events: none;
}

So it does not matter where it's rendered exactly, I just want to be it automatically available inside the vue app after using my plugin.

Any thoughts?

Upvotes: 4

Views: 3967

Answers (1)

Yom T.
Yom T.

Reputation: 9200

In Vue 2, we have Vue.extend for creating a "subclass" of the base Vue constructor, and it also allows you to mount the instance on an element, which is great for this purpose.

However, this API has been removed in Vue 3. Have a read on the RFC for global API changes.

Since the global Vue is no longer a new-able constructor, Vue.extend no longer makes sense in terms of constructor extension.

The good news is, we can still achieve pretty much the same goal by leveraging createApp to render our plugin component and mount it to a DOM element.

If you don't like the idea of instantiating multiple Vue instances, you may want to check out this unofficial library called mount-vue-component. I haven't tried it myself, but it allows you to mount a component without using createApp. Although, it seems to use some internal properties (like _context) to get things done. I'd say whatever is undocumented, is likely to change. But hey.

So, back to the createApp approach. And we won't be using Teleport here. The following steps are just my preferences, so feel free to adjust them according to your use case.

Adding interfaces

import { ComponentPublicInstance } from 'vue';

export interface INotify {
  (message: string): void;
}

export type CustomComponentPublicInstance = ComponentPublicInstance & {
  notify: INotify;
}

We are using intersection type for our custom component instance.

Plugin implementation

import { App, createApp } from 'vue';
import Notifier from './path/to/component/Notifier.vue';

export const injectionKeyNotifier = Symbol('notifier');

export default {
  install(app: App) {
    const mountPoint = document.createElement('div');
    document.body.appendChild(mountPoint);

    const notifier = createApp(Notifier).mount(mountPoint) as CustomComponentPublicInstance;

    app.provide(injectionKeyNotifier, notifier.notify);
  }
}

At this point, we simply need to expose a public method (see INotify above) from the target component (Notifier.vue). I'm calling this method notify. And it takes a string argument for the message.

The component: Notify.vue

<template>
  <div class="my-notifier">
    <div class="msg" v-text="message"></div>
  </div>
</template>

<script lang="ts">
  import { defineComponent, ref } from 'vue';

  export default defineComponent(() => {
    const message = ref('');

    function notify(msg: string) {
      message.value = msg;
    }

    return {
      message,
      notify
    }
  })
</script>

Notice a named function called notify. This is the public method we talked about earlier and we'll need it exported.

Now use() it

On your entry file (e.g. main.ts):

import { createApp } from 'vue'
import App from './App.vue'
import Notifier from 'my-custom-notifier'; // Assumes a library from the NPM registry (if you mean to publish it)

createApp(App)
  .use(Notifier)
  .mount('#app');

Example usage that displays random notification:

<template>
  <div class="home">
    <button @click="showNotifs">Show notification</button>
  </div>
</template>

<script lang="ts">
  import { defineComponent, inject } from 'vue';
  import { INotify, injectionKeyNotifier } from 'my-custom-notifier';

  export default defineComponent({
    name: 'Home',

    setup() {
      const notify = inject(injectionKeyNotifier) as INotify;

      function showNotifs() {
        notify('You have x unread messages.');
      }

      return {
        showNotifs
      }
    }
  })
</script>

And that's it! We are auto-registering the component without our users having to manually add it on the template.

Upvotes: 7

Related Questions