Home

How Do Vue Like Your Toast?

Toast! Pantone

When developing an app, it's common to need to provide feedback to users about events or actions that have taken place. For example, you may want to let them know that a database record has been created or updated. However, displaying this information shouldn't come at the cost of disrupting the content on the screen. That's where toasts come in.

Named after the similarity to bread popping up from a toaster:

A toast provides simple feedback about an operation in a small popup. It only fills the amount of space required for the message and the current activity remains visible and interactive. Toasts automatically disappear after a timeout.

Toasts

This quick guide is created for applications using Laravel and Vue, with Inertia bridging the gap between them.

Setup

If you don't already have an application set up with the above, create a new one by running laravel new NEW_APP, enter the newly created app's directory and install Laravel Breeze using Composer by running composer require laravel/breeze --dev.

Once added, install breeze with Vue with the Artisan command php artisan breeze:install vue.

Toast Package

There's a nice little package that is really easy to setup called Vue Toastification. Add the Vue Toastification package by running

# yarn
yarn add vue-toastification@next

# npm
npm install --save vue-toastification@next

and then update app.js to use the newly added package.

import './bootstrap';
import '../css/app.css';

import { createApp, h } from 'vue';
import { createInertiaApp } from '@inertiajs/vue3';
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
import { ZiggyVue } from '../../vendor/tightenco/ziggy/dist/vue.m';
+import Toast from "vue-toastification";
+import "vue-toastification/dist/index.css";

const appName = window.document.getElementsByTagName('title')[0]?.innerText || 'Laravel';

createInertiaApp({
    title: (title) => `${title} - ${appName}`,
    resolve: (name) => resolvePageComponent(`./Pages/${name}.vue`, import.meta.glob('./Pages/**/*.vue')),
    setup({ el, App, props, plugin }) {
        return createApp({ render: () => h(App, props) })
            .use(plugin)
            .use(ZiggyVue, Ziggy)
+           .use(Toast)
            .mount(el);
    },
    progress: {
        color: '#4B5563',
    },
});

Inside HandleInertiaRequests.php there is a share() method that defines the props that are shared by default inside the app. Update this to share a toast key which grabs a toast value from the session if one exists.

/**
    * Define the props that are shared by default.
    *
    * @return array<string, mixed>
    */
public function share(Request $request): array
{
    return array_merge(parent::share($request), [
        'auth' => [
            'user' => $request->user(),
            'role' => $request->user()?->roles()->first() ?? null,
            'permissions' => $request->user()?->roles()->first()?->permissions()->pluck('permission'),
        ],
        'ziggy' => function () use ($request) {
            return array_merge((new Ziggy)->toArray(), [
                'location' => $request->url(),
            ]);
        },
+       'toast' => $request->session()->get('toast', null),
    ]);
}

And finally add the below to the script section of the layout file being used in which you want to display toast notifications.

import { useToast } from "vue-toastification";

const toast = useToast();

const toastMessage = computed(() => {
    return usePage().props.toast
})

watch(toastMessage, () => {
    toast(toastMessage.value.message, {
        type: toastMessage.value.type
    });
});

Now if you were to update a model in your controller and return back to the same page with a toast message flashed to the session like below, a nice toast notification should pop up in the top right hand section of the screen and automatically hide after a few seconds.

public function store(ModelRequest $modelRequest)
{
    Model::create($modelRequest->validated());

    return back()->with('toast', [
        'title' => 'Created',
        'message' => 'Model successfully created',
        'type' => 'success',
    ]);
}

However, if you were to store the new model and redirect to another page this would not yet display the notification as Inertia destroys and recreates child layout instances between visits. Using a persistent layout solves this.

Once again, inside app.js, update the resolve property to set a default layout file.

+ import Layout from './Layout'

createInertiaApp({
- resolve: (name) => resolvePageComponent(`./Pages/${name}.vue`, import.meta.glob('./Pages/**/*.vue')),
+ resolve: name => {
+   const pages = import.meta.glob('./Pages/**/*.vue', { eager: true })
+   let page = pages[`./Pages/${name}.vue`]
+   page.default.layout = page.default.layout || Layout
+   return page
+ },
  // ...
})

Now all Vue files that match the above settings will use your defined layout and can just have a root <div> instead of importing the Layout inside the Vue file, like below:

<script setup></script>

<template>
    <div></div>
</template>

And a method that redirects to a new page will display a toast notification.

public function store(ModelRequest $modelRequest)
{
    Model::create($modelRequest->validated());

    return redirect()->route('models.index')->with('toast', [
        'title' => 'Created',
        'message' => 'Model successfully created',
        'type' => 'success',
    ]);
}

This does mean however, that pages such as a login page will have the new default layout, which is likely unwanted. Fix this by setting the default layout inside that file.

<script>
import GuestLayout from './GuestLayout'

export default {
  // Using a render function...
  layout: (h, page) => h(GuestLayout, [page]),
}
</script>

<script setup>
...
</script>

<template>
    <div>
        <Head title="Log in" />

        <LoginForm />
    </div>
</template>