5 September 2022

Creating alerts using Laravel, Vue 3 and Inertia.js

I'm currently working on a new side project using my favourite stack (Laravel, Vue and Inertia.js). I wanted to implement some "alerts" as toast notifications in the app and decided to share how I did it.

Toast notifications Image credit: Antonin Kus

Laravel

First, we need to pass some data to the front-end. Inertia makes this dead simple. We can flash some alert data and share it with the response.

// In your app (e.g. a controller)
request()->session()->flash('alert', [
    'type' => $type,
    'message' => $message,
]);

// In HandleInertiaRequests
public function share(Request $request)
{
    return array_merge(parent::share($request), [
        // ...
        'flash' => [
            'alert' => $request->session()->get('alert'),
        ],
    ]);
}

Vue

Now that we have some data we can access in our Vue components, we need to actually display the alerts (as toast notifications in this case).

const alert = computed(() => usePage().props.value.flash.alert);

One of my conditions for these alerts is that they should remain visible until the timeout expires, even if the user changes page. Thankfully, Inertia make this easy by using persistent layouts. This prevents any components in the layout from re-rendering, even when you change page in the app.

So the first thing we need is a simple store to hold these alerts. We don't need anything as complex as Vuex for this, a global ref is fine. I made a small useAlerts composable to act as my alert store:

import {ref} from "vue";
import {v4 as uuidv4} from "uuid";

const alerts = ref([]);

export default function useAlerts() {
    const removeAlert = (id) => {
        setTimeout(() => {
            alerts.value = alerts.value.filter(alert => alert.id !== id);
        }, 3000);
    };

    const addAlert = (alert) => {
        const id = uuidv4();

        alerts.value.push({
            id: id,
            ...alert
        });

        removeAlert(id);

        if (alerts.value.length > 5) {
            alerts.value = alerts.value.slice(1);
        }
    };

    return {
        alerts,
        addAlert,
        removeAlert
    }
}

Here I'm using a simple array to store my alerts. There are two methods:

  • addAlert which pushes the payload to the array giving each item an unique ID and making sure there are no more than 5 alerts shown at the same time.
  • removeAlert which removes an alert by the given ID after 3 seconds.

Now that we have some simple store logic in place, we can use this composable in a new AlertHandler component:

<template>
    <Teleport to="body">
        <div v-if="alerts.length" class="toast-container position-fixed bottom-0 end-0 p-3">
            <div
                v-for="alert in alerts"
                :key="alert.id"
                class="toast align-items-center fade show"
                role="alert"
                aria-live="assertive"
                aria-atomic="true"
            >
                <div
                    class="d-flex" :class="{
                        'bg-success text-white': alert.type === 'success',
                        'bg-warning text-white': alert.type === 'warning',
                        'bg-danger text-white': alert.type === 'danger',
                    }"
                >
                    <div class="toast-body">
                        @{{ alert.message }}
                    </div>
                    <button
                        type="button" class="btn-close me-2 m-auto" data-bs-dismiss="toast"
                        aria-label="Close">
                    </button>
                </div>
            </div>
        </div>
    </Teleport>
</template>

<script setup>
import {computed, watch} from "vue";
import {usePage} from "@inertiajs/inertia-vue3";
import useAlerts from "@/Composables/useAlerts";

const alert = computed(() => usePage().props.value.flash.alert);
const {addAlert, alerts} = useAlerts();

watch(alert, (newVal) => {
    if (newVal) {
        addAlert(newVal);
    }
});
</script>

Note that this project is using the Bootstrap 5 toast component. The logic here is:

  1. We catch any new alert payloads passed from the backend via Inertia as a page prop (usePage().props.value.flash.alert).
  2. We watch this computed alert property for any changes and call the addAlert method from our composable when watch is triggered.

The template simply loops through any alerts and renders them as toast notifications.

The final piece of the puzzle is to use our new AlertHandler component in our persistent layout:

<template>
    <div>
        <!-- layout content... -->
        <AlertHandler/>
    </div>
</template>

<script setup>
import AlertHandler from "@/Components/AlertHandler.vue";
</script>

One small thing to note: Inertia doesn't currently support the fancy layout syntax when using <script setup> components in Vue 3. To get persistent layouts to work, you need to add a second <script> tag to your components to provide the layout:

<script setup>
// ...
</script>

<script>
import layout from '@/Layouts/Authenticated.vue';

export default {layout}
</script>

Looking for more?

Subscribe to my newsletter to get infrequent updates in your inbox. Or follow me on Twitter.