g) Le routage des écrans
C'est quoi le routage d'écrans ?
Lorsqu'on parle d'une IHM, nous souhaitons généralement afficher différents écrans en réagissant aux actions des utilisateurs.
Le routage (ou routing en anglais) est ce qui rend possible l'affichage de différents écrans.
Il n'y a pas de normes imposant un système de routage, il existe une multitude de façon d'afficher différents écrans, soit :
- une simple gestion de wrappers et de génération dynamique d'HTML ; c'est l'option la plus légère qui a été sélectionnée pour ce cours.
- via l'utilisation de Web Components API [R.41] ; façon intéressante de faire du web qui demanderait un cours dédié à cette technologie ; dès lors cela n'a pas été sélectionné comme technologie pour ce cours de JS ;
- via l'utilisation d'un langage de templating : Handlebars [R.42], Mustache… ; cette technologie n'est pas appropriée lorsque l'on souhaite réagir à beaucoup d'événements différents.
- via l'utilisation de librairies légères, comme par exemple Lit [R.43] ou Underscore [R.44] ; nous préférons l'idée du Vanilla JS pour ce cours-ci plutôt que de nous lier à une librairie supplémentaire.
- via l'utilisation de frameworks / librairies : React, Vue, Angular, tous ces frameworks mettent à disposition des librairies pour gérer le routage. Néanmoins, le choix de départ fait pour ce cours est de ne pas utiliser de framework pour la création de nos IHM.
Nous allons voir ensemble comment via une simple gestion de wrapper et de génération dynamique d'HTML nous pouvons assurer le routage de nos écrans.
Ajout d'une Navbar
Nous souhaitons créer une Navbar
à l'aide de JS et de Bootstrap afin de pouvoir afficher différentes pages : HomePage
, LoginPage
ou RegisterPage
.
Dans un premier temps, nous allons créer les deux nouvelles pages.
Pour continuer notre tutoriel (dans le répertoire structured
), veuillez compléter le code de /src/Component/Pages/Login.js
:
jsimport { clearPage, renderPageTitle } from '../../utils/render';const LoginPage = () => {clearPage();renderPageTitle('Login');renderRegisterForm();};function renderRegisterForm() {const main = document.querySelector('main');const form = document.createElement('form');form.className = 'p-5';const username = document.createElement('input');username.type = 'text';username.id = 'username';username.placeholder = 'username';username.required = true;username.className = 'form-control mb-3';const password = document.createElement('input');password.type = 'password';password.id = 'password';password.required = true;password.placeholder = 'password';password.className = 'form-control mb-3';const submit = document.createElement('input');submit.value = 'Login';submit.type = 'submit';submit.className = 'btn btn-danger';form.appendChild(username);form.appendChild(password);form.appendChild(submit);main.appendChild(form);}export default LoginPage;
Veuillez compléter le code de /src/Component/Pages/Register.js
:
jsimport { clearPage, renderPageTitle } from '../../utils/render';const RegisterPage = () => {clearPage();renderPageTitle('Register');renderRegisterForm();};function renderRegisterForm() {const main = document.querySelector('main');const form = document.createElement('form');form.className = 'p-5';const username = document.createElement('input');username.type = 'text';username.id = 'username';username.placeholder = 'username';username.required = true;username.className = 'form-control mb-3';const password = document.createElement('input');password.type = 'password';password.id = 'password';password.required = true;password.placeholder = 'password';password.className = 'form-control mb-3';const submit = document.createElement('input');submit.value = 'Register';submit.type = 'submit';submit.className = 'btn btn-danger';form.appendChild(username);form.appendChild(password);form.appendChild(submit);main.appendChild(form);}export default RegisterPage;
Avant d'afficher une page, nous nous assurons de faire un "clear" du "wrapper" utilisé pour afficher les pages (main
). Comme nous allons utiliser le "clear" dans de multiples pages, afin d'éviter des duplications de code, veuillez ajouter un nouveau module render.js
dans un nouveau répertoire /src/utils
. De plus, nous allons y ajouter une fonction permettant de donner un titre à une page.
Voici le code de /src/utils/render.js
:
jsconst clearPage = () => {const main = document.querySelector('main');main.innerHTML = '';};const renderPageTitle = (title) => {if (!title) return;const main = document.querySelector('main');const pageTitle = document.createElement('h4');pageTitle.innerText = title;main.appendChild(pageTitle);};export { clearPage, renderPageTitle };
Afin de pouvoir naviguer entre les pages, nous allons créer la Navbar
.
Veuillez compléter /src/Component/Navbar/Navbar.js
:
js// eslint-disable-next-line no-unused-varsimport { Navbar as BootstrapNavbar } from 'bootstrap';import HomePage from '../Pages/HomePage';import LoginPage from '../Pages/LoginPage';import RegisterPage from '../Pages/RegisterPage';const Navbar = () => {renderNavbar();onNavBarClick();};function renderNavbar() {const navbar = document.querySelector('#navbarWrapper');navbar.innerHTML = `<nav class="navbar navbar-expand-lg navbar-light bg-danger"><div class="container-fluid"><a class="navbar-brand" href="#">e-Pizzeria</a><buttonclass="navbar-toggler"type="button"data-bs-toggle="collapse"data-bs-target="#navbarSupportedContent"aria-controls="navbarSupportedContent"aria-expanded="false"aria-label="Toggle navigation"><span class="navbar-toggler-icon"></span></button><div class="collapse navbar-collapse" id="navbarSupportedContent"><ul class="navbar-nav me-auto mb-2 mb-lg-0"><li class="nav-item"><a class="nav-link active" aria-current="page" href="#">Home</a></li><li id="loginItem" class="nav-item"><a class="nav-link" href="#">Login</a></li><li id="registerItem" class="nav-item"><a class="nav-link" href="#">Register</a></li></ul></div></div></nav>`;}function onNavBarClick() {const navItems = document.querySelectorAll('.nav-link');navItems.forEach((item) => {item.addEventListener('click', (e) => {console.log(`click on ${e.target.innerHTML} navbar item`);if (e.target.innerHTML === 'Home') {HomePage();} else if (e.target.innerHTML === 'Login') {LoginPage();} else if (e.target.innerHTML === 'Register') {RegisterPage();}});});}export default Navbar;
La gestion du routage de la bonne page, c'est-à-dire l'affichage de la page correspondant à l'élément de la Navbar
qui a été cliqué, est simple.
Lors d'un clic sur un élément de la Navbar
, on identifie cet élément grâce au texte associé à celui-ci (via e.target.innerHTML
).
Ensuite, la fonction associée à la page est appelée.
Par exemple, pour rendre la page de login, il suffit d'appeler LoginPage()
du module /src/Components/Pages/LoginPage
.
Finalement, nous allons appeler la Navbar au sein d'index.js
:
jsimport 'bootstrap/dist/css/bootstrap.min.css';import './stylesheets/main.css';import 'animate.css';import HomePage from './Components/Pages/HomePage';import Header from './Components/Header/Header';import Footer from './Components/Footer/Footer';import Navbar from './Components/Navbar/Navbar';Header();Navbar();HomePage();Footer();
Attention, il faut aussi faire un "clear" de la HomePage
avant de rendre le menu et la carte des boissons. Veillez donc à mettre à jour HomePage.js
pour le faire, sinon lorsque vous passez de LoginPage
à la HomePage
, vous allez avoir des éléments de UI qui s'additionnent.
Si tout fonctionne bien, faites un commit
de votre repo (web2
) avec comme message : structured-hmi tutorial
.
En cas de souci, vous pouvez accéder au code de cette étape du tutoriel ici : structured-hmi.
Le site de la pizzeria est fonctionnel, nous pouvons facilement naviguer entre les pages.
Néanmoins, il nous manque certaines fonctionnalités importantes.
💭 Mais qu'est-ce qui nous manque ?
Voici ce qui fait défaut :
- Si nous faisons un refresh de la page, nous perdons la page en cours. Par exemple, si nous sommes sur
LoginPage
, nous serons redirigé sur laHomePage
. - Nous n'avons pas d'historique des pages visitées, nous ne pouvons pas revenir en arrière et en avant dans le temps.
- Nous n'avons pas une URL spécifique pour chaque écran.
Nous allons donc mettre en place en routeur afin de bénéficier des fonctions manquantes.
Mise en place de quel routeur ?
Nous souhaitons mettre en place en routeur permettant ces 4 fonctionnalités :
- Routage lors d'un clic sur un élément de la Navbar : on fera appel au composant fonctionnel associé à l'élément cliqué, on affichera dans le browser l'URL associée à ce composant et l'on gardera l'URL dans l'historique.
- Routage lors du chargement de l'IHM : on fera appel au composant fonctionnel associé à l'URL en cours.
- Routage lors de l'utilisation de l'historique du browser : on fera appel au composant fonctionnel associé à l'URL présente au sommet d'une pile représentant l'état du browser.
- Routage lors d'une redirection vers une URL : on fera appel au composant fonctionnel associé à la redirection, on affichera dans le browser l'URL associée à l'élément redirigé et l'on gardera l'URL dans l'historique.
La mise en place d'un routeur pourrait se faire à l'aide de librairies comme page.js, Vaadin Router...
Dans le cadre de ce cours, pour la partie IHM, nous estimons qu'il est plus intéressant de comprendre les concepts associés à un routeur que d'utiliser une librairie existante.
Dans la suite de ce chapitre, nous allons donc créer ensemble un routeur, qui sera inspiré de :
How I Implemented my own SPA Routing System in Vanilla JS [R.45].
Ne ne vous demandons pas d'être capable de créer un routeur par vous-même.
Par contre, nous souhaitons que vous compreniez chaque ligne de code et que vous sachiez utiliser le routeur.
Plus tard dans le cours, nous utiliserons un boilerplate contenant déjà le routeur que nous allons développer ici.
Information cachée dans l'HTML : les data-attributes
Pour pouvoir associer des URL à des écrans, nous avons besoin d'améliorer la façon de cacher de l'information dans l'HTML.
Pour démarrer ce nouveau tutoriel, nous allons repartir du code du tutoriel précédent.
Au sein de votre repo (normalement web2
) veuillez donc faire un copier/coller de votre code se trouvant dans /tutorials/pizzeria/hmi/structured
au sein du nouveau répertoire /tutorials/pizzeria/hmi/routing
.
En cas de souci, vous pouvez utiliser ce code-ci : structured-hmi.
Pour la suite de ce tutoriel, /tutorials/pizzeria/hmi/routing
est considéré comme la racine du projet.
Actuellement, notre Navbar
utilise le innerHTML
de ses éléments pour déterminer sur quel élément on a cliqué. Si l'on venait à changer le nom d'un élément, il ne faudrait pas à avoir à mettre à jour notre routeur.
Nous allons donc cacher de l'information dans chaque élément HTML en utilisant les data-attributes
.
On ajoute dans l'HTML des éléments de la Navbar
un data-attribute
nommé uri
.
Le nom de la propriété commence toujours par data-
suivi du nom du data-attribute
: data-uri
.
Veuillez mettre à jour le code de la fonction renderNavbar
de Navbar.js
:
jsfunction renderNavbar() {const navbar = document.querySelector('#navbarWrapper');navbar.innerHTML = `<nav class="navbar navbar-expand-lg navbar-light bg-danger"><div class="container-fluid"><a class="navbar-brand" href="#">e-Pizzeria</a><buttonclass="navbar-toggler"type="button"data-bs-toggle="collapse"data-bs-target="#navbarSupportedContent"aria-controls="navbarSupportedContent"aria-expanded="false"aria-label="Toggle navigation"><span class="navbar-toggler-icon"></span></button><div class="collapse navbar-collapse" id="navbarSupportedContent"><ul class="navbar-nav me-auto mb-2 mb-lg-0"><li class="nav-item"><a class="nav-link active" aria-current="page" href="#" data-uri="/">Home</a></li><li id="loginItem" class="nav-item"><a class="nav-link" href="#" data-uri="/login">Login</a></li><li id="registerItem" class="nav-item"><a class="nav-link" href="#" data-uri="/register">Register</a></li></ul></div></div></nav>`;}
Pour accéder en JS à ce data-attribute
, on y accède via l'attribut dataset
d'un élément HTML, par exemple : e.target.dataset.uri
pour accéder au data-attribute
nommé uri
.
Veuillez mettre à jour le code de la fonction onNavBarClick
de Navbar.js
:
jsfunction onNavBarClick() {const navItems = document.querySelectorAll('.nav-link');navItems.forEach((item) => {item.addEventListener('click', (e) => {console.log(`click on ${e.target.dataset.uri} navbar item`);if (e.target.dataset.uri === '/') {HomePage();} else if (e.target.dataset.uri === '/login') {LoginPage();} else if (e.target.dataset.uri === '/register') {RegisterPage();}});});}
Veuillez vérifier que le site de la pizzeria fonctionne toujours après ces changements.
Si un data-attribute
contient un "-" dans l'HTML, lorsqu'on y accède en JS, il faut convertir le "-" en camelCase. Exemple pour un data-attribute
nommé project-number
, on y accédera via refToHtmlElement.dataset.projectNumber
.
Création du routeur
Routage lors d'un clic sur un élément de la Navbar
Nous souhaitons que la gestion des clics sur un élément de la Navbar
soit faite au sein du routeur.
Pour ce faire, veuillez effacer la gestion des clics faite au sein de Navbar.js
.
Votre code devrait donc se résumer à cela :
js// eslint-disable-next-line no-unused-varsimport { Navbar as BootstrapNavbar } from 'bootstrap';const Navbar = () => {renderNavbar();};function renderNavbar() {const navbar = document.querySelector('#navbarWrapper');navbar.innerHTML = `<nav class="navbar navbar-expand-lg navbar-light bg-danger"><div class="container-fluid"><a class="navbar-brand" href="#">e-Pizzeria</a><buttonclass="navbar-toggler"type="button"data-bs-toggle="collapse"data-bs-target="#navbarSupportedContent"aria-controls="navbarSupportedContent"aria-expanded="false"aria-label="Toggle navigation"><span class="navbar-toggler-icon"></span></button><div class="collapse navbar-collapse" id="navbarSupportedContent"><ul class="navbar-nav me-auto mb-2 mb-lg-0"><li class="nav-item"><a class="nav-link active" aria-current="page" href="#" data-uri="/">Home</a></li><li id="loginItem" class="nav-item"><a class="nav-link" href="#" data-uri="/login">Login</a></li><li id="registerItem" class="nav-item"><a class="nav-link" href="#" data-uri="/register">Register</a></li></ul></div></div></nav>`;}export default Navbar;
Veuillez créer un nouveau composant fonctionnel /src/Components/Router/Router.js
en créant le dossier Router
et le module Router.js
.
Le code de Router.js
serait le suivant, si l'on transfert directement la fonction onNavBarClick
de Navbar.js
vers Router.js
:
jsimport HomePage from '../Pages/HomePage';import LoginPage from '../Pages/LoginPage';import RegisterPage from '../Pages/RegisterPage';const Router = () => {onNavBarClick();};function onNavBarClick() {const navItems = document.querySelectorAll('.nav-link');navItems.forEach((item) => {item.addEventListener('click', (e) => {if (e.target.dataset.uri === '/') {HomePage();} else if (e.target.dataset.uri === '/login') {LoginPage();} else if (e.target.dataset.uri === '/register') {RegisterPage();}});});}export default Router;
Pour utiliser le Router, il faut y faire appel au bon moment dans index.js
:
jsimport 'bootstrap/dist/css/bootstrap.min.css';import './stylesheets/main.css';import 'animate.css';import HomePage from './Components/Pages/HomePage';import Header from './Components/Header/Header';import Footer from './Components/Footer/Footer';import Navbar from './Components/Navbar/Navbar';import Router from './Components/Router/Router';Header();Navbar();Router();HomePage();Footer();
Le Router doit être appelé après que la Navbar
ait été rendue.
A ce stade-ci, votre application devrait être fonctionnelle comme auparavant.
Néanmoins, notre routeur n'est pas très générique.
Nous souhaiterions pouvoir le configurer via un dictionnaire d'URI et des composants fonctionnels associés et ne jamais avoir à modifier le code de la gestion des clics.
Veuillez mettre à jour Router.js
:
js1import HomePage from '../Pages/HomePage';2import LoginPage from '../Pages/LoginPage';3import RegisterPage from '../Pages/RegisterPage';45const routes = {6'/': HomePage,7'/login': LoginPage,8'/register': RegisterPage,9};1011const Router = () => {12onNavBarClick();13};1415function onNavBarClick() {16const navItems = document.querySelectorAll('.nav-link');1718navItems.forEach((item) => {19item.addEventListener('click', (e) => {20const uri = e.target?.dataset?.uri;21const componentToRender = routes[uri];22if (!componentToRender) throw Error(`The ${uri} ressource does not exist.`);23componentToRender();24});25});26}2728export default Router;
routes
est un dictionnaire liant les URI aux composants fonctionnels.
Ce dictionnaire, qui est un objet, est composé de paires de clé / valeur :
- la clé est l'URI qui a été cachée dans le
data-attribute
uri
de laNavbar
. - la valeur est une fonction importée permettant de rendre une page.
Pour rappel, pour accéder à la valeur d'une propriété d'un objet, on peut le faire soit via objectName.key
, soit via objectName[key]
. C'est ce que l'on fait via routes[uri]
, on accède à la valeur (le composant fonctionnel) associé à la clé (l'URI).
Si l'on a bien accès à une fonction, il ne reste plus qu'à l'appeler via componentToRender()
.
Bien, tout ceci est pas trop mal, mais nous n'avons pas encore la bonne URL dans le browser ainsi que l'historique. Nous allons maintenant voir comment ajouter l'URL au sein du browser.
L'History API du browser offre cette fonction window.history.pushState(state, unused, url)
:
state
: c'est un objet qui est associé avec l'entrée historique créée parpushState
; quand un utilisateur navigue à l'aide de l'historique (vers un nouvel état), un événementpopstate
est lancé ; cet événement contient une copie de l'objetstate
sauvegardé dans l'entrée historique au sein de la propriétéstate
(e.state
par exemple). Une taille limite de 16MB est imposée à l'objetstate
.unused
: peu être omis, est présent pour des raisons historiques 😵.url
: l'URL associée à l'entrée historique, c'est l'URL qui apparaîtra dans votre browser.
Mettez à jour le code de Router.js
pour créer une entrée dans l'historique du browser et ajouter l'URL au browser :
js1import HomePage from '../Pages/HomePage';2import LoginPage from '../Pages/LoginPage';3import RegisterPage from '../Pages/RegisterPage';45const routes = {6'/': HomePage,7'/login': LoginPage,8'/register': RegisterPage,9};1011const Router = () => {12onNavBarClick();13};1415function onNavBarClick() {16const navItems = document.querySelectorAll('.nav-link');1718navItems.forEach((item) => {19item.addEventListener('click', (e) => {20e.preventDefault();21const uri = e.target?.dataset?.uri;22const componentToRender = routes[uri];23if (!componentToRender) throw Error(`The ${uri} ressource does not exist.`);2425componentToRender();26window.history.pushState({}, '', uri);27});28});29}3031export default Router;
💭 Pourquoi prévenir le comportement par défaut associé à un clic sur un hyperlink ?
Faites le test sans cette ligne et naviguer entre les pages. Vous voyez une différence ?
En fait, le comportement par défaut lorsque un hyperlink redirige vers un lien interne (même origine), l'action est de scroller en top position de l'élément (qui pourrait être indiqué par une "anchor" via l'id de l'élément) ou en haut de la page si aucun élément est trouvé (via l'éventuel id d'un élément). De plus, le contenu de href
s'ajoute à l'URL du browser, ce qui affiche un #
inutile.
NB : La configuration des routes faites via le dictionnaire routes
pourrait être externalisée dans un fichier de configuration afin de rendre le code plus générique. Cela sera fait dans le futur boilerplate du cours ; nous pouvons laisser cette action de côté pour le tutoriel.
Routage lors de l'utilisation de l'historique du browser
Nous avons maintenant une URL différente quand nous naviguons d'une page à l'autre.
Mais nous ne pouvons pas encore utiliser les flèches du browser pour revenir en arrière.
Lorsque nous avons découvert la méthode window.history.pushState(state, unused, url)
, nous avons introduit l'événement popstate
.
C'est l'événement qui se produit lorsque l'historique de la fenêtre du browser change.
Il nous suffit donc, lors d'un popstate
, de récupérer la nouvelle URI qui sera automatiquement affichée dans le browser.
Pour récupérer l'URI que nous avons assigné à une page et qui sera visible dans l'URL de la fenêtre du browser, nous l'obtenons via : window.location.pathname
.
Veuillez mettre à jour le code de Router.js
:
js1import HomePage from '../Pages/HomePage';2import LoginPage from '../Pages/LoginPage';3import RegisterPage from '../Pages/RegisterPage';45const routes = {6'/': HomePage,7'/login': LoginPage,8'/register': RegisterPage,9};1011const Router = () => {12onNavBarClick();13onHistoryChange();14};1516function onNavBarClick() {17const navItems = document.querySelectorAll('.nav-link');1819navItems.forEach((item) => {20item.addEventListener('click', (e) => {21e.preventDefault();22const uri = e.target?.dataset?.uri;23const componentToRender = routes[uri];24if (!componentToRender) throw Error(`The ${uri} ressource does not exist.`);2526componentToRender();27window.history.pushState({}, '', uri);28});29});30}3132function onHistoryChange() {33window.addEventListener('popstate', () => {34const uri = window.location.pathname;35const componentToRender = routes[uri];36componentToRender();37});38}3940export default Router;
Tout comme précédemment, via routes[uri]
, on accède à la valeur (le composant fonctionnel) associé à la clé (l'URI) de routes
.
Routage lors du chargement de l'IHM
Actuellement, le chargement de la HomePage
est fait au sein d'index.js.
Nous souhaitons changer cela afin que cela soit fait dans notre routeur.
⚡ De plus, faites le tests, si vous êtes par exemple sur la LoginPage
est que vous faites un refresh au niveau du browser, l'URL restera sur /login
mais vous afficherez la HomePage
.
L'événement qui se produit lorsque la page index.html
est chargée, incluant les stylesheets et image, c'est load
.
Lors d'un load
, nous allons récupérer l'URI que nous avons assigné à une page et qui est visible dans l'URL de la fenêtre du browser via : window.location.pathname
.
Veuillez mettre à jour le code de Router.js
:
js1import HomePage from '../Pages/HomePage';2import LoginPage from '../Pages/LoginPage';3import RegisterPage from '../Pages/RegisterPage';45const routes = {6'/': HomePage,7'/login': LoginPage,8'/register': RegisterPage,9};1011const Router = () => {12onFrontendLoad();13onNavBarClick();14onHistoryChange();15};1617function onNavBarClick() {18const navItems = document.querySelectorAll('.nav-link');1920navItems.forEach((item) => {21item.addEventListener('click', (e) => {22e.preventDefault();23const uri = e.target?.dataset?.uri;24const componentToRender = routes[uri];25if (!componentToRender) throw Error(`The ${uri} ressource does not exist.`);2627componentToRender();28window.history.pushState({}, '', uri);29});30});31}3233function onHistoryChange() {34window.addEventListener('popstate', () => {35const uri = window.location.pathname;36const componentToRender = routes[uri];37componentToRender();38});39}4041function onFrontendLoad() {42window.addEventListener('load', () => {43const uri = window.location.pathname;44const componentToRender = routes[uri];45if (!componentToRender) throw Error(`The ${uri} ressource does not exist.`);4647componentToRender();48});49}5051export default Router;
Tout comme précédemment, via routes[uri]
, on accède à la valeur (le composant fonctionnel) associé à la clé (l'URI) de routes
.
Pour pouvoir afficher la HomePage
lors du chargement du frontend, c'est maintenant le routeur qui va prendre en charge cette action.
Lors de l'événement load
, l'URI qui sera détectée par le Router est "/"
.
C'est donc bien la HomePage
qui devra être affichée comme c'est configuré dans routes
.
Veuillez donc effacer l'import et l'appel de HomePage
au sein de index.html
et vous assurer que votre application est toujours fonctionnelle.
Routage lors d'une redirection vers une URL
Nous souhaitons créer une fonction qui permette de rediriger l'utilisateur vers un nouvel écran en mettant l'écran actuel dans l'historique et en changeant l'URL au sein de la fenêtre du browser.
Veuillez ajouter cette fonction au sein d'un nouveau fichier /src/Components/Router/Navigate.js
:
jsconst Navigate = (toUri) => {const fromUri = window.location.pathname;if (fromUri === toUri) return;window.history.pushState({}, '', toUri);const popStateEvent = new PopStateEvent('popstate', { state: {} });dispatchEvent(popStateEvent);};export default Navigate;
Nous avons créé une fonction qui n'a pas de dépendances, pas d'imports associés au routeur.
Nous avons fait cela afin d'éviter les dépendances cycliques.
💭En effet, imaginez que le routeur mette à disposition la fonction Navigate
.
Le job du routeur impose qu'il importe toutes les pages.
Si une page a besoin de faire appel au routeur, par exemple pour effectuer une redirection, alors nous aurions une dépendance cyclique.
Dès lors, nous avons simulé l'événement popstate
via la fonction dispatchEvent
.
Ainsi, lors de l'appel de la fonction Navigate
, la nouvelle URI est mise à jour dans la fenêtre du browser via l'appel de pushState
, puis popstate
est simulé.
Le routeur détecte qu'il y a eu un changement d'historique et charge le composant associé à la nouvelle URI.
Nous allons tester la fonction Navigate
en ajoutant un gestionnaire de clics sur la pizza au fromage.
Lors d'un clic sur la pizza au fromage qui se trouve dans le footer, on va rediriger l'utilisateur vers la HomePage
.
Veuillez donc mettre à jour Footer.js
:
js1import pizzaImage from '../../img/pizza2.jpg';2import logo from '../../img/js-logo.png';3import Navigate from '../Router/Navigate';45const Footer = () => {6const footer = document.querySelector('footer');7footer.innerHTML = `<h1 class="animate__animated animate__bounce animate__delay-2s text-center">8But we also love JS9</h1>`;1011renderSmallImage(footer, logo);12renderSmallImage(footer, pizzaImage, 'cheesePizza');13attachOnPizzaClick();14};1516export default Footer;1718function renderSmallImage(wrapper, url, id) {19const image = new Image(); // or document.createElement('img');20image.src = url;21image.height = 50;22if (id) image.id = id;23wrapper.appendChild(image);24}2526function attachOnPizzaClick() {27const pizza = document.querySelector('#cheesePizza');28pizza.addEventListener('click', () => {29Navigate('/');30});31}
Nous avons ajouté un identifiant à l'image sur laquelle nous souhaitons écouter les clics.
Veuillez tester que tout fonctionne bien.
Si tout fonctionne bien, faites un commit
de votre repo (web2
) avec comme message : routing-hmi
.
En cas de souci, vous pouvez accéder au code du tutoriel ici : routing-hmi.
Utilisation du routeur & boilerplate
Dans le cadre de ce cours, pour vos futures applications, vous allez utiliser le boilerplate du cours avec routeur qui met à disposition un projet bien configuré.
Voici ce qu'il ne faut faire pour bien router ses écrans :
/src/Components/Router/routes.js
: c'est le fichier dans lequel vous devez associer une URL à chaque page que vous souhaitez proposer dans laNavbar
. C'est là que vous centralisez les imports de vos pages./src/Components/Navbar/Navbar.js
: c'est dans ce fichier que vous devez créer les éléments de votreNavbar
et indiquer l'URI associée à chaque élément de votreNavbar
au sein d'un data-attribute nommé uri. Par exemple, ici on a indiqué"/login"
:<a class="nav-link" href="#" data-uri="/login">Login</a>
/src/Components/Pages
: c'est le répertoire où il est recommandé d'ajouter vos futures pages ou pour mettre à jour laHomePage
.
Exercice 2.11 : Utilisation d'un router
Objectif
Vous allez continuer le développement de myMovies afin de permettre la navigation et de proposer deux nouvelles pages permettant d'enregistrer temporairement et localement des films sur une page et d'afficher les films enregistrés sur une autre page.
Navigation entre pages & router
Veuillez créer un nouveau projet dans votre repository local et votre web repository (normalement appelé web2
) nommé /exercises/2.11
sur base du boilerplate du cours avec routeur.
⚡ Si vous avez fait un clone du boilerplate, attention au Git dans le Git, n'oubliez pas de supprimer le dossier .git
présent dans votre nouveau projet.
Dans un premier temps, veuillez créer la HomePage
en utilisant votre dernier design de myMovies.
Intégrez donc le code que vous avez fait pour l'Exercice 2.5.
Afin de s'assurer que la navigation est bien mise en place, veuillez intégrer deux nouvelles pages contenant simplement un titre :
- la page permettant l'ajout d'un film :
AddMoviePage
; - la page permettant l'affichage de tous les films :
ViewMoviePage
.
A ce stade-ci, vous devriez avoir au moins 3 pages : HomePage
, AddMoviePage
et ViewMoviePage
. Vous devriez avoir configuré votre Navbar
et votre Router
pour que lorsqu'on clic sur un lien, la bonne page s'affiche. Quand la navigation entre vos pages statiques et fonctionnelle, veuillez faire un commit
de votre code avec comme message : 2.11.1 : router & static pages
.
Ajout de films
Nous allons maintenant travailler sur la page permettant d'ajouter des films à l'aide d'un formulaire. A ce stade-ci de nos connaissances, nous acceptons qu'à chaque refresh du site, on redémarre avec une liste vide de films.
Notre formulaire doit contenir les champs :
title
: titre du film ;duration
: durée du film en minutes ;budget
: pour informer du prix qu'a couté la production du film, en millions ;link
: pour donner un lien vers la description du film (lien vers imdb, rottentomatoes ou autre).
Veuillez valider les champs de votre formulaire à l'aide d'HTML5 (required
, type
, min
, max
, minlength
...).
A chaque submit du formulaire, dans un premier temps, vous allez afficher le nouveau film enregistré dans la console.
Quand cette étape est fonctionnelle, veuillez faire un commit
de votre code avec comme message : 2.11.2 : add films
.
Redirection vers une page
A chaque ajout de film, vous allez naviguer vers la page affichant la liste des films "enregistrés".
Quand cette redirection est fonctionnelle, veuillez faire un commit
de votre code avec comme message : 2.11.3 : redirect to a page
.
Affichage de films enregistrés
Ensuite, vous allez construire la page affichant la liste des films enregistrés.
Voici un exemple de ce que pourrait être la page affichant la liste des films :
Title | Duration (min) | Budget (million) |
---|---|---|
Harry Potter and the Philosopher's Stone | 152 | 125 |
Avengers: Endgame | 181 | 181 |
… |
Veuillez noter que le champs link
est utilisé pour créer un lien dans la colonne Title
.
Quand votre application est finalisée, veuillez faire un commit
de votre code avec comme message : 2.11.4 : render films dynamically
.
🤝 Tips
- Créez une structure en mémoire pour accueillir les données d'un film, un array. Lorsqu'un formulaire est soumis et est valide, vous pouvez ajouter un objet (object literal) à votre array. Voici un exemple d'ajout d'un film :
jsconst films = [];films.push({title: 'Harry...',duration: 120,budget: 111,link: 'https://amazing-javascript.world',});
- 💭 Okay, j'ai un array de films... Mais comment partager cet array avec ma
ViewMoviePage
et avecAddMoviePage
?
Pensez à la notion de module JS. Une bonne solution serait de créer un nouveau script, par exemplemovies.js
. Ce script déclarerait un array, et plutôt que de le partager (c'est interdit de modifier un objet importé en ECMAScript), exporterait des méthodes qui pourraient s'appeler, par exemple,readAllMovies
etaddOneMovie
.
Projet 2.12 : Navigation & router
Vous allez mettre à jour votre HomePage
et AboutPage
développée pour Projet 2.9 afin d'utiliser le router offert dans le boilerplate du cours.
Veuillez créer un nouveau projet dans votre repository local et votre web repository (normalement appelé web2
) nommé /project/2.12
sur base du boilerplate du cours avec routeur.
⚡ Si vous avez fait un clone du boilerplate, attention au Git dans le Git, n'oubliez pas de supprimer le dossier .git
présent dans votre nouveau projet.
Dans ce nouveau projet, veuillez intégrer le code la dernière version de votre projet, probablement écrit pour le Projet 2.9.
Quand la navigation fonctionne correctement pour naviguer entre les pages à l'aide de votre Navbar
, veuillez faire un commit
de votre code avec comme message : 2.12 : dynamic page content
.