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 useToaster
Le 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
})
// (...)