Gestion des évènements en JS
Le support Javascript dans le navigateur permet de gérer les événements qui sont déclenchés par les actions de l'utilisateur. Plus de quatre-vingts types d'événements peuvent être capturés par le navigateur. Il est par exemple possible de capturer les événements déclenchés par les clics de la souris (click
, dblclick
), par le déplacement de la souris (mousemove
, mouseover
, mouseenter
), par le clavier (keypress
, keydown
, keyup
), par la soumission d'un formulaire (submit
), par le changement d'une valeur d'un champ de saisis (change
), par le chargement d'une nouvelle page (load
), etc.
Pour le projet, nous en aurons besoin pour récupérer des informations saisies par un utilisateur ou mettre à jour dynamiquement le contenu de la page (afficher les pions par exemple quand on clique sur une case).
Une action réalisée par un utilisateur va déclencher un événement du type concerné. Cet événement est un objet Javascript qui contient plusieurs propriétés décrivant l'action qui vient d'être réalisée. Par exemple, la propriété timeStamp
contient le temps qui s'est écoulé depuis le chargement de la page jusqu'à la réalisation de l'action (en millisecondes). La propriété target
référence l'élément ciblé par l'action (l'élément cliqué par exemple). D'autres propriétés dépendent du type d'événement. Par exemple les événements de type clavier (keypress
, keydown
, keyup
) ont la propriété key
qui décrit la touche du clavier qui a été utilisée. L'objet de ce tutoriel n'est pas de décrire tous les types d'événements et leurs propriétés, mais de présenter comment capturer un événement et comment utiliser les informations qu'il contient.
L'API DOM propose la méthode addEventListener
pour activer la capture d'un type d'événement. Cette méthode a deux paramètres. Le premier paramètre précise le type d'événement à capturer. Le deuxième paramètre est une fonction qui sera exécutée à chaque occurrence de l'événement. Cette fonction prend en paramètre l'objet événement qui contient les informations sur l'événement capturé. La méthode addEventListener
doit être appelée sur un objet qui référence une balise HTML.
Méthode addEventListener
Comme souvent en JS, il existe plusieurs manières de faire la même chose. C'est le cas pour addEventListener
. Je vous en présente 3 ici.
soit le code HTML
<button id="b1">bouton 1</button>
<button id="b2">bouton 2</button>
<button id="b3">bouton 3</button>
<button id="b4">bouton 4</button>
Les 3 approches sont fonctionnellement équivalentes.
Si vous lisez du code JS un peu ancien vous trouverez principalement les deux premières versions. Comme indiqué dans la section sur les fonctions, dans le cadre de ce cours, vous privilégierez les fonctions fléchées.
Approche par fonction nommée
let b1 = document.getElementById("b1");
b1.addEventListener("click", maFonctionDeGestionEvenement);
function maFonctionDeGestionEvenement(event){
console.log(event);
}
Approche avec une fonction anonyme
let b2 = document.getElementById("b2");
b2.addEventListener("click", function (event){
console.log(event);
});
Approche avec une fonction fléchée (depuis ES6)
let b3 = document.getElementById("b3");
b3.addEventListener("click", (event) => {
console.log(event);
});
Méthode removeEventListener
La méthode EventTarget.removeEventListener()
supprime d'une EventTarget
(cible) un écouteur d'évènements précédemment enregistré avec EventTarget
.
Si on repart du code précédent, voici comment utiliser cette méthode.
let b4 = document.getElementById("b4");
b4.addEventListener("click", (event) => {
b1.removeEventListener("click", maFonctionDeGestionEvenement);
//b2.removeEventListener("click", ???????); //impossible
//b3.removeEventListener("click", ???????); //impossible
//plus compliqué pour b2 et B3, car nous n'avons pas de référence à la fonction
// Nous verrons ceci plus tard.
});
Qui est this ?
Dans le contexte de la gestion d'évènements, savoir qui est this n'est pas toujours simple.
Comme défini par Mozilla : "Il est souvent souhaitable de référencer l'élément sur lequel le gestionnaire d’événements a été lancé. Si le gestionnaire est attaché à une fonction à l'aide de addEventListener()
, la valeur de this
, dans le gestionnaire est une référence à l'élément".
Reprenez l'exemple précédent, mettez un point d'arrêt sur chaque console.log
et à l'aide du debbuger regardez à qui fait référence this
dans les 3 cas.
Cet exemple est un peu différent. Cette fois le gestionnaire est encapsulé dans un objet (j'ai choisi la syntaxe de la création d'objets la plus courte).
Tout d'abord créez un paragraphe dans votre html
<p id = "paragraphe">
texte dans un paragraphe
</p>
Toute la question est de savoir, quand on écrit this
s'il fait référence à l'instance ou à l'élément sur lequel a été généré l'évènement.
Je vous propose deux exemples pour bien comprendre la mécanique du this
.
1er exemple: pas de classe utilisée
const element = document.getElementById("paragraphe");
const name = 'Something Good';
const onclick1 = function () {
console.log(name); // ok
this.style.color = 'red'; //OK, ici this référence le paragraphe cliqué
};
const onclick2 = (event) => {
console.log(name); // 'Something Good'
//this.style.color = 'red'; //impossible ici this référence l'objet windows et pas le paragraphe
event.target.style.color = 'green'; // event est là pour récupérer l'élément ayant généré l'évènement
};
element.addEventListener('click', onclick1, false);
element.addEventListener('click', onclick2, false);
- Dans le premier cas, c'est une fonction nommée. Le
this
fait donc référence aunode
(ici un paragraphe) sur lequel a été généré l'évènement. - Dans le second cas, c'est une fonction fléchée qui n'a pas son propre contexte
this
.JS
va chercher l'élément parent direct ayant un contextethis
. Ce code n'est pas dans une classe, l'élément le plus proche est donc l'objetwindows
. Doncthis.style.color = 'red';
génère une erreur. Cette ligne a été écrite comme si on pouvait avoir accès au paragraphe cliqué. Ce qui est une erreur. - Dans le second cas, pour avoir accès à l'élément cliqué, on utilise l'attribut
target
de l'objetevent
passé automatiquement par le moteurJS
. Vous avez donc directement accès au paragraphe en écrivantevent.target.style.color
Comme indiqué dans la section sur les fonctions, vous utiliserez uniquement les fonctions fléchées.
2nd cas de figure : le code est dans une classe
Soient deux paragraphes dans un code HTML.
<p >
texte dans un paragraphe
</p>
<p >
un texte dans un autre paragraphe
</p>
Voyons comment interagir avec ces deux paragraphes si le code "métier" est écrit dans une classe.
C'est exactement le même principe qu'avant, sauf qu'il ne faut pas se tromper quand on utilise this
.
class Something {
constructor() {
this.name = 'Something Good';
}
maMethodeFlechee = (event) =>{
console.log(`${this.name}`); //this est l'instance de Something car nous sommes dans une fonction fléchée
event.target.style.color = 'green';
}
maMethodeClassique(){
console.log(`${this.name}`); // impossible car c'est une fonction nomée (this référence ici le paragraphe cliqué)
this.style.color = 'red'; //this est bien le paragraphe cliqué
}
}
//le code ci-dessous pourrait être dans un autre fichier ou on aurait importé
//la classe something
const something = new Something();
const elements = document.getElementsByTagName("p");
//boucle pour associer le même comportement à chaque pargraphe
for (e of elements){
e.addEventListener('click',something.maMethodeClassique);
e.addEventListener('click',something.maMethodeFlechee);
}
Comme indiqué dans la section sur les fonctions, vous privilégierez les fonctions fléchées. Même dans les classes.
Interagir avec les composants les plus courants
Liste déroulante (menu déroulant) :
HTML :
<select id="menuDeroulant">
<option value="1">Option 1</option>
<option value="2">Option 2</option>
<option value="3">Option 3</option>
</select>
JavaScript (pour gérer le changement de sélection) :
const menuDeroulant = document.getElementById("menuDeroulant");
menuDeroulant.addEventListener("change", () => {
const selectedOption = menuDeroulant.options[menuDeroulant.selectedIndex].value;
console.log("Option sélectionnée :", selectedOption);
});
Liste à cocher :
HTML :
<label><input type="checkbox" id="checkbox1" value="valeur1"> Option 1</label><br>
<label><input type="checkbox" id="checkbox2" value="valeur2"> Option 2</label><br>
<label><input type="checkbox" id="checkbox3" value="valeur3"> Option 3</label><br>
JavaScript (pour récupérer les valeurs sélectionnées) :
const checkboxes = document.querySelectorAll("input[type=checkbox]");
checkboxes.forEach(checkbox => {
checkbox.addEventListener("change", () => {
if (checkbox.checked) {
console.log("Option cochée :", checkbox.value);
} else {
console.log("Option décochée :", checkbox.value);
}
});
});
Sélecteur de couleur :
HTML :
<input type="color" id="colorPicker">
JavaScript (pour récupérer la couleur sélectionnée) :
const colorPicker = document.getElementById("colorPicker");
colorPicker.addEventListener("input", () => {
console.log("Couleur sélectionnée :", colorPicker.value);
});
Champ de recherche :
HTML :
<input type="search" id="searchInput" placeholder="Recherche...">
JavaScript (pour récupérer le texte saisi) :
const searchInput = document.getElementById("searchInput");
searchInput.addEventListener("input", () => {
console.log("Texte de recherche :", searchInput.value);
});
Bouton d'envoi :
HTML :
<button id="submitButton">Envoyer</button>
JavaScript (pour gérer le clic) :
const submitButton = document.getElementById("submitButton");
submitButton.addEventListener("click", () => {
console.log("Bouton envoyé !");
});
Apprendre à structurer son code en JS
Définition
L'exercice un peu plus loin permet de travailler sur les interactions. C'est le bon moment pour aborder la problématique du code dans un projet JS.
L'exercice nécessitera de créer 3 fichiers :
- un fichier html avec tous les composants qui seront affichés au chargement de la page
- un premier fichier js avec le code permettant de gérer toute la logique d'interactions avec la page (gérer les clics)
- un second fichier js avec une classe modélisant un
Utilisateur
.
Lien entre les 3 fichiers.
- le fichier html inclus le premier fichier JS. Qui peut être vu comme un main en java.
- le premier fichier va devoir importer le second fichier pour pouvoir créer et manipuler des objets de la classe
Utilisateur
- le second fichier JS (la classe
Utilisateur
) nécessitera d'exporter explicitement la classeUtilisateur
Mécanique de l'exportation / importation entre fichiers JS
Lorsque vous exportez une classe ou toute autre chose depuis un fichier JavaScript, vous avez deux options principales : l'export global (ou par défaut) et l'export nommé.
-
Export global (par défaut) :
- Lorsque vous utilisez l'export global, vous exportez directement votre classe (ou autre élément) sans lui attribuer de nom spécifique.
- Cela signifie que lorsque vous importez cette classe dans un autre fichier, vous pouvez lui donner n'importe quel nom que vous voulez.
- Voici comment exporter la classe
Utilisateur
en tant qu'export global :
export default class Utilisateur {
#nom;
#prenom;
constructor(nom, prenom) {
this.#nom = nom;
this.#prenom = prenom;
}
afficherNomComplet = () => {
console.log(`Nom complet: ${this.#nom} ${this.#prenom}`);
}
}
Lors de l'importation, vous pouvez nommer la classe comme vous le souhaitez :
import NomDeVotreChoix from './Utilisateur.js';
// Création d'une instance de la classe Utilisateur
const utilisateur = new NomDeVotreChoix("Doe", "John");
// Utilisation des méthodes de la classe Utilisateur
utilisateur.afficherNomComplet();
- Export nommé :
- Lorsque vous utilisez l'export nommé, vous exportez votre classe (ou autre élément) avec un nom spécifique.
- Cela signifie que lorsque vous importez cette classe dans un autre fichier, vous devez utiliser le même nom que celui que vous avez spécifié lors de l'exportation.
- Voici comment exporter la classe
Utilisateur
en tant qu'export nommé :
export class Utilisateur {
//même code
}
Lors de l'importation, vous devez utiliser le même nom que celui spécifié lors de l'exportation et utiliser des accolades :
import { Utilisateur } from './Utilisateur.js';
const utilisateur = new Utilisateur("Doe", "John");
// Utilisation des méthodes de la classe Utilisateur
utilisateur.afficherNomComplet();
En résumé, la principale différence entre un export global et un export nommé réside dans la manière dont vous importez l'élément dans un autre fichier et dans la façon dont vous référencez cet élément une fois qu'il est importé.
Mécanique de l'importation d'un fichier JS
depuis un fichier HTML
Jusqu'à maintenant, vous avez utilisé l'une de ces deux options :
- Mettre le js dans le HTML (mauvaise habitude)
<body>
<p> du texte </p>
<script>
//du code JS
</script>
</body>
- Intégrer des liens vers les fichiers
js
dans le HTML (déjà un peu mieux)
<body>
<p> du texte </p>
<script src="chemin/vers/votre/fichier.js"></script>
<script src="chemin/vers/votre/autreFichier.js"></script>
</body>
Dans ce second cas, toutes les variables toutes les fonctions et variables sont accessibles depuis tous les ficihers. Ce qui peut vite devenir compliqué à gérer (un peu comme si vous aviez fait un seul fichier). De plus, il n'est pas possible d'importer/exporter des classes ou des fonctions comme on vient de le voir.
Pour les codes conséquents (par exemple l'exercice ci-cessous, ou votre projet), il faudra inclure des fichiers JavaScript
en utilisant le type de module
. L'ajout de type="module"
dans votre balise <script>
indique au navigateur que le fichier JavaScript
est un module, ce qui permet l'utilisation de fonctionnalités de modules JavaScript.
Voici comment vous pouvez inclure un fichier JavaScript en tant que module dans votre fichier HTML :
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Votre Titre</title>
<button id="b1">Créer un utilisateur</button>
</head>
<body>
<!-- Inclure un fichier JavaScript en tant que module -->
<!-- Voyez ça un peu comme votre classe main (en java) -->
<script type="module" src="app.js"></script>
</body>
</html>
Les modules JavaScript introduisent la portée au niveau du fichier, ce qui signifie que les variables et les fonctions déclarées dans un module ne sont pas automatiquement accessibles depuis d'autres modules, à moins qu'elles ne soient explicitement exportées. De même, vous devez importer explicitement ce dont vous avez besoin dans chaque module.
Voici un exemple de module JavaScript (app.js
) qui crée un utilisateur et affiche son nom quand on clique sur un bouton :
import { Utilisateur } from './Utilisateur.js';
document.getElementById("b1").addEventListener('click',(event) => {
console.log("Bouton cliqué");
try {
const user = new Utilisateur("Nicholas", "Journet", new Date("1979-11-10"));
user.afficherNomComplet();
}
catch (e)
{
console.error('Erreur de création d\'utilisateur', e);
}
});
Exercice
Comme sur la vidéo, créez une page web permettant de
- saisir un login/mail/date pour une personne (les 3 doivent être remplis)
- il ne peut pas y avoir deux fois le même login (utilisez une map ce sera plus simple)
- la liste doit s'afficher comme sur la vidéo
- il faut pouvoir supprimer une entrée dans la liste
<body>
<h1> Liste d'utilisateurs </h1>
<ul id="userList">
</ul>
<h1>Ajout d'un individu</h1>
<div>
<label>Login</label><input id="login" type="text" name="login">
<label>Mail</label><input id="mail" type="mail" name="mail">
<label>Date de naissance</label><input type="date" id="dateNaissance" name="date">
<button id="addUser">Ajouter un utilisateur</button>
</div>
<div style="margin-top: 30px;">
<label>Nom à effacer</label>
<select id="delUserList" style="width:100px;">
</select>
<button id="delUser">del</button>
</div>
<script type="module" src="listePersonne.js"></script>
</body>