Skip to content

Ajouter un système d’alerte (toaster)

Le composable useToaster

ts
// use-toaster.ts
import { 
getRandomId
, type
TitleTag
} from '@gouvminint/vue-dsfr'
export type
Message
= {
id
?: string;
title
?: string;
description
: string;
type
: 'info' | 'success' | 'warning' | 'error';
closeable
: boolean;
titleTag
:
TitleTag
;
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
<
Message
[]>([])
const
useToaster
= () => {
const
removeMessage
= (
id
: string): void => {
const
index
=
messages
.
findIndex
(
message
=>
message
.
id
===
id
)
clearTimeout
(
timeouts
[
id
])
if (
index
=== -1) {
return }
messages
.
splice
(
index
, 1)
} const
addMessage
= (
message
:
Message
): void => {
message
.
id
??=
getRandomId
('toaster')
messages
.
push
({ ...
message
,
description
: `${
message
.
description
} (${
message
.
timeout
})` })
timeouts
[
message
.
id
] =
window
.
setTimeout
(() =>
removeMessage
(
message
.
id
as string),
message
.
timeout
)
} return {
messages
,
addMessage
,
removeMessage
,
} } export default
useToaster

Le composant AppToaster

vue
<script lang="ts" setup>
// AppToaster
import { DsfrAlert } from '@gouvminint/vue-dsfr'

import type { Message } from '@/composables/use-toaster'

defineProps<{ messages: Message[] }>()
const emit = defineEmits(['close-message'])
const close = (id: string) => emit('close-message', id)
</script>

<template>
  <div class="toaster-container">
    <transition-group
      mode="out-in"
      name="list"
      tag="div"
      class="toasters"
    >
      <DsfrAlert
        v-for="message in messages"
        :key="message.id"
        class="app-alert"
        v-bind="message"
        @close="close(message.id as string)"
      />
    </transition-group>
  </div>
</template>

<style scoped>
.toaster-container {
  pointer-events: none;
  position: fixed;
  bottom: 1rem;
  width: 100%;
  z-index: 1;
}
.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>

Utilisation dans une app

vue
<script setup lang="ts">
import { ref } from 'vue'
import { useRegisterSW } from 'virtual:pwa-register/vue'

import ReloadPrompt from '@/components/ReloadPrompt.vue'
import AppToaster from '@/components/AppToaster.vue' // Import du composant AppToaster
import useToaster from './composables/useToaster'      // Import du composable useToaster()

const serviceTitle = 'Service'
const serviceDescription = 'Description du service'
const logoText = ['Ministère', 'de l’intérieur']

const quickLinks = [
  {
    label: 'Home',
    path: '/',
    icon: 'ri-home-4-line',
    iconAttrs: { color: 'var(--red-marianne-425-625)' },
  },
  {
    label: 'À propos',
    path: '/a-propos',
    class: 'fr-fi-user-line',
  },
]
const searchQuery = ref('')

const toaster = useToaster() // Récupération du toaster depuis le composable

setTimeout(() => toaster.addMessage({ // Ajout d’un message...
  title: 'Message 1',
  description: 'Description 1',
  type: 'info',
  closeable: true,
  titleTag: 'h3',
  timeout: 6000, // ...qui disparaîtra après 6 secondes...
}), 500) // ... et qui apparaître après 0.5 seconde

setTimeout(() => toaster.addMessage({ // Ajout d’un deuxième message...
  description: 'Description 2',
  type: 'warning',
  closeable: true,
  titleTag: 'h3',
  timeout: 3000, // ...qui disparaîtra après 3 secondes...
}), 2000) // ... et qui apparaître après 2 secondes

setTimeout(() => toaster.addMessage({ // Ajout d’un troisième message...
  title: 'Message 3',
  description: 'Description 3',
  type: 'success',
  closeable: true,
  titleTag: 'h3',
  timeout: 4000, // ...qui disparaîtra après 3 secondes...
}), 3500) // ... et qui apparaître après 3.5 secondes
</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>