Skip to content

Théorie - Semaine 9 - Routage et navigation dans les applications Vue

Jusqu'à présent, nous avons vu comment créer des applications Vue de base avec une seule page. Cependant, la plupart des applications web modernes ont plusieurs pages et nécessitent une navigation entre ces pages. C'est là que le routage entre en jeu.

3 étapes pour ajouter le routage à une application Vue

  1. Associer des routes à des composants
  2. Ajoutez-le <RouterView> sur la page principale
  3. Ajouter des liens dans une barre de navigation ou sur des boutons

Nous allons voir en détail chacune de ces étapes.

Premièrement, nous allons créer un dossier router dans le répertoire src de notre projet Vue. Ce dossier contiendra tous les fichiers liés au routage de notre application.

Ensuite, dans le dossier précédemment créé, nous allons créer un fichier index.ts qui contiendra la configuration de notre router Vue. C'est dans ce fichier que nous allons définir les différentes routes de notre application et les composants qui leur sont associés.

Assurez-vous d'ajouter le contenu suivant dans le fichier index.ts. Cela va constituer la configuration de base de notre router Vue. Il contient également le nécessaire pour faire fonctionner l'exemple vu lors de la précédente théorie.

typescript
import type { RouteRecordRaw } from 'vue-router';

import Articles from '@/components/ListeArticles.vue';

const routes: Array<RouteRecordRaw> = [
    {
        path: '/',
        component: Articles,
    },
];

export { routes }

Information ❗

On remarque que chaque route a un chemin(path) et une composante à charger. Le chemin par défaut est /.

Attention ⚠

Il nous manque la bibliothèque vue-router pour aller plus loin. Dans votre terminal, positionnez-vous a la racine de votre projet puis installez vue-router avec cette commande : npm i vue-router.

Maintenant, dans le fichier main.ts, nous devons ajouter les routes au router de Vue.

typescript
import { createApp } from 'vue';
import App from './App.vue';
// Import le vue-router et les routes que nous avons définies dans le fichier `index.ts` du dossier `router`.
import { createRouter, createWebHistory } from 'vue-router';
import { routes } from '@/router';

// Créer le router de Vue en utilisant les routes que nous avons définies dans le fichier `index.ts` du dossier `router`.
const router = createRouter({
  history: createWebHistory(),
  routes,
});

const app = createApp(App);

// Dire à notre application Vue d'utiliser le router que nous venons de créer.
app.use(router);

app.mount('#app');

Truc 💡

L'avantage du fichier index.ts est qu'on peut l'importer facilement en référençant le dossier directement au lieu de mettre le dossier et le nom. C'est comme le fichier par défaut du répertoire.

Maintenant que nous avons configuré notre router, nous devons ajouter le composant <RouterView> dans notre composant principal App.vue. C'est à cet endroit que les composants associés aux différentes routes seront affichés.

Nous allons également pouvoir enlever la référence a ListeArticles .vue dans App.vue puisque nous allons maintenant afficher ce composant via le router.

vue
<script setup lang="ts">
  import { RouterView } from 'vue-router'
</script>

<template>
  <h1>Gestion d'inventaire</h1>
  <RouterView></RouterView>
</template>

Maintenant, on devrait voir le composant ListeArticles.vue s'afficher lorsque nous accédons à la route / de notre application. Nous avons réussi à configurer le routage de base dans notre application Vue !

Il serait intéressant d'ajouter d'autres routes afin de tester le fonctionnement du router!

Dans le fichier index.ts du dossier router, ajoutez les routes suivantes :

typescript
const routes: Array<RouteRecordRaw> = [
    {
        path: '/connexion',
        component: PageConnexion,
    },
    {
        path: '/articles',
        component: Articles,
    },
    {
        path: '/articles/ajouter',
        component: AjouterArticle,
    },
    {
        path: '/articles/:nom',
        component: ArticleDetails,
    },
    {
        path: '/',
        redirect: '/connexion',
        name: 'accueil'
    },
    {
        path: '/:catchAll(.*)',
        component: PageNonTrouvee,
    },
];

L'ordre des routes a de l'importance. Ce sera toujours la première route qui concorde avec l'url qui sera exécutée. Vous devez toujours mettre la route la plus spécifique en premier.

Truc 💡

C'est pour cela que la route qui concorde avec tout -> '/:catchAll(.*)' est à la fin. Elle sera exécutée seulement si aucune autre route ne concorde.

Les routes sont maintenant configurées, mais nous devons créer les composants associés à chacune de ces routes pour pouvoir les afficher.

Une bonne pratique est de créer un dossier views dans le répertoire src de notre projet Vue pour y mettre tous les composants associés aux différentes routes de notre application.

Déplacer le fichier ListeArticles.vue dans le dossier views et créer les fichiers suivants dans le même dossier :

  • PageConnexion
  • AjouterArticle
  • ArticleDetails
  • PageNonTrouvee

Ajouter ce contenu dans chacun de ces fichiers, en changeant le nom du titre h2 :

vue
<script setup lang="ts">
</script>

<template>
  <h2>Ajouter un article</h2>
</template>

Dans le fichier index.ts du dossier router, n'oubliez pas d'importer les composants que nous venons de créer pour les associer aux routes.

typescript
import type { RouteRecordRaw } from "vue-router";

import Articles from '@/views/ListeArticles.vue';
import PageConnexion from '@/views/PageConnexion.vue';
import AjouterArticle from '@/views/AjouterArticle.vue';
import ArticleDetails from '@/views/ArticleDetails.vue';
import PageNonTrouvee from '@/views/PageNonTrouvee.vue';

//... les routes

Nous avons maintenant un site fait avec Vue qui peut gérer plusieurs pages grâce aux routes!

Si vous entrez une route qui n'existe pas, vous devriez voir la page PageNonTrouvee ou la page 404.

Elle peut ressembler à cela.

vue
<script setup lang="ts">
import { RouterLink } from 'vue-router';
</script>

<template>
  <div class="row">
    <div class="col-md-12">
      <div>
        <h2>404 Page non trouvée</h2>
        <div>Désolé, la page que vous cherchez n'existe pas.</div>
        <div>
          <RouterLink :to="{ name: 'accueil' }">
            Retour à l'accueil
          </RouterLink>
        </div>
      </div>
    </div>
  </div>
</template>

Ajout d'une barre de navigation

Maintenant que nous avons configuré le routage de notre application Vue, nous allons ajouter une barre de navigation pour permettre aux utilisateurs de naviguer entre les différentes pages de notre application.

Dans le dossier components, créez un fichier NavBar.vue et ajoutez le contenu suivant :

vue
<script setup lang="ts">
</script>

<template>
  <nav class="navbar navbar-expand-md bg-light">
    <div class="container-fluid">
      <a
        class="navbar-brand"
        href="#"
      >Gestionnaire d'articles</a>
      <button
        class="navbar-toggler"
        type="button"
        data-bs-toggle="collapse"
        data-bs-target="#navbarNav"
        aria-controls="navbarNav"
        aria-expanded="false"
        aria-label="Toggle navigation"
      >
        <span class="navbar-toggler-icon" />
      </button>
      <div
        class="collapse navbar-collapse"
        id="navbarNav"
      >
        <ul class="navbar-nav">
          <li class="nav-item">
            <a
              class="nav-link"
              href="#"
            >Connexion</a>
          </li>
          <li class="nav-item">
            <a
              class="nav-link"
              href="#"
            >
              <i class="bi bi-cart2" />Articles
            </a>
          </li>
        </ul>
      </div>
    </div>
  </nav>
</template>

Il nous reste maintenant à ajouter ce composant NavBar.vue dans notre composant principal App.vue pour qu'il soit affiché sur toutes les pages de notre application.

vue
<script setup lang="ts">
import { RouterView } from 'vue-router'
import NavBar from '@/components/NavBar.vue'
</script>

<template>
  <NavBar />
  <RouterView />
</template>

Attention ⚠

Il nous manque bootstrap pour que la barre de navigation s'affiche comme il faut. Nous allons y revenir un peu plus tard.

Avec Vue, il n'est pas conseillé d'utiliser href pour spécifier des liens!

Attention ⚠

N'utilisez pas href!

En gros, le href demande a la page de se rafraichir. Avec Vue, on ne veut pas devoir rafraichir la page pour naviguer entre les différentes pages de notre application. Il s'agit du principe de SPA (Single page app).

Nous allons donc utiliser le composant <RouterLink> pour spécifier les liens de navigation dans notre barre de navigation. Ce composant sera transformé en un lien cliquable qui permettra de naviguer entre les différentes pages de notre application sans avoir à rafraichir la page. Voici comment utiliser le composant <RouterLink> dans notre composant NavBar.vue :

vue
<script setup lang="ts">
import { RouterLink } from 'vue-router'
</script>

<template>
  <nav class="navbar navbar-expand-md bg-light">
    <div class="container-fluid">
      <router-link
        class="navbar-brand"
        :to="{ name: 'accueil' }"
      >
        Gestionnaire d'articles
      </router-link>
      <button
        class="navbar-toggler"
        type="button"
        data-bs-toggle="collapse"
        data-bs-target="#navbarNav"
        aria-controls="navbarNav"
        aria-expanded="false"
        aria-label="Toggle navigation"
      >
        <span class="navbar-toggler-icon" />
      </button>
      <div
        class="collapse navbar-collapse"
        id="navbarNav"
      >
        <ul class="navbar-nav">
          <li class="nav-item">
            <router-link
              class="nav-link"
              to="/connexion"
            >
              Connexion
            </router-link>
          </li>
          <li class="nav-item">
            <router-link
              class="nav-link"
              to="/articles"
            >
              <i class="bi bi-cart2" /> Articles
            </router-link>
          </li>
        </ul>
      </div>
    </div>
  </nav>
</template>

Information ❗

Il existe deux façons de préciser l'url de navigation. Soit, on utilise le nom de la page/route ou l'url en soit dans le paramètre to.

Dans ce cas-ci, on peut aussi spécifier des paramètres comme le nom (on n'a pas mis de id dans un article) d'un article pour aller dans les détails d'un article

Dans ListeArticles.vue, ajoutons un bouton pour aller dans les détails d'un article. Ajoutez/remplacer le contenu suivant dans le template de ListeArticles.vue :

vue
<template>
  <h2>{{ titre }}</h2>
  <ul>
    <li
        v-for="article in articles"
        :key="article"
    >
      <router-link
          class="nav-link"
          :to="{ path: '/articles/:nom', params: { nom: article } }"
      >
        {{ article }}
      </router-link>
    </li>
  </ul>
</template>

C'est important de matcher l'url de la route qu'on a définie dans le fichier router/index.ts avec la propriété path. Même chose pour le paramètre. Comme on a mis :nom dans l'url, il faut mettre params: { nom: article }.

Information ❗

Cependant, Vue recommande d'utiliser le nom de la route au lieu de spécifier le path, c'est plus pratique pour mettre à jour les routes dans le futur.

Croyez-moi c'est chiant quand ça l'arrive 😉.

Pour s'assurer que chaque route ait un nom, il suffit d'ajouter dans le fichier du router la propriété nom pour chacune des routes.

src/router/index.ts
typescript
const routes: Array<RouteRecordRaw> = [
    {
        path: '/connexion',
        name: 'connexion',
        component: PageConnexion,
    },
    {
        path: '/articles',
        name: 'articles',
        component: Articles,
    },
    {
        path: '/articles/ajouter',
        name: 'ajouter-articles',
        component: AjouterArticle,
    },
    {
        path: '/articles/:nom',
        name: 'article',
        component: ArticleDetails,
    },
    {
        path: '/',
        redirect: '/connexion',
        name: 'accueil'
    },
    {
        path: '/:catchAll(.*)',
        name: 'page-non-trouvee',
        component: PageNonTrouvee,
    },
];

Il sera maintenant possible de référencer les routes par leur nom dans le composant <RouterLink> au lieu de spécifier le path. Voici comment faire :

vue
<template>
  <h2>{{ titre }}</h2>
  <ul>
    <li
        v-for="article in articles"
        :key="article"
    >
      <router-link
          class="nav-link"
          :to="{ name: 'article', params: { nom: article } }"
      >
        {{ article }}
      </router-link>
    </li>
  </ul>
</template>

Accéder aux paramètres d'une route

C'est bien beau avoir des routes accessibles mais il faut aussi pouvoir accéder aux paramètres de ces routes pour pouvoir afficher les bonnes informations.

Par exemple, dans la route /articles/:nom, nous avons un paramètre nom qui correspond au nom de l'article dont nous voulons afficher les détails.

La première étape est de se créer une classe Article. Nous allons créer cette classe dans un dossier typings a la racine de src et y ajouter le fichier article.ts avec le contenu suivant :

typescript
/**
 * Représente un article à acheter avec son nom et prix
 */
export default class Article {
    nom: string;
    prix: number;

    /**
     * Construit un article
     * @param {string} nom - nom de l'article
     * @param {number} prix - prix de l'article
     */
    constructor(nom: string, prix: number) {
        this.nom = nom;
        this.prix = prix;
    }
}

Ensuite, on doit se créer une méthode dans notre ArticlesService pour récupérer le détail d'un article en fonction de son nom. Voici le service mis à jour avec l'objet Article et la méthode pour avoir des informations sur un article :

typescript
import Article from '@/typings/Article';

export default class ArticlesService {
    obtenirArticles(): Article[] {
        return [
            new Article('Bâton de baseball', 250),
            new Article('Gant de hockey', 120),
            new Article('Médaille du tournoi de mini-putt', 100_000_001),
        ]
    }

    chercher(nom: string): Article | undefined {
        return this.obtenirArticles().find((a) => a.nom === nom)
    }
}

Attention ⚠

Nous allons devoir changer le code dans ListeArticles.vue pour que les articles soient maintenant des objets de type Article au lieu de simples chaînes de caractères. Maintenant, nous allons devoir faire article.nom au lieu de juste article pour accéder au nom de l'article.

Maintenant, dans le composant ArticleDetails.vue, nous allons pouvoir accéder au paramètre nom de la route pour afficher les détails de l'article correspondant. Voici comment faire :

vue
<script setup lang="ts">
  import { useRoute } from 'vue-router';
  import ArticlesService from '@/services/articles.ts';

  const route = useRoute();

  const service = new ArticlesService();
  const article = service?.chercher(<string>route.params.nom);
</script>

<template>
  <h2>Détails d'un article</h2>
  <div>{{ article.nom }}</div>
  <div>{{ article.prix }}</div>
</template>

Pour obtenir le paramètre, nous utilisons route.params.nom qui est un Observable du router.

Pour obtenir l'observable, il faut déclarer qu'on veut utiliser la route const route = useRoute();. Ainsi, Vue n'a pas besoin de recharger le composant si l'identifiant change. Il n'a qu'à exécuter de nouveau la partie let article = service.chercher(route.params.nom);.

Information ❗

La propriété nom dans route.params.nom vient de la route que nous avions configurée (le paramètre :nom):

typescript
{path: 'articles/:nom', name: 'article' component: ArticleDetails},

On utilise les ? pour indiquer que service peut être undefined et éviter une erreur de compilation.

Quoi faire si article est null?

Cela est en effet possible que l'article soit null si l'utilisateur entre une url qui n'existe pas ou qui ne correspond à aucun article.

On va devoir alors utiliser une nouvelle directive, soit v-if et v-else pour afficher un message d'erreur si l'article est null et afficher les détails de l'article sinon.

vue
<template>
  <h2>Détails d'un article</h2>
  <div v-if="article">
    <div>{{ article.nom }}</div>
    <div>{{ article.prix }}</div>
  </div>
  <div v-else>
    Impossible de trouver l'article
  </div>
</template>

En gros, si la valeur de article existe, on affiche les détails de l'article. Sinon, on affiche un message d'erreur.

Magique, non!

Paramètres de requête

Il est aussi possible d'avoir des paramètres de requête dans les routes. Par exemple, on pourrait avoir une route /articles?categorie=sport pour afficher les articles de la catégorie sport.

Syntaxe

Les paramètres de requête se retrouvent après le ? et sont séparés par des &. Ici categorie est égal à sport.

Pour notre application, nous allons utiliser cela pour spécifier l'ordre des noms des articles dans les paramètres.

Ajoutez un type pour définir l'ordre dans typings/ordre.ts

typescript
export enum Ordre {
    asc = 'asc',
    desc = 'desc',
}

Modifiez le bouton de la barre de navigation pour envoyer le paramètre d'ordre (asc ou desc)

Dans NavBar.vue :

vue
<router-link
  class="nav-link"
  :to="{ name: 'articles', query: { ordre: Ordre.asc } }"
>
  <i class="bi bi-cart2"></i> Articles
</router-link>

Attention ⚠

N'oubliez pas d'importer Ordre dans la section script du fichier NavBar.vue -> import { Ordre } from '@/typings/ordre';

Information ❗

La propriété query à l'intérieur du :to sert à ajouter un objet représentant chacun des paramètres que l'on veut gérer.

Maintenant, dans le composant ListeArticles.vue, nous allons pouvoir accéder au paramètre d'ordre pour trier les articles en conséquence. Voici comment faire :

vue

<script setup lang="ts">
  import {useRoute} from 'vue-router';
  import {RouterLink} from 'vue-router';
  import {ref} from 'vue';
  import ArticlesService from '@/services/articles.ts';
  import type {Ordre} from '@/typings/ordre.ts';

  const titre = ref('Mes articles');
  const route = useRoute();

  const service = new ArticlesService();
  const articles = service.obtenirArticlesAvecOrdre(<Ordre>route.query.ordre);
</script>

Nous devons encore utiliser useRoute pour accéder aux paramètres de requête de la route. Ensuite, nous pouvons passer le paramètre d'ordre à notre service pour obtenir les articles dans l'ordre spécifié utilisable avec route.query.ordre qui est un Observable du router.

Il suffit d'ajouter la méthode obtenirArticlesAvecOrdre au service d'articles pour corriger l'erreur.

typescript
obtenirArticlesAvecOrdre(ordre: Ordre): Article[] {
    return this.obtenirArticles().sort((a, b) => {
      if (ordre === Ordre.asc) {
        return a.nom.localeCompare(b.nom);
      } else {
        return b.nom.localeCompare(a.nom);
      }
    });
}

Attention ⚠

N'oubliez pas d'importer Ordre dans le haut du fichier import { Ordre } from '@/typings/ordre';.

Pour tester, modifiez l'url pour articles?ordre=desc. L'ordre des articles devraient changer.

Props des composants

Vue peut passer les paramètres de la route par props. Ce sont des propriétés que la composante s'attend d'avoir de son parent ou du router.

Pour supporter cela, il faut ajouter la valeur props: true sur la route en question dans le router

typescript
const routes: Array<RouteRecordRaw> = [
  //...
  {
    path: '/articles/:nom',
    name: 'article',
    component: ArticleDetails,
    props: true
  },
  //...
];

À l'intérieur de ArticleDetails.vue, on peut obtenir la propriété avec defineProps au lieu d'utiliser le router

vue
<script setup lang="ts">
  import ArticlesService from '@/services/articles.ts';

  const props = defineProps({ nom: { type: String, required: true } });
  const service = new ArticlesService();
  const article = service?.chercher(props.nom);
</script>

<template>
  <h2>Détails d'un article</h2>
  <div v-if="article">
    <div>{{ article.nom }}</div>
    <div>{{ article.prix }}</div>
  </div>
  <div v-else>
    Impossible de trouver l'article
  </div>
</template>

Information ❗

C'est vraiment une question de préférence dans ce cas entre l'utilisation de defineProps et du router.

Parfois, il est pratique de naviguer via le JavaScript pour rediriger, par exemple s'il y a une erreur ou après un clic.

Dans ArticleDetails.vue, nous pouvons ajouter un bouton Retour pour revenir en arrière à la liste des articles.

Dans la section template ajoutez ce bouton.

html
<button 
    class="btn btn-primary" 
    @click="$router.push({ name: 'articles', query: { ordre: Ordre.asc } })"
  >
    Retour
</button>

Dans la section script, ajoutez l'importation de Ordre dans le haut import { Ordre } from '@/typings/ordre'

Information ❗

$router nous permet d'obtenir le router à l'intérieur du template pour faire la même chose ou presque qu'avec les router-link, mais en JavaScript.

Si on veut accéder au router directement dans la section script, par exemple avec une fonction, il faut importer l'observable du router

typescript
import { useRouter } from 'vue-router'
const router = useRouter()

/**
 * Retourner à la page des articles
 */
function retourPageArticles(){
  router.push({ name: 'articles', query: { ordre: Ordre.asc } })
}

Cela permet de simplifier le bouton en appelant la fonction retourPageArticles si on clique sur le bouton.

html
  <button
    class="btn btn-primary"
    @click="retourPageArticles"
  >
    Retour
  </button>

Truc 💡

Si vous avez plusieurs actions à faire, c'est mieux de faire le tout dans la section script avec une fonction.

Cela permet de réutiliser le même bout de code.

420-2W1-DM - Développement d'applications web 2