Pour ce premier article de 2014, je vais aborder un outil que n’importe quel développeur web ou intégrateur a déjà dû utiliser. Je veux parler de jQuery.
En effet, jQuery est présent sur une très grande majorité de sites Web, il est présent par défaut dans des frameworks comme Ruby on Rails et possède une très importante base de plugins et d’utilisateurs.
Il existe une controverse concernant l’utilisation inconditionnelle de jQuery et le fait que cette dernière tend à favoriser l’ignorance de JavaScript. C’est à dire que beaucoup de développeurs ne connaissent que jQuery pour tout ce qui concerne le DOM, la propagation d’événements, la sélection d’éléments, les manipulation de propriétés, etc.
Attention, l’article est rédigé par une ce des personnes. N’étant pas un développeur JavaScript j’ai baigné dans le jQuery avant toute autre chose. Cela ne veut pas dire que je ne connais rien au JavaScript, mais jQuery a été ma porte d’entrée et je le trouve toujours très pragmatique même s’il n’est pas exempt de défaut.
Il m’arrive souvent d’avoir à intégrer du code fourni par un intégrateur. Parfois, certains composants JavaScript sont manquants. Commence alors une quête du composant préconstruit idéal. Très souvent la base de code est dépendante de jQuery et j’utilise Unheap, que Victor m’a conseillé, pour trouver la perle rare qui fera exactement ce dont j’ai besoin.
Oui, mais non ! L’intégrateur avait probablement fait la même chose avant moi car en général, je ne trouve pas ce qu’il me faut.
Voila donc un billet qui s’adresse aux intégrateurs de la part d’un développeur et qui lance un message : « Développez vos propres plugins ! ».
Voilà comment s’utilisent une majorité des plugins jQuery :
// Initialisation du plugin
$("#mon-element").monPlugin({
"des options": "et des valeurs",
// ...
});
// Appel de méthodes
$("#mon-element").monPlugin("le nom de ma methode", "ses", "arguments");
On a deux cas d’utilisation : l’initialisation et l’appel de méthode. Dans les deux cas,
on voit que l’appel se fait sur un objet jQuery
.
Pour cet article, je vais utiliser un plugin jQuery de démonstration. Ce plugin
permet d’afficher un calendrier et de mettre en avant la présence d’événements sur certains
jours. Dans la suite de l’article, c’est bCalendar
et pas monPlugin
qui sera utilisé.
Pour traiter les deux cas d’utilisation que nous avons vu dans la partie précédente, il faudra écrire le code ci-dessous :
$.fn.bCalendar = function(optionsOrMethod) {
var args = arguments;
if (typeof optionsOrMethod === "string")
this.each(function(_, element){
bCalendarMethodCall.apply($(element), args);
});
else
this.each(function(_, element){
bCalendarInit.apply($(element), args);
});
return this;
};
Ajouter une fonction bCalendar
à $.fn
permet de rendre cette fonction disponible sur
n’importe quel objet jQuery
comme $("#mon-element")
. Notre fonction bCalendar
sera
bindée avec notre objet jQuery sur lequel la fonction bCalendar
aura été
appelée.
Les fonctions auxiliaires que j’utilise (bCalendarInit
et bCalendarMethodCall
)
attendent un objet jQuery
ne contenant qu’un seul Element
. Pour traiter le cas ou
l’appel à bCalendar
se ferait sur plusieurs éléments, j’utilise each
.
À la fin de bCalendar
, on donne la valeur de retour : this
afin de pouvoir chaîner
les appels. Attention, selon les méthodes disponibles, vous voudrez peut être retourner
autre chose. Je pense à des accesseurs potentiels comme getMonth
qui devrait retourner
autre chose qu’un objet jQuery
.
De manière générale, on préfère minimiser le nombre de fonctions que l’on ajoute sur $.fn
.
C’est pour cela qu’on garde une seule fonction bCalendar
plutôt que deux fonctions
bCalendarInit
et bCalendarMethodCall
directement dans $.fn
. Ce dernier point relève
d’une convention, ce n’est pas une contrainte.
C’est tout, on a fait le tour du nécessaire pour que jQuery offre à l’utilisateur les interfaces classiques d’un plugin.
Je vous rassure, je ne m’arrête pas là. En effet, même si je vous ai présenté le strict minimum, il y a encore des astuces toutes aussi primordiales à connaître.
Dans mon exemple précédent j’ai utilisé directement la variable $
. Dans le cas où jQuery
est utilisé en mode sans conflit, $
est laissé inchangé. Pour permettre à votre plugin de
fonctionner aussi bien en mode normal qu’en mode sans conflit, il faut légèrement transformer
le code :
(function($){
// $.fn.bCalendar = ...
})(jQuery)
Quand le nombre d’option d’un plugin devient important, il est bon d’éviter à l’utilisateur de devoir saisir toutes les options. Dans ces cas là il faut fournir un ensemble d’options par défaut. C’est d’autant plus agréable lorsque l’on peut les modifier.
Voici le code qu’il est possible d’ajouter pour mettre à disposition ces options par défaut :
$.fn.bCalendar = function(optionsOrMethod) {
// ...
};
$.fn.bCalendar.defaults = {
begin: moment().date(1),
events: function(startAt, callback){ callback([]) },
eventClass: 'bc-event'
};
function bCalendarInit(options) {
options = $.extend({}, $.fn.bCalendar.defaults, options);
// ...
};
Cela permettra à l’utilisateur d’écrire ce type de code avant l’initialisation mais après le chargement du plugin :
$.fn.bCalendar.defaults.eventClass = "event";
C’est bien beau mais tout ce que je viens de montrer n’est que la glue nécessaire pour coller votre code dans jQuery. Voici quelques conseils pour organiser votre code.
Une pratique que je trouve intéressante est de créer un objet par Element
et surtout
de conserver cet objet tout au long de la vie de l’élément.
Voici l’intégralité de la fonction bCalendarInit
:
function bCalendarInit(options) {
options = $.extend({}, $.fn.bCalendar.defaults, options);
var calendar = new Calendar(this, options);
this.data('bCalendar', calendar);
};
On voit que je créé un objet à partir du constructeur Calendar
et que je l’associe
à l’objet jQuery
qui est bindé à ma fonction bCalendarInit
via la fonction data
.
La fonction data
permet en effet de valuer un data-attribute d’un élément. La valeur
de cet attribut peut être un objet JavaScript, aussi complexe soit-il.
L’avantage de ce type de pratique est qu’à partir de notre élément du DOM, on va pouvoir
accèder à notre objet Calendar
très facilement. C’est ce qu’on fait dans la fonction
bCalendarMethodCall
:
function bCalendarMethodCall() {
var calendar = this.data('bCalendar');
if (calendar) {
var methodName = arguments[0];
var methodArgs = arguments.slice(1);
return calendar[methodName].apply(calendar, methodArgs);
}
throw "Please init the bCalendar before calling methods on it.";
};
Cet objet Calendar
me permet d’encapsuler tout le comportement de mon plugin. Si je dois
créer des objets additionnels, utiliser des fonctions auxiliaires, ou autre alors ce
sera dans Calendar
.
Lorsque l’on fait un composant graphique dynamique on doit attacher des événements aux actions réalisées par l’utilisateur comme un clic sur un jour du calendrier. Dans ce cas, le calendrier va déclencher un événement que l’utilisateur va pouvoir utiliser. Voici un exemple d’une telle utilisation :
var calendar = $("#calendar-widget").bCalendar({
events: function(startAt, callback) {
callback([
{ date: moment(startAt).add(10, "days"), title: "Event 1" }
]);
}
});
// Utilisation d'un événement personnalisé
calendar.on('eventClicked', function(event, clickedEvent) {
console.log("Click on event:", clickedEvent);
alert(clickedEvent.title);
});
Pour arriver à ce résultat, voici le code que j’ai mis en place :
function Calendar(container, options) {
// ...
// this.table est l'élément 'table' qui est inséré dans
// le conteneur (#calendar-widget)
this.table.on("click", "tbody td.bc-event", function() {
var event = $(this).data("bcEvent");
container.trigger("eventClicked", [event]);
});
//...
};
Dans cet extrait de mon constructeur de Calendar
, j’attache un seul handler pour
l’événement. Une mauvaise solution aurait été d’attacher un handler sur chaque td
en utilisant la fermeture de chaque handler pour référencer le paramètre à envoyer
avec l’événement. Voici un comparatif des deux pratiques :
// events = [ { title: ... }, { ... }, ... ];
tdWithEvent.each(function(i, td) {
var td = $(td),
ev = events[i];
// Cas n°1 : Attacher un handler pour chaque cellule
td.on('click', function(){ container.trigger("eventClicked", [ev]); });
// Cas n°2 : Simplement passer par l'attribut data les informations utiles au handler
td.data("bcEvent", ev);
});
À la place d’utiliser la fermeture du handler pour passer un argument, j’utilise
l’attribut data bcEvent
donc le second cas dans le code ci dessus.
C’est tout pour cette introduction à la réalisation de plugin jQuery. Je vous invite à parcourir le code qui m’a servi de support pour cet article. Il est bien entendu perfectible et je suis ouvert aux pull-requests.
J’espère que cela vous encouragera à créer vos propres composants réutilisables et à les partager à la communauté.
L’équipe Synbioz.
Libres d’être ensemble.