Aller au contenu principal

Séminaire

Objectifs

  • Faire un projet avec Vue.js.

Rendu

Mise en place

  • Cloner le nouveau dépôt GitHub Classroom dans le répertoire du cours.
  • Ouvrir le dossier du dépôt dans Visual Studio Code.

Estimation

  • Créer un ficher report.md dans le dossier du dépôt.
  • Estimer le temps nécessaire pour réaliser ce travail dans le rapport.
    • Découper le travail en tâches pour faciliter l'estimation.
  • Une fois terminé, comparer le temps estimé avec le temps réellement passé.
TâcheTemps estiméTemps passéCommentaire
Estimation10m15m...
............
Total2h1h30...

Semaine 1

Vue.js

  • Cloner le nouveau dépôt Git dans le répertoire du cours.
  • Ouvrir le répertoire du dépôt Git dans Visual Studio Code.
  • Créer le fichier report.md.
  • Créer un projet Vue.js depuis le répertoire du dépôt : npm create vue@latest
    • Project name: quiz
    • Use TypeScript? Yes
    • Select features to include in your project:
      • Router (SPA development)
      • Linter (error prevention)
      • Prettier (code formatting)
    • Select experimental features to include in your project:
      • Rien sélectionner
    • Skip all example code and start with a blank Vue project? Yes
  • Vérifier où se trouve le projet :
    ./
    ├── .github/
    ├── quiz/
    └── ...
    ├── README.md
    └── report.md
  • Copier tous les fichiers du projet Vue.js (dossier quiz) dans le répertoire du dépôt Git (dossier sem07-app-{pseudo}) :
    ./
    ├── .github/
    ├── .vscode/
    ├── public/
    ├── src/
    ├── .editorconfig
    ├── .gitattributes
    ├── .gitignore
    ├── .oxlintrc.json
    ├── .prettierrc.json
    ├── env.d.ts
    ├── eslint.config.js
    ├── index.html
    ├── package.json
    ├── README.md
    ├── report.md
    ├── tsconfig.app.json
    ├── tsconfig.json
    ├── tsconfig.node.json
    └── vite.config.ts
  • Pour le fichier README.md, copier le contenu du projet Vue.js dans le fichier README.md du dépôt Git, puis supprimer le fichier README.md du projet Vue.js.
  • Supprimer le dossier quiz (qui devrait être vide).
  • Installer les dépendances et formater le code :
    npm install
    npm run format
  • Pour lancer le projet en mode développement :
    npm run dev
  • Ouvrir le navigateur à l'adresse indiquée pour voir le projet.

Bootstrap

  • Installer Bootstrap et Bootstrap Icons :
    npm install bootstrap @popperjs/core bootstrap-icons
  • Changer la langue et le titre de l'application en modifiant index.html :
    <!doctype html>
    <html lang="fr">
    <head>
    ...
    <title>Quiz</title>
    </head>
    ...
    </html>
  • Dans eslint.config.ts, remplacer pluginVue.configs['flat/essential'] par pluginVue.configs['flat/recommended'].
  • Créer les dossiers src/components, src/assets et src/views (dans le dossier src existant).
  • Créer ou modifier les fichiers suivants :
src/main.ts
import "bootstrap-icons/font/bootstrap-icons.min.css";
import "bootstrap/dist/css/bootstrap.min.css";
import "./assets/main.css";

import "bootstrap";
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";

const app = createApp(App);

app.use(router);

app.mount("#app");
  • Vérifier que l'application fonctionne correctement.
  • Ne pas oublier de faire régulièrement des commits à chaque fois qu'on a un état stable du projet (code fonctionnel, comme ici).
  • Déployer le projet sur GitHub Pages (voir la section Déploiement).
  • Tester le projet (voir la section Test).

Quiz

  • Modifier le quiz pour qu'il y ait deux-trois questions à choix multiples. Voici quelques idées :
    • De quelle couleur est le cheval blanc de Napoléon ?
    • Combien de pattes a un chat ?
    • Quelle est la capitale de la Suisse ?
  • Proposer quatre réponses possibles pour chaque question.
  • Afficher le score à la fin du quiz.
    • Mettre la logique du calcul dans la fonction submit, juste après le event.preventDefault();
    • Une fois le score calculé, l'afficher dans l'alerte à la place de la réponse choisie.
    • Afficher le score en pourcentage ou en nombre de bonnes réponses sur le nombre total de questions.
  • Afficher un message de félicitations si le score est parfait.
    • Afficher un message différent selon le score.
  • Ajouter un bouton pour réinitialiser le quiz.
    • Ajouter un bouton dans QuizForm.vue :
      <button class="btn btn-secondary" @click="reset">Réinitialiser</button>
    • Le bouton va appeler une fonction reset qu'il faudra créer.
  • Modifier la couleur des .btn-primary dans main.css.
  • Changer les icônes dans la bar de navigation (en haut) en utilisant Bootstrap Icons.

Semaine 2

QuestionRadio

C'est fastidieux de devoir répéter les mêmes étapes pour chaque question. On va donc créer un composant pour les questions : QuestionRadio.vue.

Commencer par définir comment on voudrait que le composant fonctionne. On pourrait vouloir remplacer chaque question par un composant QuestionRadio :

src/components/QuizForm.vue
<template>
<form>
<QuestionRadio
id="cheval"
v-model="cheval"
text="De quelle couleur est le cheval blanc de Napoléon ?"
:options="[
{ value: 'blanc', text: 'Blanc' },
{ value: 'brun', text: 'Brun' },
{ value: 'noir', text: 'Noir' },
]"
/>
...
</form>
</template>
  • Le composant QuestionRadio doit recevoir les propriétés suivantes :
    • v-model : la valeur de la réponse (bi-directionnel, car on veut pouvoir modifier la réponse depuis le composant parent lorsqu'on clique sur le bouton "Réinitialiser" et récupérer la réponse depuis le composant parent pour calculer le score).
    • id : un identifiant unique pour le groupe de boutons radio.
    • text : le texte de la question.
    • options : un tableau d'objets pour les options de réponse. Chaque objet doit avoir une propriété value pour la valeur de la réponse et une propriété text pour le texte affiché de l'option.
  • Ne pas oublier d'importer le nouveau composant dans QuizForm.vue :
    <script setup lang="ts">
    import QuestionRadio from "@/components/QuestionRadio.vue";
    ...
    </script>

Créer le fichier QuestionRadio.vue dans le dossier src/components :

src/components/QuestionRadio.vue
<script setup lang="ts">
import { type PropType } from "vue";

const model = defineModel<string | null>();
const props = defineProps({
id: { type: String, required: true },
text: { type: String, required: true },
options: {
type: Array as PropType<Array<{ value: string; text: string }>>,
required: true,
},
});
</script>

<template>
{{ props.text }}
<div v-for="option in props.options" :key="option.value" class="form-check">
<input
:id="`${props.id}-${option.value}`"
v-model="model"
class="form-check-input"
type="radio"
:name="props.id"
:value="option.value"
/>
<label class="form-check-label" :for="`${props.id}-${option.value}`">
{{ option.text }}
</label>
</div>
</template>
  • Dans la partie <script>, on utilise les fonctions defineModel et defineProps pour définir le modèle (v-model) et les propriétés (text, name, options) du composant.
  • Dans la partie <template> :
    • On affiche le texte de la question : {{ props.text }}.
    • On affiche les options de réponse en utilisant une boucle v-for sur props.options : le <div> sera répété pour chaque option.
    • La différence entre les attributs qui commencent par : et ceux qui ne commencent pas par : est que les premiers sont des expressions JavaScript (interprétées) et les seconds sont des chaînes de caractères (non interprétées).
Question rapport

Quelle est la différence entre un prop et un modèle (v-model) ?

QuestionText

De manière similaire, créer un composant QuestionText.vue pour les questions à réponse textuelle libre. Voici un code qu'on voudrait extraire dans le composant QuestionText.vue :

src/components/QuizForm.vue
<label for="exampleFormControlInput" class="form-label">
Combien de pattes a un chat ?
</label>
<input
id="exampleFormControlInput"
v-model="reponse"
class="form-control"
placeholder="Veuillez saisir un nombre"
/>

Et on souhaiterait le remplacer avec un nouveau composant QuestionText.vue comme ceci dans QuizForm.vue :

src/components/QuizForm.vue
<QuestionText
id="chat"
v-model="reponse"
text="Combien de pattes a un chat ?"
placeholder="Veuillez saisir un nombre"
/>
Question rapport

Comment rendre la propriété placeholder optionnelle ?

Documentation : Vue.js + Bootstrap.

API

Open Trivia Database est une API qui fournit des questions de quiz. On va utiliser son API pour obtenir des questions aléatoires pour notre quiz :

On peut obtenir des questions en faisant une requête HTTP GET à l'URL suivante : https://opentdb.com/api.php?amount=3&type=multiple

  • amount : le nombre de questions à obtenir.
  • type : le type de questions (multiple ou boolean).

Ajouter une nouvelle tab Trivia dans App.vue :

src/App.vue
...
<ul class="navbar-nav">
<li class="nav-item">
<RouterLink class="nav-link" to="/trivia">
<i class="bi bi-question"></i>
Trivia
</RouterLink>
</li>
...
</ul>
...

Créer une nouvelle vue TriviaView.vue dans le dossier src/views :

src/views/TriviaView.vue
<script setup lang="ts">
import QuizTrivia from "@/components/QuizTrivia.vue";
</script>

<template>
<div class="container mt-3">
<h1>Trivia</h1>
<QuizTrivia />
Source :
<a href="https://opentdb.com/" target="_blank">Open Trivia Database</a>
</div>
</template>

Mettre à jour le fichier router/index.ts en ajoutant une nouvelle route :

src/router/index.ts
...
const router = createRouter({
history: createWebHashHistory(import.meta.env.BASE_URL),
routes: [
...
{
path: '/trivia',
name: 'trivia',
component: () => import('../views/TriviaView.vue'),
},
]
...

Finalement ajouter le composant QuizTrivia.vue dans le dossier src/components :

src/components/QuizTrivia.vue
<script setup lang="ts">
import QuestionRadio from "@/components/QuestionRadio.vue";
import { reactive, ref } from "vue";

const questions = ref<
{
question: string;
correct_answer: string;
incorrect_answers: string[];
}[]
>([]);
const answers = reactive<{ [key: number]: string | null }>({});

fetch("https://opentdb.com/api.php?amount=3&type=multiple")
.then((response) => response.json())
.then((data) => (questions.value = data.results));
</script>

<template>
<form>
<QuestionRadio
v-for="(question, index) in questions"
:id="index.toString()"
:key="index"
v-model="answers[index]"
:text="question.question"
:options="[
{ value: question.correct_answer, text: question.correct_answer },
...question.incorrect_answers.map(answer => ({
value: answer,
text: answer,
})),
]"
/>
</form>
</template>

À sa création, ce composant va récupérer 3 questions avec l'API (https://opentdb.com/api.php?amount=3&type=multiple) et stocker les questions dans la ref questions. Ensuite, on affiche chaque question avec le composant QuestionRadio (avec une boucle v-for) en passant les propriétés nécessaires.

QuestionCheckbox (bonus)

Les checkboxes sont comme les radios, mais on peut en sélectionner plusieurs. Créer un composant QuestionCheckbox.vue pour les questions à choix multiples. Voici un exemple d'utilisation :

src/components/QuizForm.vue
<div class="form-check">
<input
id="checkboxJane"
v-model="checkedNames"
class="form-check-input"
type="checkbox"
value="Jane"
/>
<label class="form-check-label" for="checkboxJane">Jane</label>
</div>
<div class="form-check">
<input
id="checkboxJohn"
v-model="checkedNames"
class="form-check-input"
type="checkbox"
value="John"
/>
<label class="form-check-label" for="checkboxJohn">John</label>
</div>

Noter que comme la réponse est une liste, il faut initialiser la ref avec une liste vide :

const checkedNames = ref<string[]>([]);

Documentation : Vue.js + Bootstrap.

Améliorations

Ce projet reprend les deux premières semaines de celui de l'année dernière. Retrouver d'autres améliorations possibles dans les semaines suivantes.

Voici quelques idées supplémentaires pour aller plus loin :

  • QuestionCheckbox.vue : Sélectionner plusieurs réponses.
  • QuestionSelect.vue : Sélectionner une réponse dans une liste déroulante.
  • Accepter plusieurs réponses possibles pour QuestionText.vue (par exemple, "2" ou "deux").
  • Adapter le Trivia pour pouvoir y jouer.
  • Ordre aléatoire des choix dans QuestionRadio.vue.
  • Ordre aléatoire des questions.
  • Paramétrer les questions de l'API.
    • Choisir le nombre de questions, la catégorie, la difficulté, le type, …

Comme les améliorations ne demandent pas toutes le même travail, elles seront pondérées différemment selon leur complexité (par exemple, l'ordre aléatoire des choix est simple donc il ne comptera que pour 0.5 pour le critère).

Question rapport

Indiquer les améliorations réalisées :

  • Pourquoi les avoir choisies ?
  • Comment les avoir réalisées (problèmes rencontrés, solutions trouvées, …) ?

Quelles améliorations supplémentaires pourraient être réalisées ?

Déploiement

Comme pour le générateur de site statique, on a besoin de compiler le projet avant de le déployer.

Sur la page GitHub du dépôt, aller dans Settings > Pages > Sous Build and deployment puis Source, sélectionner GitHub Actions.

Créer un fichier .github/workflows/deploy.yml avec le contenu suivant :

.github/workflows/deploy.yml
# https://vite.dev/guide/static-deploy.html#github-pages
name: Deploy static content to Pages

on:
push:
branches:
- main
workflow_dispatch:

permissions:
contents: read
id-token: write
pages: write

concurrency:
group: "pages"
cancel-in-progress: true

jobs:
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Set up Node
uses: actions/setup-node@v6
with:
node-version: lts/*
cache: npm
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Setup Pages
uses: actions/configure-pages@v6
- name: Upload artifact
uses: actions/upload-pages-artifact@v4
with:
path: "./dist"
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

Ajouter le lien du site dans le rapport.

Le lien renvoie vers une page blanche. Pour corriger cela, modifier le fichier vite.config.ts pour ajouter le chemin de base (nom du dépôt GitHub) :

vite.config.ts
import vue from "@vitejs/plugin-vue";
import { fileURLToPath, URL } from "node:url";
import { defineConfig } from "vite";
import vueDevTools from "vite-plugin-vue-devtools";

// https://vite.dev/config/
export default defineConfig({
base: "/sem07-app-{remplacer-pseudo}/",
plugins: [vue(), vueDevTools()],
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
},
},
});
Question rapport

Pourquoi cette erreur ? Comment la solution proposée la corrige-t-elle ?

Lorsqu'on change de page et qu'on la rafraîchit, on obtient une erreur 404. Pour corriger cela, remplacer createWebHistory par createWebHashHistory dans le fichier src/router/index.ts :

src/router/index.ts
import { createRouter, createWebHashHistory } from 'vue-router'

const router = createRouter({
history: createWebHashHistory(import.meta.env.BASE_URL),
routes: [
...
],
})
...
Question rapport

Pourquoi cette erreur ? Comment la solution proposée la corrige-t-elle ?

Test

Exécuter la commande npm install -D eslint-plugin-prettier.

Ajouter Prettier dans ESLint en modifiant le fichier eslint.config.ts (parfois caché sous package.json) :

eslint.config.ts
...
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'

export default defineConfigWithVueTs(
...
skipFormatting,
eslintPluginPrettierRecommended,
)

Créer un fichier test.js à la racine du dépôt avec le contenu suivant :

test.js
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import { execSync } from "child_process";

test("présence des fichiers et dossiers", () => {
// Liste des chemins attendus dans le projet.
const expectedPaths = [
".git",
".github/workflows/deploy.yml",
".gitignore",
".prettierrc.json",
"eslint.config.ts",
"index.html",
"package.json",
"README.md",
"report.md",
"src/App.vue",
"src/main.ts",
"src/router/index.ts",
"test.js",
"tsconfig.json",
"vite.config.ts",
];
// Vérifie que chaque chemin existe dans le projet.
expectedPaths.forEach((path) => {
assert.ok(
fs.existsSync(path),
`Le fichier ou dossier ${path} doit exister.`,
);
});
});

test("validation des fichiers", () => {
// Exécute la validation en utilisant https://eslint.org/
try {
const output = execSync("npx eslint").toString();
console.log(output);
} catch (error) {
assert.fail(error.output);
}
});

test("construction du site", () => {
try {
const output = execSync("npm run build").toString();
console.log(output);
} catch (error) {
assert.fail(error.output);
}
});

Ajouter le script test dans le fichier package.json pour pouvoir exécuter les tests avec la commande npm test :

package.json
{
...
"scripts": {
...
"test": "node --test"
}
}

Vérifier que les tests passent avec la commande npm test et corriger le code si cela n'est pas le cas.

Rapport

  • Expliquer brièvement les principales difficultés rencontrées et comment les résoudre.
  • Compléter les estimations.
  • Ajouter une auto-évaluation du projet en utilisant les critères d'évaluation.
report.md
# Rapport

...

## Auto-évaluation

| Critère | Auto-évaluation (0, ½ ou 1) | Commentaire |
| ------------------------------------------------------------------------------------ | --------------------------- | ----------- |
| Le rendu est correct avec un journal de bord complet. | | |
| Le journal de bord est bien structuré et synthétique. | | |
| L'application a les mêmes fonctionnalités que l'exemple. | | |
| L'application est personnalisée. | | |
| L'application a plus de fonctionnalités que l'exemple. | | |
| L'application a encore plus de fonctionnalités que l'exemple. | | |
| Bootstrap est correctement utilisé pour rendre l'application responsive. | | |
| Le code suit les conventions de codage (formatage, nommage, organisation, &hellip;). | | |
| Le code est lisible et maintenable (nommage, commentaires, &hellip;). | | |
  • Pousser tous les changements sur GitHub.
  • Copier le lien du dépôt GitHub dans le devoir sur Moodle.

Aides

Nettoyage et vérification

  • Vérifier tout le projet et nettoyer les codes qui ne sont plus utilisés.
  • Vérifier que le code est correct localement, on peut construire le projet :
    npm run build
  • Vérifier que le site est correct en ligne et fonctionne sur différents appareils (ordinateur, téléphone, …).
  • Vérifier que le rendu du rapport est correct.

Documentations

S'aider des documentations officielles pour réaliser le projet :

Code d'exemple :

Évaluation

L'évaluation du projet se portera sur les critères suivants :

  • Rapport
    • Le rendu est correct avec un journal de bord complet.
    • Le journal de bord est bien structuré et synthétique.
  • Fonctionnalités
    • L'application a les mêmes fonctionnalités que l'exemple.
    • L'application est personnalisée.
    • L'application a plus de fonctionnalités que l'exemple.
    • L'application a encore plus de fonctionnalités que l'exemple.
    • Bootstrap est correctement utilisé pour rendre l'application responsive.
  • Code
    • Le code suit les conventions de codage (formatage, nommage, organisation, …).
    • Le code est lisible et maintenable (nommage, commentaires, …).
Note 1  2 2.5 3 3.5 4 4.5 5 5.5 6 
Nombre de critères validés0123456789
  • En gras : critères principaux.
  • En italique : critères secondaires.