<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 "alerts" 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()->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'),
],
]);
}</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(() => 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) => {
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
}
}</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"><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">
</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></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"><template>
<div>
<!-- layout content... -->
<AlertHandler/>
</div>
</template>
<script setup>
import AlertHandler from "@/Components/AlertHandler.vue";
</script></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><script setup></code> components in Vue 3. To get persistent layouts to work, you need to add a second <code><script></code> tag to your components to provide the <code>layout</code>:</p>
<pre><code class="language-html"><script setup>
// ...
</script>
<script>
import layout from '@/Layouts/Authenticated.vue';
export default {layout}
</script></code></pre>