Développement d'un quizz avec VueJS 3

Mise en place

Dans cette capsule vidéo, on va construire une petite application de jeu qui va mettre en valeur les points les plus importants de la “Composition API” de Vue 3, et on va voir en quoi ça simplifie développement.

Dans notre application y aura un mode joueur qui proposera des questions de type quizz et qui gérera le score, et il y aura un mode admin qui permettra d’éditer l’ensemble des questions.

On va utiliser Vite pour l’outillage de développement. Vite va nous permettre de convertir dynamiquement les fichiers .vue en modules javascript, et de les servir dynamiquement au navigateur, de builder le projet quand il sera fini, et j’en passe

Pour ce qui concerne l’installation de Vite, je vous laisse aller sur la page officielle du projet Vite. –> (VISUEL LIEN) https://vitejs.dev

Donc j’ouvre un terminal, je me place dans le répertoire dans lequel je mets tous mes projets et pour créer un nouveau projet avec Vite, je tape:

npm init vite

Quand on utilise la commande ‘npm init’ avec un argument, le module ‘create-vite’ est chargé et exécuté, et il se trouve que c’est un scafolder de projet vite (demo)

Je vais appeler le projet ‘quizz’, je choisis ‘vue’, puis ‘vue-vanilla’. Voilà, un dossier ‘quizz’ a été créé, je me place dedans. Il faut installer les packages référencés dans package.json ; comme d’habitude il faut taper npm install et les packages sont chargés et stockés dans le répertoire node_modules/ (demo)

On peut lancer le serveur de dev en tapant npm run dev J’ouvre un navigateur sur localhost:6000 et voilà (demo)

Maintenant on va développer

Pour alterner entre les deux modes ‘joueur’ et ‘admin’, on pourrait utiliser le plugin vue-router, mais on va plus simplement utiliser un v-if / v-else

<template>
   <input type='checkbox' v-model="isAdminMode" /> Admin
   
   <Admin v-if="isAdminMode"></Admin>
   <Play v-else></Play>
</template>

<script setup>
import { ref } from 'vue'

import Admin from '/src/components/Admin.vue'
import Play from '/src/components/Play.vue'

const isAdminMode = ref(true)
</script>

Pour tester on va créer des vues Play.vue et Admin.vue minimales (demo)

‘isAdminMode’ est une variable ‘réactive’, c’est à dire qu’en la modifiant en JS elle est modifiée dans les vues où elle apparait, et en la modifiant dans l’interface des vues, ça modifie la valeur JS. C’est cette simplicité qu’apportent tous les framework réactifs comme Vue, React, Svelte, Angular et autres.

On va créer une section <script> pour mettre le code javascript, avec la syntaxe recommandée <script setup> En Vue2, il aurait fallu déclarer ‘isAdminMode’ dans une section ‘data’. C’était simple, mais impossible du coup de partager ces data avec une autre vue de façon simple.

Avec Vue3 et la composition API c’est plus simple et direct, pour créer un objet réactif, on utilise la fonction spéciale ref()

const isAdminMode = ref(true)

On donne en paramètre une valeur initiale, et c’est tout !

Si on regarde dans l’onglet Vue des devtools, voilà ce qu’on obtient (demo) Je vous rappelle que pour avoir cet onglet, il faut installer l’extension “VueJS Devtools” pour votre navigateur. On voit apparaitre ‘isAdminMode’ et on voit que c’est une ‘ref’, une variable réactive. Si je change la valeur de ‘isAdminMode’, on voit que la vue change, et si je clique sur la case, la valeur change. Ça ne ferait pas la même chose avec des valeurs ordinaires. Par exemple je peux déclarer un booléen ‘isAdmin’ et la référencer dans le template. Il apparait dans le devtools, mais il n’apparait pas comme une ref et je ne peux pas la changer.

Interface d’administration

Maintenant, on va commencer par gérer les questions de notre jeu, et on va faire ça dans la vue ‘admin’ On va d’abord créer un formulaire minimal pour saisir une nouvelle question (demo) Dans le template, on créé deux <input> avec les v-model ‘text’ et ‘answer’

   <input v-model="text" /> - Réponse : <input type="checkbox" v-model="answer" />

‘text’ et ‘answer’ sont également des variables réactives. Avec Vue3 et la composition API c’est plus simple et direct, pour créer un objet réactif, on utilise la fonction spéciale ref()

const text = ref('')
const answer = ref(false)

Comme tout à l’heure, on peut les voir dans les Devtools (demo)

Maintenant je peux rajouter un bouton ‘ajouter’ dans le template

<button @click="addQuestion">ajouter</button>

Dans le script, je commence par ajouter une ‘ref’ ‘questions’ qui contiendra la liste des questions et initialisée à la liste vide.

const questions = ref([])

Ensuite j’ajoute la fonction ‘addQuestion’ qui ajoute une nouvelle question à la liste

function addQuestion() {
   questions.value.push({
      text: text.value,
      answer: answer.value,
   })
}

En JS, la valeur d’une ‘ref’ x, c’est x.value, ça ne peut pas être x elle-même. x est objet complexe, qui contient non seulement sa valeur, mais des écouteurs à l’affut des changements, et d’autres choses encore.

Si on essaie, on voit qu’à chaque appui sur ‘ajouter’, une nouvelle question est ajoutée à ‘questions’. Pour voir en direct la liste, on peut créer une boucle v-for dans le template

   <div v-for="question in questions">
      {{ question.text }} {{ question.answer }}
   </div>

On va rajouter un bouton ‘supprimer’ en face de chaque question

   <div v-for="question in questions">
      {{ question.text }} {{ question.answer }} <button @click="deleteQuestion(question)">supprimer</button>
   </div>

et on ajoute la fonction ‘deleteQuestion’

const deleteQuestion = (question) => {
   questions.value = questions.value.filter(q => q !== question)
}

La fonction on l’écrit comme on veut par exemple ici avec une ‘fat arrow’ et une affectation On utilise filter pour ne garder que les éléments différents de la question qu’on supprime

Si on essaie, on a une petite interface d’admin fonctionnelle.

Partage d’état

Maintenant je voudrais jouer au jeu sur la vue Play, mais comment faire pour récupérer la liste ‘questions’ ? Pour ceux qui ont fait du Vue2, c’était un problème difficile, puisque questions aurait été dans une section data de la vue Admin et n’aurait pas pu être mis dans un module. On aurait utilisé des mixins, ou un gestionnaire d’état comme Vuex. Bref, des choses compliquées, pour un besoin somme toutes simple ! Avec Vue3 et la Composition API, maintenant qu’un objet réactif est créé simplement par l’appel de la fonction ref() on peut mettre ce code dans un module, qu’on importera partout où on en a besoin.

Donc on va créer un module, qu’on va appeler ‘useQuestions’ et qu’on va mettre dans un répertoire ‘use’ et on va y mettre tout le code métier relatif aux questions

import { ref } from 'vue'

export const questions = ref([])

export function addQuestion(text, answer) {
   questions.value.push({
      text,
      answer,
   })
}
export function deleteQuestion(question) {
   questions.value = questions.value.filter(q => q !== question)
}

Ensuite il suffit d’importer le module

<script setup>
import { ref } from 'vue'
import { questions, addQuestion, deleteQuestion } from '/src/use/useQuestions'

const text = ref('')
const answer = ref(false)
</script>

Maintenant on va vérifier que questions est bien accessible dans la vue ‘Play’. Pour ça on peut créer la vue minimale :

<template>
   {{ questions }}
</template>

<script setup>
import { questions } from '/src/use/useQuestions'
</script>

On voit que ‘questions’ est bien conservé d’une vue à l’autre.

Il y a quelque chose d’essentiel à comprendre pour bien saisir comment ça marche. Lors de l’exécution de “import { questions } from ‘/src/use/useQuestions’”, le code du module N’EST EXÉCUTÉ QU’UNE SEULE FOIS, lors du premier import, c’est une caractéristique des modules JS. Lors des imports ultérieurs, on récupère les mêmes objets ‘questions’ etc., sans que le code soit réexécuté, donc sans réinitialisation de ‘questions’, l’objet ‘questions’ est donc exactement le même dans Play.vue et dans Admin.vue, il est partagé entre les deux vues. Comme il est réactif, tout changement dans l’un va se voir dans l’autre.

Si vous avez fait du Vue2, partager les objets réactifs entre plusieurs vues était impossible directement. On utilisait générallement un module de gestion d’état centralisé comme Vuex. Avec Vue3 et la “Composition API” on peut choisir quels objets réactifs on veut partager entre les vues, comme ‘questions’, en les mettant dans des modules comme ‘useQuestions’ Vue3 et la “Composition API” encourage l’organisation du code en modules ‘useTruc’ qui encapsulent toute la logique métier de l’aspect ‘Truc’, et qui stockent éventuellement dans un bout “d’état” des valeurs comme ‘questions’

Par ailleurs, cela encourage l’écriture de modules “use quelque chose” génériques. Par exemple il existe l’excellent VueUse. –> (VISUEL LIEN) https://vueuse.org

Par exemple on peut ajouter très simplement un affichage de l’état de charge du device : (demo avec useBattery / charging)

On va se servir d’un autre module de vueuse pour quelque chose de plus utile, la persistance des données lors d’un rechargement de page. Actuellement si on recharge la page, on perd toutes nos données puisque tous les scripts sont réexécutés, et les ‘ref’ reprennent leurs valeurs par défaut. On va utiliser useLocalStorage pour associer ces ‘ref’ (qui constituent l’état de l’application) à des clé/valeur de LocalStorage. LocalStorage, je vous le rappelle, c’est un espace de stockage dans le navigateur, qui est commun à tous les onglets, et qui survit même après fermeture du navigateur.

import { useLocalStorage } from '@vueuse/core'

useLocalStorage('questions', questions)

Lorsque ce code est réexécuté à la suite d’un rechargement, l’exécution de ‘const question = ref([])’ remet brièvement questions à la liste vide, mais ensuite l’exécution de ‘useLocalStorage(’questions’, questions)’ charge questions avec la valeur stockée dans LocalStorage, qui elle n’a pas été effacée.

Jouer

Cette quatrième et dernière partie est optionnelle, puisque les notions essentielles sur le partage de code et le partage d’état ont déjà été présentées. Mais puisque j’ai promis au début le développement d’une petite application de type quizz complète, on va terminer rapidement le mode ‘joueur’, toujours en utilisant la “Composition API”

On va avoir besoin de différentes variables réactives :

const currentQuestion = ref()  --> qui sera la question choisie
const score = ref(0)

On va créer une fonction newQuestion qui provoquera le tirage aléatoire d’une question de la liste des questions

<button @click="newQuestion">nouvelle question</button>
...
const currentQuestion = ref()

const newQuestion = () => {
   const index = Math.floor(Math.random() * questions.value.length)
   currentQuestion.value = questions.value[index]

Attention à ne pas oublier ‘.value’ côté script, pour avoir accès à la valeur d’une variable réactive.

Ensuite il faut poser la question

   <template v-if="currentQuestion">
      <p>Vrai ou faux ?</p>
      {{ currentQuestion.text }} - c'est vrai : <input type="checkbox" v-model="playerAnswer" />
      <br/>
      <button @click="checkAnswer">vérifier</button>
   </template>

Pour finir la fonction checkAnswer qui vérifie la réponse et qui passe à la question suivante :

const checkAnswer = () => {
   if (playerAnswer.value === currentQuestion.value.answer) {
      alert("Bonne réponse")
      score.value += 1
   } else {
      alert("Mauvaise réponse")
   }
   newQuestion()
}

On constate un problème : score revient à 0 lorsqu’on change de mode. C’est dû au fait que le setup est ré-exécuté, à cause du v-if Une solution, c’est de mettre score dans l’état partagé, c’est à dire dans le module useQuestions

Conclusion

Pour conclure cette capsule, on a pu réaliser une petite application avec très peu de lignes de code. On peut placer toute la logique métier d’un aspect dans un module JS ordinaire tel que useQuestions, qu’on peut importer dans chaque vue qui en a besoin. Si dans ces modules il y a des objets réactifs avec ‘ref’, ils seront partagés par toutes les vues qui importent ce module, ce qui permet une gestion modulaire de l’état global de l’application, sans nécessairement faire appel à un plugin spécial.