Yoan Blanc’s weblog

Another lost swiss guy

Custom event and modular widgets with DOM3

Yoan BlancMon, 22 Dec 2008, , ,

I’ve previously here presented some custom event using jQuery and made this summer a lightning talk at the Webtuesday presenting most of the event mechanisms I know. I went through DOM0 and DOM2 events but missed DOM3 which offers what I called CustomEvent (using the same name as YUI’s). The ultimate goal of using event is to come with small and reusable modules loosely coupled together. For that purpose I created a carousel module, which only does the carousel (reading isn’t usable alone).

But first, go back to DOM0 and how you can do DOM0 modules. This is DOM0.

document.body.onclick = function(event) {
    alert("clicked");
};

I compared it to the delegates that uses C# (using =+ and =- though). It’s possible to create an object that will call functions you defined when something happened. You’ll have to override those method to get the callback but as only one callback is allowed per event, i.e. onclick it can get tricky.

DOM2 introduced the event listeners (à la Swing), which is called event handlers in the MSIE world, but I guess you know the story. The event listeners enable (mainly) to have multiple listeners per event.

document.body
.addEventListener("click",
 function(event) {
  alert("clicked");
 },
 false
);

This works well for existing DOM events fired by the browser itself but in order to build more specific applications, the different libraries came up with their way of doing it. Inspired by Qt connect, by a channel-based publication/subscription like XMPP, by the Observer pattern from the GOF there are many solutions giving their abstraction of the existing model.

DOM3 introduces a custom event called Event (or Events), like MouseEvent (mousemove), HTMLEvent (click), MutationEvent (change), … to do what you want with it. My idea is to create a module that encapsulate a DOM Node and offers the same interface as the other nodes. Basically:

function Module(element) {
 this.element = element;
};
Module.prototype.addEventListener =
 function(name, listener, capture) {
  return element.addEventListener(name, listener, capture);
 };
Module.prototype.removeEventListener =
 function(name, listener, capture) {
  return element.removeEventListener(name, listener, capture);
 };

The dispatchEvent function is missing from the original interface, up to you to add it.

var bd = new Module(document.body);
bd.addEventListener("click",
 function(event) {
  alert("clicked");
 },
 false
);

From that point, I can consider this instance as a DOM Node, with special powers. Let’s add them.

Module.prototype.hide =
 function() {
  this.element.style.visibility = "hidden";
  this.changed();
 };
Module.prototype.show =
 function() {
  this.element.style.visibility = "show";
  this.changed();
 };

Not magic yet…

Module.prototype.changed =
 function() {
  var event = document.createEvent("Events");
  event.initEvent("visible", true, true); 
  event.visilibility = this.element.style.visibility;
  this.element.dispatchEvent(event);
 };

So, when anyone shows or hides this element, it will fire a visible event. As you might have noticed, the event listener isn’t binding the module, so "this" isn’t the module but the DOM tag itself (this can be fixed of course, I just wanted to keep it simple).

bd.addEventListener("visible",
 function(event) {
  alert(event.visibility);
 },
 false
);

The goal of this, and this is overused inside YUI, is to create independent modules/component/widgets that aren’t explicitly linked to each other but communicate using events.

So the mentioned above carousel has a pagination and a status displaying the current page and the total number of pages. Those two elements aren’t linked to the carousel explicitly. They fire a page event containing the information about the current page (and some other useful bits). If you’re crazy enough, it’s possible to pilot n carousel from one pagination, or one carousel with n paginations.

Simple modules, small code, infinite possibilities, just like LEGO’s.

I think that by having such a simple architecture can enable a better reutilization, simpler unit testing and more modularity. How small a graphical module can be in term of functionalities, can it be smaller? Do it. DOM3 applies to the content of the page, so to the UI of it and isn’t appropriate to be used with XMLHttpRequest or other asynchronous elements of course. This is why it will mostly be used on graphical widgets to put together a rich interface than on the part that communicates with a server for example.

As a conclusion, DOM3 isn’t still on all the browsers so you’d better stick to yours favourite libraries if you aim to cover a wide range of browsers.

J’ai ici rapidement introduit le système évènementiel de jQuery, système permettant la réalisation de module collaborant indirectement. Avec en exemple un sudoku inspiré par celui présent dans GNOME. Cet été, lors d’un webtuesday zürichois j’ai tenté de présenter les différents systèmes des différentes bibliothèques permettant de se servir d’évènements selon tel ou tel paradigme existant : Qt, GObject (GTK), Swing, SWF (C#), ... en commençant par parler de ce qui est livré avec DOM, c’est à dire DOM0 (onclick) et DOM2 (addEventListener).

DOM0 s’il est très simple à mettre place, devient vite un casse-tête quand il s’agit d’avoir plusieurs éléments devant réagir à un même évènement. L’exemple le plus célèbre est à mon avis la solution trouvée par Simon Willison il y a fort longtemps pour ne pas réécrire un window.onload s’il existe déjà. Tout simplement en le chainant.

function addLoadListener(listener) {
 if(typeof window.onload !== "function") {
  window.onload = listener;
 } else {
  var old = window.onload;
  window.onload = function() {
   old();
   listener();
  };
 }
}

S’il n’existe pas de onload, on peut directement assigner la fonction devant réagir. Dans le case contraire, il va être créé une fonction anonyme appelant l’ancien callback (old puis celui que l’on vient d’assigner. Très rudimentaire, et la méthode propre à dojo, dojo.connect fonctionne un peu de cette manière là, mais de manière générale.

dojo.connect(window, "onload",
 function() {
  alert("loaded");
 }
);

Ça ressemble à du Qt ou du GTK+ n’est-ce pas ? Voilà pour DOM0 qu’il est très facile d’appliquer en disant : lorsque ceci se passe, l’objet X va exécuter la méthode Y, à vous de la définir si vous en avez le besoin. Intéressant mais un peu trop gray box à mon goût.

DOM2 et son addEventListener séparent plus clairement ce qui se passe lors d’un évènement, et encapsule toutes les informations pouvant être passées dans un object de type Event (il y en a de différents types). On entre dans un paradigme plus proche de Swing (mais avec la légèreté d’un langage dynamique faiblement typé).

DOM2 ne s’applique cependant qu’aux usages du navigateur, qu’aux évènements qu’il peut/va déclencher. DOM3 introduit plus d’éléments, notamment pour d’autres formats tels que SVG et XUL (uniquement pour les navigateurs à base Gecko jusqu’ici, comme Firefox). Parmi ceux-ci un évènement générique, tout simplement nommé "Event" ou ("Events").

L’idée que j’ai dernière la tête est de, lors la conception de module riches tels que des onglets, sliders, carousels, menu déroulants, ... utiliser les mêmes conventions que des éléments plus simples comme les éléments de formulaires, liens, ... pour leur architecture. Mon système d’onglet offre cette interface et va déclencher l’évènement suivant:

tabchanged
comprenant le contenu du href de l’élément cliqué, ainsi que son id si présente.

Une fois ceci dit, ce module en question est utilisable par une personne connaissant le minimum vital de DOM2 et n’impose l’apprentissage d’aucune nouvelle abstraction du langage (qui est un synonyme pessimiste de bibliothèque (library) ou cadriciel (framework)).

En exemple un carousel fonctionnant de manière indépendante de sa pagination et de l’élément affichant quelle est la page courante par rapport au nombre de page. L’aspect très intéressant est que les modules sont au final assez identiques (je n’ai pas désiré mettre d’héritage cependant pour conserver un code relativement simple à lire) et compacts. Le mot d’ordre est un peu quel est le plus petit nombre de fonctionnalités qui permettent de rendre tel ou tel élément valable? Le plus petit, réutilisable, simple et testable est un composant; le meilleur il est selon moi.

Le but visé n’est pas d’aller chercher des noises à YUI ou ExtJS, mais simplement de continuer l’exploration de qu’est que le desktop peut apprendre au web et comment le web peut s’en servir sans réinventer le desktop.

About

meYoan Blanc is a web developer that lives in Norway (Markveien 24, 0554 Oslo) works for Opera and comes from La Chaux-de-Fonds. This web site is for this weblog, a playground for random stuffs and can help keeping me connected with some friends out there.

Get my vCard or contact me by phone (skype:yoan.blanc) or email ().

Misc

RSS, list.blogug.ch

This site reflects only my opinion and is not affiliated with anyone else.

copyright 2006-2009 — doSimple.ch