La fonction
addEventListener ()
permet d'associer une fonction (le gestionnaire d'événement) à un événement. Lorsque l'événement survient, le navigateur appelle le gestionnaire d'événement en lui transmettant un unique argument sous la forme d'un objet décrivant l'événement.
Comment transmettre autre chose à ce gestionnaire, comme par exemple une référence sur un objet spécifique ?
La solution
La solution passe par une closure. Une closure est un ensemble de variables préservées en dehors de la portée du sous-programme où elles sont définies, parce que un ou plusieurs autres sous-programmes y font référence.
Concrètement une fonction
f ()
retourne une fonction g ()
(ie : un objet Function
) telle que g ()
utilise la variable o
transmise en argument à f ()
comme si c'était une variable globale. Cette variable étant une variable locale de f ()
, elle devrait être désallouée à la fin de l'appel à f ()
. Toutefois, comme g ()
y fait référence, la variable est préservée. Par exemple :
function f (o) { return (function g () { o.value ++; // g () référence la variable locale o de f () comme si c'était une variable globale }); } // Fin de la fonction : o n'est pas désallouée car l'objet Function g () retourné y fait référence
Il suffit donc de fournir à
addEventListener ()
une fonction g ()
retournée par un appel à f ()
à laquelle une référence sur un objet est transmise via l'argument o
.
Le code JavaScript
En JavaScript, la solution se traduit par le code suivant. Il s'agit d'un exemple dans lequel on dispose d'une liste d'objets contenant trois propriétés :
text
, color
et backgroundColor
. Pour chacun de ces objets, on souhaite générer un
contenant le texte de l'objet. Par ailleurs, on souhaite que cet élément HTML prenne la couleur et la couleur de fond de l'objet correspondant quand l'utilisateur clique dessus. Ce qui donne :
var tagDIV, messages, i;
messages = [
{text:"Bonjour", color:"red", backgroundColor:"blue"},
{text:"Au revoir", color:"green", backgroundColor:"yellow"}
];
for (i = 0; i != messages.length; i ++) {
tagDIV = document.createElement ("div");
tagDIV.innerHTML = messages[i].text;
tagDIV.addEventListener ("click", function (o) { return (function (e) { this.style.color = o.color; this.style.backgroundColor = o.backgroundColor; }); } (messages[i]), false);
document.body.appendChild (tagDIV);
}
Ce code se lit ainsi :
-
la fonction anonyme
function (o)
retourne une fonction anonyme function (e)
servant de gestionnaire d'événément (e
correspond à l'objet de l'événement) ;
-
function (e)
fait référence à l'argument o
de function (o)
(lequel argument constitue une variable locale de cette fonction) comme s'il s'agissait d'une variable globale ;
-
de ce fait,
o
est préservée à la fin de l'appel à function (o)
, et par conséquent l'objet du tableau messages[]
de l'itération courante à laquelle cette variable fait référence ;
-
quand l'événement survient, le gestionnaire d'événement
function (e)
dispose toujours d'une référence valide sur cet objet via o
, si bien qu'il peut en accéder aux propriétés.
Cliquez ici pour tester cet exemple. Vous pourrez visualiser le code et le récupérer pour travailler avec.
La petite subtilité à bien saisir : c'est la référence sur la variable
o
qui est maintenue. On le voit bien quand o est une constante et non une référence sur un objet :
function f (o) { return (function g () { alert (o); }); } var x = 3; window.setTimeout (f (x), 2000); // Créer un gestionnaire d'événement alors que x vaut 3, et l'appeler dans 2 secondes window.setTimeout (function () { x = 5; }, 1000); // 1 seconde avant, passer la valeur de x à 5
Dans cet exemple, le gestionnaire d'événement va afficher 3, car
o
référence la constante 3 et non un objet dont la valeur serait passée de 3 à 5.
Il est possible de simplifier l'écriture en factorisant un peu le code de
function (o)
. A cette occasion, il est intéressant d'introduire un raffinement qui se révèle fort utile quand on souhaite utiliser une fonction d'un objet comme gestionnaire d'événement. Dans le code du gestionnaire d'événement, il s'agit de faire en sorte que this
référence l'objet transmis en argument à function (o)
plutôt que l'élément HTML à l'origine de l'événement (toujours accessible via e.target
).
Ainsi, le code du gestionnaire peut s'écrire comme une fonction d'un objet :
function CMessage (text, color, backgroundColor) { this.text = text; this.color = color; this.backgroundColor = backgroundColor; } CMessage.prototype.onClick = function (e) { e.target.style.color = this.color; e.target.style.backgroundColor = this.backgroundColor; };
Le coeur de ce système est
createEventListener ()
, produit de la factorisation :
function createEventListener (o, handler) { return (function (e) { handler.call (o, e); }); }
Et le code du programme devient :
function run () { var tagDIV, messages, i; messages = new Array (); messages.push (new CMessage ("Bonjour", "red", "blue")); messages.push (new CMessage ("Au revoir", "green", "yellow")); for (i = 0; i != messages.length; i ++) { tagDIV = document.createElement ("div"); tagDIV.innerHTML = messages[i].text; tagDIV.addEventListener ("click", createEventListener (messages[i], CMessage.prototype.onClick), false); document.body.appendChild (tagDIV); } }
L'exemple
Cliquez ici pour tester cet exemple. Vous pourrez visualiser le code et le récupérer pour travailler avec.
La logique
La fonction
createEventListener ()
est très courte. Pour autant, son code doit être lu avec soin, car il accomplit beaucoup :
-
La fonction retourne une fonction anonyme (
function (e)
) qui prend un seul argument. En tant que telle, cette fonction peut être utilisée comme gestionnaire d'un événement HTML, son argumente
correspondant alors à cet événement. -
Quand l'événement survient, la fonction anonyme ainsi utilisé est donc appelée. A son tour, elle appelle le véritable gestionnnaire via
call ()
. Pour rappel, tout objet est doté decall ()
. En effet, c'est une fonction deObject
dont le prototype se retrouve dans la chaîne des prototypes de tout objet créé via l'opérateurnew
(ou pour le dire moins rigoureusement, mais plus simplement : "dont tout objet hérite"). -
Ce véritable gestionnaire, c'est la fonction référencée par
handler
de l'objeto
. L'appel viacall ()
permet d'imposer la valeur dethis
dans le code du véritable gestionnaire comme une référence sur l'objeto
.
Une version plus élaborée de
createEventListener ()
permet de passer au véritable gestionnaire d'événement d'autres arguments que e
. Pour cela, la fonction utilise non plus call ()
, mais apply ()
et bind ()
, deux autres fonctions héritées de Object
:
/*------------------------------------------------------------------------------ Crée un gestionnaire d'évènement qui appelera une fonction en lui transmettant une référence this sur un objet, un référence sur l'évènement et éventuellement d'autres arguments ENTREE object Objet sur lequel this pointera dans le corps de la fonction handler Fonction appelée par le gestionnaire d'évènement args Tableau de arguments (optionnel) transmis de plus à la fonction SORTIE Pointeur sur le gestionnaire d'évènement ------------------------------------------------------------------------------*/ function createEventListener (object, handler, args) { var f; if (args instanceof Array) f = function (e) { args.push (e); handler.apply (this, args); args.pop (); }; else f = handler; return (f.bind (object)); }
Les arguments supplémentaires doivent être transmis via un tableau. Par exemple, pour transmettre trois arguments "Hello", 1970 et 3.14 au gestionnaire :
var eventHandler; eventHandler = createEventListener (messages[i], CMessage.prototype.onClick, ["Hello", 1970, 3.14]); tagDIV.addEventListener ("click", eventHandler, false);
Les gestionnaire d'événément prendra alors la forme suivante :
CMessage.prototype.onClick = function (e, word, year, pi) { // Code du gestionnaire };
Cliquez ici pour tester un exemple. Vous pourrez visualiser le code et le récupérer pour travailler avec.