5

I want to know if there's a way to create a reusable scritp/class/service with primevue toast function calls, in such a way that I don't need to call the primevue toast functions directly in every single component.

What I've tried to do up until now, was to create a ToastService.ts like this:

import { useToast } from 'primevue/usetoast';

    const toast = useToast();

    export function addMsgSuccess(): void {
        toast.add({ severity: 'success', summary: 'Test', detail: 'Test', life: 3000 });
    }

But this code crashes my application and I get the following error:

Uncaught Error: No PrimeVue Toast provided!at useToast (usetoast.esm.js?18cb:8:1) eval (ToastService.ts?73ba:3:1) Module../src/shared/service/ToastService.ts (app.js:1864:1) webpack_require (app.js:849:30) fn (app.js:151:20) eval (cjs.js?!./node_modules/babel-loader/lib/index.js!./node_modules/ts-loader/index.js?!./node_modules/eslint-loader/index.js?!./src/views/cadastro-plano/CadastroPlano.ts?vue&type=script&lang=ts:31:87) Module../node_modules/cache-loader/dist/cjs.js?!./node_modules/babel-loader/lib/index.js!./node_modules/ts-loader/index.js?!./node_modules/eslint-loader/index.js?!./src/views/cadastro-plano/CadastroPlano.ts?

Does anyone know how to solve this problem, or to create functions that make this add() call, so I don't need to call it everysingle time?

Thalys Menezes
  • 345
  • 1
  • 4
  • 17

6 Answers6

10

This solution works for me, but I'm not sure it's a good solution.

First: export app from main.ts

// main.ts
import {createApp} from 'vue';
import App from '@/App.vue';

import PrimeVue from 'primevue/config';
import ToastService from 'primevue/toastservice';

export const app = createApp(App);

app.use(PrimeVue);
app.use(ToastService);

app.mount('#app')

Second: import app and use toast service with app.config.globalProperties

// myToastService.ts
import {ToastSeverity} from 'primevue/api';
import {app} from '@/main';

const lifeTime = 3000;

export function info(title: string = 'I am title', body: string = 'I am body'): void {
  app.config.globalProperties.$toast.add({severity: ToastSeverity.INFO, summary: title, detail: body, life: lifeTime});
};

export function error(title: string = 'I am title', body: string = 'I am body'): void {
  app.config.globalProperties.$toast.add({severity: ToastSeverity.ERROR, summary: title, detail: body, life: lifeTime});
};

Third: import myToastService in your component.

// myTestToastComponent.vue
<script setup lang="ts">
import {info, error} from '@/components/myToastService';

info();
</script>
Nina
  • 156
  • 1
  • 7
  • I don't see why this isn't a good approach. I did the same thing to add global filters in vue3. I'll try this approach and if it works, I'll upvote!! Thanks in advance for the answer, I'm confident it will solve my problem. – Thalys Menezes Mar 22 '22 at 00:38
  • It worked perfectly. I'm upvoting your solution because it is with pure Vue and JS. Why did you say it was not a good approach? – Thalys Menezes Mar 24 '22 at 19:55
  • 1
    I think it's kind of weird to export the whole app and import it again in the App. But I have same idea with you that I don't want to use Pinia or Vuex to solve this problem. – Nina Mar 25 '22 at 09:51
  • At first when I was implementing your solution, I didn't see that you were exporting the 'app' const, and the I got an error. After exporting it I thought a bit weird as well, but at least it was a clean solution with no vuex, and no complexity at all. I'm gonna take a look if it is a bad practice to export the whole app. – Thalys Menezes Mar 25 '22 at 14:26
  • It's slightly weird to export app just to import it elsewhere, but it's the best way to get access to global properties. The toast function can be further simplified by doing `app.$toast....` – db2 Jun 15 '22 at 13:46
  • 1
    Update: Instead of exporting all the app I just exported the app.config.globalProperties.$toast and imported it in my ToastService.ts. This way I avoided exporting and importing all the vue app inside the app haha. This one is still the best solution for me, since it is a native one. – Thalys Menezes Nov 24 '22 at 14:40
7

Maybe the following solution works for you:

Add a Toast in App.vue and add a watch that checks a message from the store

<template>
  <router-view />
  <Toast position="bottom-right" group="br" />
</template>
<script>
import { watch } from "vue";
import { useStore } from "vuex";
import { useToast } from "primevue/usetoast";
export default {
  setup() {
   const store = useStore();
   const toast = useToast();

    watch(
      () => store.getters.getErrorMessage,
       (message, prevMessage) => {
        console.log("error message", message);
        if (message) {
          toast.add({
            severity: "error",
            summary: "Error",
            detail: message,
            group: "br",
            life: 6000,
          });
        }
      }
    );
  },
};
</script>

Store

import { createStore } from "vuex";

export default createStore({
    state: { errorMessage: "" },
    mutations: {        
        setErrorMessage(state, payload) {
            state.errorMessage = payload;
        },
    },
    actions: {},
    modules: {},
    getters: {   
            getErrorMessage: (state) => state.errorMessage,
    },
});

then, in any other component just update the message

store.commit("setErrorMessage", error.message);
Wakawey
  • 389
  • 1
  • 6
2

Another option is to handle toasts in a single location (App.vue) and then receive them via a message bus.

First set up a bus in main.js:

import mitt from 'mitt'
const toastBus = mitt()

const app = createApp(App)
// Provide the bus for injection for use by components
app.provide('toastBus', toastBus)

// Export the bus for use by 'non-component' scripts
export { toastBus }

Then inject the bus into App.vue, listen for events and generate toasts:

<template>
<div id="app"></div>
<Toast />
</template>

<script>
import { inject } from 'vue'
import { useToast } from 'primevue/usetoast'

const toastBus = inject('toastBus')
const toast = useToast()

toastBus.on('*', (type, args) => {
  if (type === 'add') {
    toast.add(args)
  } else if (type === 'remove') {
    toast.remove(args)
  }
})

Then to send a toast from a component:

let toastBus = inject('toastBus')

toastBus.emit('add', { severity: 'success', summary: 'Test', detail: 'Test', life: 3000 })

Or from non-component code:

import { toastBus } from './main'

toastBus.emit('add', { severity: 'success', summary: 'Test', detail: 'Test', life: 3000 })
match
  • 10,388
  • 3
  • 23
  • 41
  • I like this answer as it is short and sweet, but need a little adjustment, please check below. – wui May 06 '22 at 16:08
1

After playing around with it for a while, I found a solution I believe to be elegant, though it uses Pinia. With this method, you can call toasts in helper functions as well, and reusing the functions are quite simplistic.

main.ts

import { createApp } from "vue";
import App from "./App.vue";

import { createPinia } from "pinia";
import PrimeVue from "primevue/config";
import ToastService from "primevue/toastservice";
import Toast from "primevue/toast";


createApp(App)
    .use(router)
    .use(PrimeVue)
    .use(createPinia())
    .use(ToastService)
    .component("Toast", Toast)
    .mount("#app");

interfaces.ts

export interface Message {
    severity: string;
    summary: string;
    detail: string;
}

useNotifications.ts

import { defineStore } from "pinia";
import { Message } from "@interfaces";

interface State {
    info: Message;
    notify: Message;
    confirm: Message;
}

const useNotifications = defineStore({
    id: "notificationStore",
    // Might be better to only have one piece of state, but this is the solution I went with
    // info for basic notifs, notify for sticky notifs, confirm for notifs that need confirmation
    state: (): State => ({
        info: {
            severity: "",
            summary: "",
            detail: "",
        },
        notify: {
            severity: "",
            summary: "",
            detail: "",
        },
        confirm: {
            severity: "",
            summary: "",
            detail: "",
        },
    }),
});

export default useNotifications;

App.vue

<script setup lang="ts">
    import { useToast } from "primevue/usetoast";
    import { useNotifications } from "@store";
    import { Message } from "@interfaces";

    const notificationStore = useNotifications();
    const toast = useToast();

    // Watches changes on notificationStore throughout the app
    notificationStore.$subscribe((mutation, state) => {
        // Checks which part of the state has been mutated, and updates that state based on those conditions
        // mutation.events.key will throw a TypeScript error, but it will still work (until build time where another solution should be found)
        const key = mutation.events.key;
        if (key === "info") {
            const { severity, summary, detail } = state.info;
            toast.add({ severity, summary, detail, life: 3000, group: "br" });
        } else if (key === "notify") {
            const { severity, summary, detail } = state.notify;
            toast.add({ severity, summary, detail, group: "br" });
        } else if (key === "confirm") {
            const { severity, summary, detail } = state.confirm;
            toast.add({ severity, summary, detail, group: "bc" });
        }
    });

    // Use provide to make Toast functionality easily injectable
    provide("toastConfirm", (args: Message) => {
        const { severity, summary, detail } = args;
        notificationStore.confirm = { severity, summary, detail };
    });
    provide("toastNotify", (args: Message) => {
        const { severity, summary, detail } = args;
        notificationStore.notify = { severity, summary, detail };
    });
    provide("toastInfo", (args: Message) => {
        const { severity, summary, detail } = args;
        notificationStore.info = { severity, summary, detail };
    });

    const denyToast = () => {
        toast.removeGroup("bc");
    };
    // Have not figured out how to make this function useable
    const acceptToast = () => {
        toast.removeGroup("bc");
    };
</script>

<template>
    <!-- This group will represent the toasts I do not wish to have to confirm -->
    <Toast position="bottom-right" group="br" />
    <!-- The 'confirmable' toast template is basically copy pasted from PrimeVue docs -->
    <!-- This Toast will appear in the bottom center -->
    <Toast position="bottom-center" group="bc">
        <template #message="slotProps">
            <div>
                <div>
                    <i class="pi pi-exclamation-triangle" />
                    <h4>{{ slotProps.message.summary }}</h4>
                    <p>{{ slotProps.message.detail }}</p>
                </div>
                <div>
                    <div>
                        <Button class="p-button-danger" label="No" @click="denyToast" />
                        <Button class="p-button-success" label="Yes" @click="acceptToast" />
                    </div>
                </div>
            </div>
        </template>
    </Toast>
</template>

AnyChildComponent.vue

<script setup lang="ts">
    import { inject } from "vue";
    import { Message } from "@interface";

    const notifyMe = inject("toastNotify", (args: Message) => {});

    const handleClick = () => {
        notifyMe({
            severity: "success",
            summary: "success!",
            detail: "this is from child component",
        });
    };
</script>

<template>
    <Button @click="handleClick" />
</template>

exampleOfUsageInHelperFunction.ts

import { useNotifications } from "@store";

// Database helper function
const send = async (channel: string, data?: Object) => {
    const notificationStore = useNotifications();
    if (data) data = JSON.parse(JSON.stringify(data));

    // response will return something like: { message: "Query failed", error: error } or { message: "Query succeeded", success?: returnData }
    const response = await window.electron.ipcRenderer.invoke(channel, data);
    if (response.error) {
        console.log(response.error);
        notificationStore.notify = {
            severity: "danger",
            summary: "Error",
            detail: response.message,
        };
    } else {
        notificationStore.info = {
            severity: "success",
            summary: "Success",
            detail: response.message,
        };
        if (response.success) return response.success;
    }
};

export default send;
Peter
  • 68
  • 6
  • I liked your approach as well, but I'm not marking it as the one who best solved my problem because I found it a bit too complex and long. Moreover, you had to use another lib (Pinia) and VueX, which I really didn't want to use it. – Thalys Menezes Mar 24 '22 at 20:05
0

for answer:above @match and for those who are struggling change App.vue to:

  <template>
       <div id="app"></div>
       <Toast />
  </template>
    
  <script>
    import { inject } from 'vue'
    import { useToast } from 'primevue/usetoast'
    
    export default {
       setup() {
          const toastBus = inject('toastBus')
          const toast = useToast()
    
          toastBus.on('*', (type, args) => {
          if (type === 'add') {
            toast.add(args)
          } else if (type === 'remove') {
            toast.remove(args)
          }
       })    
    }
  }

</script>
wui
  • 400
  • 3
  • 11
0

Example for anyone who needs to use toast in the js file

 // App.vue

        <template>
            <Toast />
        </template>
        
        <script>
        import Toast from "primevue/toast";
        
        export default {
          components: {
            Toast: Toast,
          },
        </script>
// store.js

        import { useToast } from "primevue/usetoast";
        
        export const StoreExample = defineStore("StoreExample", () => {
          const toast = useToast();
    
          const onActionGetProducts = async () => {
             return API_EXAMPLE.onApiGetProducts().then(() => {
    
               toast.add({
                  severity: "success",
                  summary: "Success Message",
                  detail: "Order submitted",
                  life: 3000,
               });
             });
          };
    
         return {
           onActionGetProducts,
         };
       });
Quang Dong
  • 467
  • 5
  • 7