Toaster (snackbar)
Le principe est d’avoir un composable (ici useToaster) qui va recevoir les messages et qui saura les gérer, c’est-à-dire en ajouter et en supprimer dans le tableau des messages accesibles par tous les utilisateurs de ce composable.
Ensuite, il faut un composant toaster (ici AppToaster) qui lira la liste de messages du composable, et qui les affichera.
Enfin, n’importe quel autre composant, composable, store, ou autre fichier, pourra ajouter des messages à la liste de message.
Le composable useToaster
Tout d’abord il faut créer le composable qui recevra et gérera les messages : il exposera la liste de message (messages), une fonction pour ajouter un message à la liste (addMessage()), et un autre pour supprimer un message de la liste (removeMessage).
// use-toaster.ts
import { reactive } from 'vue'
const alphanumBase = 'abcdefghijklmnopqrstuvwyz0123456789'
const alphanum = alphanumBase.repeat(10)
const getRandomAlphaNum = () => {
const randomIndex = Math.floor(Math.random() * alphanum.length)
return alphanum[randomIndex]
}
const getRandomString = (length: number) => {
return Array.from({ length })
.map(getRandomAlphaNum)
.join('')
}
const getRandomHtmlId = (prefix = '', suffix = '') => {
return (prefix ? `${prefix}-` : '') + getRandomString(5) + (suffix ? `-${suffix}` : '')
}
export type Message = {
id?: string
title?: string
description: string
type?: 'info' | 'success' | 'warning' | 'error'
closeable?: boolean
titleTag?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
timeout?: number
style?: Record<string, string>
class?: string | Record<string, string> | Array<string | Record<string, string>>
}
const timeouts: Record<string, number> = {}
const messages: Message[] = reactive([])
const useToaster = (defaultTimeout = 10000) => {
function removeMessage (id: string) {
const index = messages.findIndex(message => message.id === id)
clearTimeout(timeouts[id])
if (index === -1) {
return
}
messages.splice(index, 1)
}
function addMessage (message: Message) {
if (message.id && timeouts[message.id]) {
removeMessage(message.id)
}
message.id ??= getRandomHtmlId('toaster')
message.titleTag ??= 'h3'
message.closeable ??= true
message.type ??= 'info'
message.timeout ??= defaultTimeout
messages.push({ ...message, description: `${message.description}` })
timeouts[message.id] = window.setTimeout(() => removeMessage(message.id as string), message.timeout)
}
function addSuccessMessage (message: Message | string) {
const msg = typeof message === 'string' ? { description: message } : message
addMessage({
...msg,
type: 'success',
})
}
function addErrorMessage (message: Message | string) {
const msg = typeof message === 'string' ? { description: message } : message
addMessage({
...msg,
type: 'error',
})
}
return {
messages,
addMessage,
removeMessage,
addSuccessMessage,
addErrorMessage,
}
}
export default useToasterLe composant AppToaster
Ensuite, il faut créer le composant qui lira les messages depuis ce composable.
<script lang="ts" setup>
import type { Message } from '../composables/use-toaster'
defineProps<{ messages: Message[] }>()
const emit = defineEmits<{
closeMessage: [id: string]
}>()
const close = (id: string) => emit('closeMessage', id)
</script>
<template>
<div class="toaster-container">
<TransitionGroup
mode="out-in"
name="list"
tag="div"
class="toasters"
>
<template
v-for="message in messages"
:key="message.id"
>
<DsfrAlert
class="app-alert"
v-bind="message"
@close="close(message.id as string)"
/>
</template>
</TransitionGroup>
</div>
</template>
<style scoped>
.toaster-container {
pointer-events: none;
position: fixed;
bottom: 1rem;
width: 100%;
z-index: 1750; /* To be on top of .fr-modal which has z-index: 1750 */
}
.toasters {
display: flex;
flex-direction: column;
align-items: center;
}
.app-alert {
background-color: var(--grey-1000-50);
width: 90%;
pointer-events: all;
}
.list-move, /* apply transition to moving elements */
.list-enter-active,
.list-leave-active {
transition: all 0.5s ease;
}
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: translateY(30px);
}
/* ensure leaving items are taken out of layout flow so that moving
animations can be calculated correctly. */
.list-leave-active {
position: fixed;
}
</style>Ajouter ce composant AppToaster dans App.vue
Ce composant AppToaster sera ajouté une seule fois dans l’application : dans le composant principal App.vue, à la toute fin (pour qu’il soit au dessus de tous les autres).
<script setup lang="ts">
// (...)
import { ref } from 'vue' // Import du composant AppToaster
import AppToaster from '@/components/AppToaster.vue'
// (...)
</script>
<template>
<DsfrHeader
v-model="searchQuery"
:service-title="serviceTitle"
:service-description="serviceDescription"
:logo-text="logoText"
:quick-links="quickLinks"
show-search
/>
<div class="fr-container">
<router-view />
</div>
<AppToaster
:messages="toaster.messages"
@close-message="toaster.removeMessage($event)"
/>
</template>Utilisation dans une app
Enfin, depuis n’importe quel fichier, composant ou non, il est possible d’ajouter des messages simplement en utilisant la fonction addMessage() du composable :
import useToaster from './composables/useToaster' // Import du composable useToaster()
const toaster = useToaster() // Récupération du toaster depuis le composable
// (...)
toaster.addMessage({ // Ajout d’un message...
title: 'Message 1',
description: 'Description 1',
type: 'info', // ...de type info...
closeable: true, // ...que l’utilisateur peut fermer...
titleTag: 'h3',
timeout: 6000, // ...qui disparaîtra après 6 secondes
})
// (...)