September 5, 2022

Creating alerts using Laravel, Vue 3 and Inertia.js

<p>I'm currently working on a <a href="https://devmail.email/">new side project</a> using my favourite stack (Laravel, Vue and Inertia.js). I wanted to implement some &quot;alerts&quot; as toast notifications in the app and decided to share how I did it.</p> <p><img src="https://images.ctfassets.net/9b1r03jrrwqy/41AfTgJplpisr0wZvEQZ16/7f464bf1f499f8b15dfe4b8c1f7d2125/9e9ca9c4fa9a2e9d9dcc797c3109b8b3.png" alt="Toast notifications" /> <em>Image credit: <a href="https://dribbble.com/shots/15137093--Notifications-New-Countly-UI">Antonin Kus</a></em></p> <h2>Laravel</h2> <p>First, we need to pass some data to the front-end. Inertia makes this dead simple. We can flash some alert data and <a href="https://inertiajs.com/shared-data">share it</a> with the response.</p> <pre><code class="language-php">// In your app (e.g. a controller) request()-&gt;session()-&gt;flash('alert', [ 'type' =&gt; $type, 'message' =&gt; $message, ]); // In HandleInertiaRequests public function share(Request $request) { return array_merge(parent::share($request), [ // ... 'flash' =&gt; [ 'alert' =&gt; $request-&gt;session()-&gt;get('alert'), ], ]); }</code></pre> <h2>Vue</h2> <p>Now that we have some data we can <a href="https://inertiajs.com/shared-data#accessing-shared-data">access in our Vue components</a>, we need to actually display the alerts (as toast notifications in this case).</p> <pre><code class="language-js">const alert = computed(() =&gt; usePage().props.value.flash.alert);</code></pre> <p>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 <a href="https://inertiajs.com/pages#persistent-layouts">persistent layouts</a>. This prevents any components in the layout from re-rendering, even when you change page in the app.</p> <p>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 <a href="https://vuejs.org/guide/scaling-up/state-management.html#simple-state-management-with-reactivity-api">global ref</a> is fine. I made a small <code>useAlerts</code> <a href="https://vuejs.org/guide/reusability/composables.html#composables">composable</a> to act as my alert store:</p> <pre><code class="language-js">import {ref} from "vue"; import {v4 as uuidv4} from "uuid"; const alerts = ref([]); export default function useAlerts() { const removeAlert = (id) =&gt; { setTimeout(() =&gt; { alerts.value = alerts.value.filter(alert =&gt; alert.id !== id); }, 3000); }; const addAlert = (alert) =&gt; { const id = uuidv4(); alerts.value.push({ id: id, ...alert }); removeAlert(id); if (alerts.value.length &gt; 5) { alerts.value = alerts.value.slice(1); } }; return { alerts, addAlert, removeAlert } }</code></pre> <p>Here I'm using a simple array to store my <code>alerts</code>. There are two methods:</p> <ul> <li><code>addAlert</code> which pushes the payload to the array giving each item an <a href="https://github.com/uuidjs/uuid">unique ID</a> and making sure there are no more than 5 alerts shown at the same time.</li> <li><code>removeAlert</code> which removes an alert by the given ID after 3 seconds.</li> </ul> <p>Now that we have some simple store logic in place, we can use this composable in a new <code>AlertHandler</code> component:</p> <pre><code class="language-html">&lt;template&gt; &lt;Teleport to="body"&gt; &lt;div v-if="alerts.length" class="toast-container position-fixed bottom-0 end-0 p-3"&gt; &lt;div v-for="alert in alerts" :key="alert.id" class="toast align-items-center fade show" role="alert" aria-live="assertive" aria-atomic="true" &gt; &lt;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', }" &gt; &lt;div class="toast-body"&gt; &lt;/div&gt; &lt;button type="button" class="btn-close me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"&gt; &lt;/button&gt; &lt;/div&gt; &lt;/div&gt; &lt;/div&gt; &lt;/Teleport&gt; &lt;/template&gt; &lt;script setup&gt; import {computed, watch} from "vue"; import {usePage} from "@inertiajs/inertia-vue3"; import useAlerts from "@/Composables/useAlerts"; const alert = computed(() =&gt; usePage().props.value.flash.alert); const {addAlert, alerts} = useAlerts(); watch(alert, (newVal) =&gt; { if (newVal) { addAlert(newVal); } }); &lt;/script&gt;</code></pre> <p>Note that this project is using the <a href="https://getbootstrap.com/docs/5.2/components/toasts/">Bootstrap 5 toast component</a>. The logic here is:</p> <ol> <li>We catch any new alert payloads passed from the backend via Inertia as a page prop (<code>usePage().props.value.flash.alert</code>).</li> <li>We watch this computed <code>alert</code> property for any changes and call the <code>addAlert</code> method from our composable when <code>watch</code> is triggered.</li> </ol> <p>The template simply loops through any <code>alerts</code> and renders them as toast notifications.</p> <p>The final piece of the puzzle is to use our new <code>AlertHandler</code> component in our persistent layout:</p> <pre><code class="language-html">&lt;template&gt; &lt;div&gt; &lt;!-- layout content... --&gt; &lt;AlertHandler/&gt; &lt;/div&gt; &lt;/template&gt; &lt;script setup&gt; import AlertHandler from "@/Components/AlertHandler.vue"; &lt;/script&gt;</code></pre> <p>One small thing to note: Inertia <a href="https://github.com/inertiajs/inertia/discussions/651">doesn't currently support</a> the fancy <code>layout</code> syntax when using <code>&lt;script setup&gt;</code> components in Vue 3. To get persistent layouts to work, you need to add a second <code>&lt;script&gt;</code> tag to your components to provide the <code>layout</code>:</p> <pre><code class="language-html">&lt;script setup&gt; // ... &lt;/script&gt; &lt;script&gt; import layout from '@/Layouts/Authenticated.vue'; export default {layout} &lt;/script&gt;</code></pre>