---
contentId: fe3fd9b9-481e-4d38-9cea-969595fc913a
title: "Les boîtes de dialogue dans l'expérience utilisateur"
description: "Après des années de recherche d'une approche efficace pour gérer les boîtes de dialogue, j'ai enfin découvert une technique fiable et efficace."
date: 2025-03-29
---

Autant que je me souvienne, ce défi a été le plus persistant depuis que j'ai commencé le **développement web**. Récemment, j'ai découvert une méthode pour construire une bibliothèque de composants, ce qui a été un long processus. Cependant, résoudre les subtilités de la gestion des boîtes de dialogue s'est avéré encore plus vaste, complexe, et crucial en raison de leur présence omniprésente dans chaque application.

<CalloutReadMore id="75df6bff-de40-4056-af23-fae2401cd21a" />

Aujourd'hui, je suis ravi de partager la solution que j'ai trouvée, _je ne l'ai pas développée_, pour gérer efficacement les boîtes de dialogue au sein de vos applications. Cette solution découle de plusieurs années d'expérience, et j'espère qu'elle vous sera aussi bénéfique qu'elle l'a été pour moi.

## Comprendre le problème

Avant de plonger dans le problème, alignons-nous sur ce dont nous parlons. Cet article se concentre sur les **boîtes de dialogue**, **modaux**, et **slideover**. Chacun sert à afficher des informations ou à solliciter une confirmation, différant principalement en termes de présentation et d'utilisation.

Essayez d'ouvrir la boîte de dialogue en cliquant sur le bouton ci-dessous.

<div class="flex justify-center">
  <Modal
    title="Boîte de Dialogue"
    description="Ceci est une boîte de dialogue"
    :ui="{ content: 'min-w-[280px]' }"
  >
    <Button label="Ouvrir la Boîte de Dialogue" />
  </Modal>
</div>

De même, vous pouvez ouvrir un slideover en cliquant sur le bouton.

<div class="flex justify-center">
  <Slideover
    title="Slideover"
    description="Ceci est un slideover"
    :ui="{ content: 'min-w-[280px]' }"
  >
    <Button label="Ouvrir le Slideover" />
  </Slideover>
</div>

Le design et les types de dialogues peuvent sembler secondaires ; l'aspect important réside dans le code sous-jacent. Examinons-le :

::: code-group

```vue [src/components/Modal.vue]
<template>
  <Modal
    title="Boîte de Dialogue"
    description="Ceci est une boîte de dialogue"
  >
    <Button label="Ouvrir la Boîte de Dialogue" />
  </Modal>
</template>
```

```vue [src/components/Slideover.vue]
<template>
  <Slideover
    title="Slideover"
    description="Ceci est un slideover"
  >
    <Button label="Ouvrir le Slideover" />
  </Slideover>
</template>
```

:::

Ils sont très similaires, reposant sur le même composant en coulisses. La véritable intrigue réside dans la manière dont ce code est utilisé. Ces exemples illustrent que les boîtes de dialogue s'intègrent parfaitement comme tout élément HTML, ce qui est le cœur du problème.

Les boîtes de dialogue sont un type particulier de composant dans une application :

1. Ils peuvent être invoqués de n'importe où dans l'application.
2. Ils s'ouvrent par divers moyens, tels que :
    - Clics de bouton
    - Pressions de touches
    - Navigations dans les routes
3. La nature omniprésente rend des applications sans boîtes de dialogue presque impraticables sans sacrifier l'expérience utilisateur :
    - Création d'éléments
    - Modification d'éléments
    - Confirmations d'action
    - Visualisations d'éléments
    - Etc.

    Chaque action peut mener à des conséquences immédiates ou attendre une réponse du serveur, ce qui complique les possibilités d'utilisation des boîtes de dialogue.

En raison de ces facteurs, les boîtes de dialogue nécessitent une attention minutieuse lors du développement et de l'utilisation ; sinon, ils et leur logique peuvent rapidement devenir chaotiques.

De plus, j'ai toujours eu une préférence personnelle pour permettre l'empilement de boîtes de dialogue avec des animations fluides, comme ceci :

<figure>
  <video autoplay loop muted playsinline>
    <source src="https://images.soubiran.dev/posts/a-journey-to-craft-interactive-ui-experiences-with-dialogs/modal-stacking.mp4" type="video/mp4">
  </video>
  <figcaption>Empiler des boîtes de dialogue avec une animation élégante.</figcaption>
</figure>

## Boîte de dialogue en ligne

Une première tentative pour relever ce défi est via des **boîtes de dialogue en ligne**. C'est une approche répandue et qui convient aux cas simples. La stratégie consiste à incorporer un composant modal directement dans le template nécessitant celui-ci, comme vu précédemment.

```vue [src/components/Modal.vue]
<template>
  <Modal
    title="Dialogue"
    description="Ceci est un dialogue"
  >
    <Button label="Ouvrir le Dialogue" />
  </Modal>
</template>
```

Bien que l'implémentation soit simple, cette approche présente au moins deux inconvénients significatifs :

1. Les composants de boîte de dialogue deviennent étroitement couplés avec le composant qui les utilise, compliquant la compréhension et le refactoring.
2. Il devient coûteux, en terme de code, d'ajouter une nouvelle boîte de dialogue, surtout lorsqu'on ouvre les ouvre sans boutons ou en découplant les templates d'une boîte de dialogue. Un exemple d'ouverture d'une boîte dialogue par pression de touche suit :

```vue [src/components/InlineModal.vue]
<script lang="ts" setup>
import { ref } from 'vue'

const isDialogOpen = ref(false)

defineShortcuts({
  meta_k: () => {
    isDialogOpen.value = !isDialogOpen.value
  }
})
</script>

<template>
  <div>
    <Modal
      v-model:open="isDialogOpen"
      title="Boîte de Dialogue"
      description="Ceci est une boîte de dialogue"
    />
  </div>
</template>
```

Cette méthode devient rapidement encombrante et difficile à maintenir au fil du temps. Plusieurs boîtes de dialogue intensifient le code répétitif, nécessitant des ajouts répétés dans chaque composant utilisant la boîte de dialogue.

## Boîtes de dialogue globales

Améliorer la gestion des boîtes de dialogue implique d'en ouvrir programatiquement de manière universelle, découplant leur invocation de toute relation directe avec un composant. Ce modèle facilite le refactoring du code en séparant physiquement les boîtes de dialogue des composants, améliorant la réutilisabilité à travers plusieurs composants sans incorporations redondantes.

Un fichier `App.vue` pourrait ressembler à ceci :

```vue [src/App.vue]
<template>
  <div>
    <Layout>
      <RouterView />
    </Layout>

    <ModalFeature1 /> <!-- [!code highlight] -->
    <ModalFeature2 /> <!-- [!code highlight] -->
    <ModalFeature3 /> <!-- [!code highlight] -->

    <!-- Et ainsi de suite... -->
  </div>
</template>
```

Atteindre cette configuration exige de partager une variable entre le composant ouvrant la boîte de dialogue et la boîte de dialogue ell-même. Dans Vue, une approche simple, bien que pas idéale, est de créer une composable externalisant son état.

```ts [src/composables/useDialog.ts]
import { ref } from 'vue'

const isOpen = ref(false) // [!code highlight]

export function useDialog() {
  function open() {
    isOpen.value = true
  }

  function close() {
    isOpen.value = false
  }

  return {
    isOpen,

    open,
    close
  }
}
```

Utilisez ce composable pour le composant ouvrant la boîte de dialogue et la boîte de dialogue elle-même.

```vue [src/App.vue]
<script lang="ts" setup>
import { useDialog } from '@/composables/useDialog'

const { open } = useDialog() // [!code highlight]
</script>

<template>
  <Button label="Ouvrir la Boîte de Dialogue" @click="open" />
</template>
```

```vue [src/components/ModalFeature1.vue]
<script lang="ts" setup>
import { useDialog } from '@/composables/useDialog'

const { isOpen, close } = useDialog() // [!code highlight]
</script>

<template>
  <Modal
    v-model:open="isOpen"
    title="Boîte de Dialogue"
    description="Ceci est une boîte de dialogue"
  >
    <Button label="Fermer" @click="close" />
  </Modal>
</template>
```

Cette approche est certainement supérieure à la méthode en ligne mais présente tout de même des inconvénients :

1. L'inclusion de boîtes de dialogue au niveau de l'application (ou de la route) crée une redondance de code.
2. La gestion des boîtes de dialogue devient complexe, particulièrement avec de nombreuses boîtes de dialogue ou propriétés, chacune nécessitant un passage à travers la composable.

    ```ts
    import { ref } from 'vue'

    const isOpen = ref(false)
    const title = ref('') // [!code highlight]
    const description = ref('') // [!code highlight]

    export function useDialog() {
      function open(newTitle: string, newDescription: string) {
        title.value = newTitle
        description.value = newDescription
        isOpen.value = true
      }

      function close() {
        isOpen.value = false
      }

      return {
        isOpen,
        title,
        description,
        open,
        close
      }
    }
    ```

> [!NOTE]
> Simplifiez l'écriture des composables avec [`createSharedComposable`](https://vueuse.org/shared/createSharedComposable/) de [`@vueuse/core`](https://vueuse.org/) pour créer un composable singleton, partageant automatiquement le même état entre tous les composants qui l'utilisent.

## Dialogues programmatiques

La méthode la plus récente et profondément efficace que j'ai découverte implique **la gestion programmatique des boîtes de dialogue**. Cette méthode résout les difficultés mentionnées plus tôt en permettant un contrôle complet des boîtes de dialogue par le biais du code plutôt que des templates.

Ayant utilisé des boîtes de dialogue programmatiques dans **Angular** avec Angular Material, j'ai apprécié sa capacité à simplifier le code.

```ts [Angular Material Dialog]
const dialogRef = dialog.open(UserProfileComponent, {
  height: '400px',
  width: '600px',
})

dialogRef.afterClosed().subscribe((result) => {
  console.log(`Résultat du dialogue : ${result}`) // Pizza !
})

dialogRef.close('Pizza !')
```

Pendant longtemps, j'ai espéré d'une fonctionnalité similaire dans Vue.

Ce temps est désormais arrivé, lorsque [Nuxt](https://nuxt.com/) a sorti la version 3 de [Nuxt UI](https://ui.nuxt.com/) avec la composable `useOverlay`. Ce composable partagé par l'application permet d'ouvrir des boîtes de dialogue de n'importe quel endroit, imitant la fonctionnalité d'Angular Material.

```vue [src/App.vue]
<script setup lang="ts">
const overlay = useOverlay()

async function openModal() {
  overlay
    .create(MyModal)
    .open()
}
</script>
```

Magnifique, n'est-ce pas ? 😍

Sous le capot, cette méthode utilise la directive [`component`](https://vuejs.org/guide/essentials/component-basics.html#dynamic-components) avec une simple boucle [`v-for`](https://vuejs.org/api/built-in-directives.html#v-for) pour rendre les boîtes de dialogue.

```vue [OverlayProvider.vue]
<script lang="ts" setup>
const { overlays, close } = useOverlay()
</script>

<template>
  <component
    :is="overlay.component"
    v-for="overlay in overlays"
    :key="overlay.id"
    v-bind="overlay.props"
    v-model:open="overlay.modelValue"
  />
</template>
```

> [!NOTE]
> Il s'agit d'une version simplifiée du code.

Avec cette méthode, les coûts de la gestion d'état sont éliminés, ce qui supprime le besoin de composables partagés ou de boîtes de dialogue globales. Cela améliore considérablement la lisibilité, la maintenabilité et la création d'expériences UI interactives.

## Empilement des boîtes de dialogue sans effort

Le point culminant de cette approche est l'empilement de boîtes de dialogue sans effort. Il suffit de rendre un tableau de boîtes de dialogue via une boucle pour permettre l'ouverture simultanée de boîtes de dialogue, facilitant un empilement élégant avec des animations fluides.

::: code-group

```vue [src/App.vue]
<script lang="ts" setup>
import DialogFeature1 from './components/DialogFeature1.vue'

const overlay = useOverlay()
function openOverlay() {
  overlay
    .create(DialogFeature1, {
      props: {
        title: 'Premier Modal',
        description: 'Ceci est le premier modal'
      },
      destroyOnClose: true
    })
    .open()
}
</script>

<template>
  <div>
    <Button label="Ouvrir le Premier Overlay" @click="openOverlay" />

    <OverlayProvider />
  </div>
</template>
```

```vue [src/components/DialogFeature1.vue]
<script lang="ts" setup>
import DialogFeature2 from './DialogFeature2.vue'

const props = defineProps<{
  title: string
  description: string
}>()

const overlay = useOverlay()
function openOverlay() {
  overlay
    .create(DialogFeature2, {
      props: {
        title: 'Deuxième Modal',
        description: 'Ceci est le deuxième modal'
      },
      destroyOnClose: true
    })
    .open()
}
</script>

<template>
  <Modal
    :title="props.title"
    :description="props.description"
  >
    <template #body>
      <Button
        label="Ouvrir le Deuxième Modal"
        @click="openOverlay"
      />
    </template>
  </Modal>
</template>
```

```vue [src/components/DialogFeature2.vue]
<script lang="ts" setup>
const props = defineProps<{
  title: string
  description: string
}>()
</script>

<template>
  <Modal
    :title="props.title"
    :description="props.description"
  />
</template>
```

:::

Explorez le code complet sur <GitHubLink repo="barbapapazes/vue-dialog-stacking" /> et même [une démo en direct](https://vue-dialog-stacking.barbapapazes.dev/).

<figure>
  <iframe
    src="https://vue-dialog-stacking.barbapapazes.dev/"
    width="100%"
    height="600"
    frameborder="0"
    scrolling="no"
    allowfullscreen
    class="bg-white"
  ></iframe>
  <figcaption>Démo en direct de l'empilement de boîtes de dialogue</figcaption>
</figure>

## Conclusion

Après des années de lutte avec les boîtes de dialogue et les modaux et d'essai de plusieurs approches, j'ai enfin trouvé une méthode qui fonctionne pour moi et qui peut fonctionner pour vous.

J'ai d'abord rencontré cette approche avec Angular Material et j'ai espéré quelque chose de similaire dans Vue. [Anthony Fu](https://github.com/antfu) a proposé un début prometteur avec [`useTemplatePromise`](https://github.com/antfu/vue-template-promise), mais cela est resté complexe uniquement pour l'ouverture de boîtes de dialogue.

Enfin, je remercie [zernonia](https://github.com/zernonia) pour [Reka UI](https://reka-ui.com/) et [Eugen Istoc](https://github.com/genu) pour la composable `useOverlay` dans Nuxt UI, je peux désormais gérer les boîtes de dialogue de manière appropriée dans Vue.

Je suis convaincu que cette approche est un changement majeur pour les expériences UI interactives dans Vue. N'hésitez pas à l'essayer !
