mirror of
https://github.com/maciejpedzich/maciejpedzi.ch.git
synced 2024-11-27 15:45:47 +01:00
Publish post about the useSnackbar composable
This commit is contained in:
parent
e12793033d
commit
5f7ede3c4d
@ -0,0 +1,179 @@
|
||||
---
|
||||
title: coding up an app-wide notification system for racemash
|
||||
description: "Find out how I took advantage of Vue 3's composables and Vuetify's Snackbar component to design an application-wide alert system"
|
||||
pubDate: 2023-05-27T09:20:02.721Z
|
||||
draft: false
|
||||
categories:
|
||||
- dev diary
|
||||
tags:
|
||||
- racemash
|
||||
- formulaone
|
||||
- appwrite
|
||||
- vue
|
||||
- vuetify
|
||||
---
|
||||
|
||||
Hey folks!
|
||||
|
||||
In this post, I'll show you how I've used a custom composable in conjunction with Vuetify's `v-snackbar` component to create a notification system I can use across my entire Vue 3 application. Let's get into it!
|
||||
|
||||
## Preparing the useSnackbar composable
|
||||
|
||||
It consists of two elements - shared snackbar state and a `showSnackbar` method, for... well, showing the snackabar/notification/alert/whatever you want to call it.
|
||||
|
||||
### Shared snackbar state
|
||||
|
||||
It consists of three properties:
|
||||
|
||||
- `visible`, a boolean flag, which I'm confident speaks for itself. It should be set to `false` by default
|
||||
- `status` that can be set to either `error` or `success`. It should also dictate the alert's color and title
|
||||
- `message` to communicate why the notification showed up in the first place
|
||||
|
||||
Unlike in the `useAuth` composable, this time I opted to use a `reactive` object to hold the snackbar's internal state, because it made sense to group them.
|
||||
|
||||
I did, however, use Vue 3's `toRefs` function as the composable's return value, because accessing, say, `message` in the actual Snackbar component implies that we're referring to the snackbar's state/property. And of course, it's shorter than writing `snackbar.message`.
|
||||
|
||||
With a proper description in place, here's how I converted it to actual code:
|
||||
|
||||
```ts
|
||||
import { reactive, toRefs } from 'vue';
|
||||
|
||||
interface Snackbar {
|
||||
visible: boolean;
|
||||
status: '' | 'error' | 'success';
|
||||
message: string;
|
||||
}
|
||||
|
||||
const snackbar = reactive<Snackbar>({
|
||||
visible: false,
|
||||
status: '',
|
||||
message: ''
|
||||
});
|
||||
|
||||
export function useSnackbar() {
|
||||
return toRefs(snackbar);
|
||||
}
|
||||
```
|
||||
|
||||
### showSnackbar method
|
||||
|
||||
Now, while we could technically leave it at that and just manually set each `ref`s value whenever we wanted to display a notification, I believe a more reasonable approach would be calling a `showSnackbar` method that would accept an object with only the `status` and `message` fields (and which would append `visible: true` behind the scenes) to override the `snackbar` object.
|
||||
|
||||
Here's how the `showSnackbar`'s function definition can look like:
|
||||
|
||||
```ts
|
||||
const showSnackbar = (options: Omit<Snackbar, 'visible'>) =>
|
||||
Object.assign(snackbar, { ...options, visible: true });
|
||||
```
|
||||
|
||||
Notice the use of `Object.assign` instead of the `=` operator. This is because utilising the latter would result in our `snackbar` object losing reactivity.
|
||||
|
||||
### Result
|
||||
|
||||
AKA what you probably came here for anyway. Enjoy!
|
||||
|
||||
```ts
|
||||
import { reactive, toRefs } from 'vue';
|
||||
|
||||
interface Snackbar {
|
||||
visible: boolean;
|
||||
status: '' | 'error' | 'success';
|
||||
message: string;
|
||||
}
|
||||
|
||||
const snackbar = reactive<Snackbar>({
|
||||
status: '',
|
||||
message: '',
|
||||
visible: false
|
||||
});
|
||||
|
||||
export function useSnackbar() {
|
||||
const showSnackbar = (options: Omit<Snackbar, 'visible'>) =>
|
||||
Object.assign(snackbar, { ...options, visible: true });
|
||||
|
||||
return { ...toRefs(snackbar), showSnackbar };
|
||||
}
|
||||
```
|
||||
|
||||
## Creating and using a custom Snackbar component
|
||||
|
||||
In our custom `Snackbar.vue` component's script, I should only have to grab all the snackabr's state `ref`s and create a `computed` property for displaying the right title based on the `status`. And it's as simple as this:
|
||||
|
||||
```ts
|
||||
import { computed } from 'vue';
|
||||
import { useSnackbar } from '@/composables/useSnackbar';
|
||||
|
||||
const { visible, status, message } = useSnackbar();
|
||||
const title = computed(() => (status.value === 'error' ? 'Error' : 'Success'));
|
||||
```
|
||||
|
||||
This component's `template` isn't too complex either, as it boils down to setting the right props and `v-model` of the `v-snackbar` component and adding appropriate tags for displaying the `title` and `message`. Take a look:
|
||||
|
||||
```html
|
||||
<v-snackbar v-model="visible" :color="status" vertical>
|
||||
<h6 class="text-h6 mb-1">{{ title }}</h6>
|
||||
<p class="text-body-1">{{ message }}</p>
|
||||
</v-snackbar>
|
||||
```
|
||||
|
||||
The `vertical` prop is here to enable us to position the `h6` right above the `p`, instead of having them displayed side-by-side.
|
||||
|
||||
Also, we don't need to worry about resetting the `visible` ref's value back to `false` after a couple seconds, because by default, the `v-snackbar` will automatically do it for us after a few seconds. Applying a nice fade-out transition included! How cool is that?
|
||||
|
||||
But right now, we're still unable to see the Snackbar component in action for two reasons:
|
||||
|
||||
1. We haven't placed it anywhere in our app
|
||||
2. We never call the `showSnackbar` function
|
||||
|
||||
We can tackle issue no. 1 by going to `App.vue`, importing the `Snackbar.vue` component and placing it anywhere in the template, just like this:
|
||||
|
||||
```vue
|
||||
<script lang="ts" setup>
|
||||
// ...
|
||||
import Snackbar from './components/ui/Snackbar.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-app>
|
||||
<!-- ... -->
|
||||
<Snackbar />
|
||||
</v-app>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Displaying alerts on successful and failed login
|
||||
|
||||
But what about issue no. 2? Remember how I placed `#login-error` and `#login-success` hashes in the OAuth callback URLs in my previous article? We can check for their presence in the route's hash and show a snackbar in appropriate color depending on the hash in the same `App.vue` component.
|
||||
|
||||
```ts
|
||||
import { onMounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const router = useRouter();
|
||||
const { showSnackbar } = useSnackbar();
|
||||
|
||||
onMounted(async () => {
|
||||
await router.isReady();
|
||||
|
||||
const loginStatusHashes = ['#login-error', '#login-success'];
|
||||
const routeHash = router.currentRoute.value.hash;
|
||||
|
||||
if (loginStatusHashes.includes(routeHash)) {
|
||||
showSnackbar({
|
||||
status: routeHash.replace('#login-', '') as 'error' | 'success',
|
||||
message:
|
||||
routeHash === '#login-error'
|
||||
? 'Failed to log you in'
|
||||
: "You're logged in"
|
||||
});
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Notice the `router.isReady` call. It's necessary, because the `onMounted` hook can get triggered before the router's been initalised, meaning the `includes` check would fail with the `routeHash` being an empty string.
|
||||
|
||||
## Wrapping up
|
||||
|
||||
Thank you so much for reading all the way to the end! I really enjoyed coding up this system and documenting its inner workings, however simple it may be. I believe it's a perfect showcase of the power and flexibilty of Vue 3's composables as a perfect solution for implementing shared application state or an event bus.
|
||||
|
||||
I'll see you all in the next post - take care!
|
Loading…
Reference in New Issue
Block a user