Écrire ses propres plugins avec jQuery

Publié le 8 janvier 2014 par Nicolas Zermati | front

Cet article est publié sous licence CC BY-NC-SA

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.

Avant de commencer

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.

Partager et réutiliser

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 ! ».

Un plugin jQuery vu de l’extérieur

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.

Un plugin jQuery vu de l’intérieur

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.

C’est pas déjà la fin ?

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.

jQuery et pas $

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)

Définir des options par défaut

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";

Où mettre son code et comment l’organiser

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.

Gestion des évènements

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.

Conclusion

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.