Développer un plugin jQuery UI : fonctionnalités avancées

Bookmark and Share
Mercredi 24 août 2011
Posté par Gilles Felix

Développer un plugin jQuery UI : fonctionnalités avancées
Les 4 premiers chapitres nous ont permis de faire le tour des fonctionnalités principales du framework de création de widgets jQuery UI. Voyons aujourd'hui les fonctionnalités avancées.

Sommaire du tutoriel
Développer un plugin jQuery UI

  1. Introduction
  2. Mise en forme
  3. Options et méthodes
  4. Événements
  5. Fonctionnalités avancées
  6. Héritage

Le prototype pour widgets jQuery UI implémente par défaut plusieurs méthodes qui ont vocation à être étendues. Libre à vous de les implémenter ou non. Nous avons déjà vu la méthode _create, mieux vaut l'implémenter sans quoi notre widget ne ferait pas grand chose. Mais il y a aussi _init, destroy, enable, disable et option (les descriptions qui suivent sont fortement inspirées de la documentation jQuery UI) :

  • _init() : Peut facilement être confondu avec _create. À la création d'un widget, _create puis _init  sont exécutées successivement. Si nous ré-exécutons notre widget sur un élément du DOM déjà instancié, seul _init est appelée.
  • destroy() : Supprime l'instance du widget qui a été stocké en data dans notre élément du DOM. Le retour de l'élément du DOM, sur lequel est basé notre widget, dans l'état où il était avant la création du widget est à notre charge.
  • option(String key[, String value]) ou option(JSON {key1 : value1, key2 : value2} ) : Renvoi ou modifie une ou plusieurs options de notre widget.
  • enable() : Met l'option disabled à false. Le widget est libre de respecter ou non cette option.
  • disable() : Met l'option disabled à true. Le widget est libre de respecter ou non cette option.

Rien ne vous oblige à implémenter toutes ces méthodes. Si vous le faites ce sont des possibilités supplémentaires que vous offrez aux utilisateurs de votre plugin. Nous imaginons rarement tout ce que les utilisateurs peuvent vouloir faire avec nos créations. Vous ne voyez peut-être pas l'utilité de pouvoir réinitialiser, détruire ou désactiver votre widget mais il y a de fortes chances pour que certains de vos utilisateurs en aient besoin.

Voyons maintenant tout cela dans le détail en essayant de trouver des applications fonctionnelles sur notre exemple fil rouge. Je vous laisse juge du résultat dans la démo en fin d'article.

 

_init

Je m'auto-cite : « À la création d'un widget _create puis _init  sont exécutées successivement. Si nous ré-exécutons notre widget sur un élément du DOM déjà instancié, seul _init est appelée. »

Si nous poussons le résonnement, _create doit contenir la création d'éléments indispensables à notre widget mais indépendants de toutes options. A l'inverse _init doit juste se contenter de modifications sur le widget en fonction des options.

Dans le cadre de notre exemple cela veut donc dire que l'encapsulation de notre élément dans un container et que la création du bloc pour le titre doivent être dans _create. Par contre la modification du contenu du titre et l'application du toggle, qui eux dépendent d'options, doivent se faire dans l'_init.

Reprenons notre code.

// La fonction _create est appelée à la construction du widget
_create: function() {
    this.uiBlocContainer = $('<div></div>')
        .addClass('ui-bloc ui-widget ui-widget-content ui-corner-all')
        .insertAfter(this.element);

    this.element.addClass('ui-bloc-content').appendTo(this.uiBlocContainer);

    this.uiBlocTitle = $('<h5></h5>').addClass('ui-bloc-title ui-widget-header ui-corner-top')
        .prependTo(this.uiBlocContainer);

    // On encapsule un SPAN dans le bloc titre pour y écrire le titre et pouvoir le modifier à posteriori
    $('<span></span>').appendTo(this.uiBlocTitle);

    // On ajoute un SPAN au bloc titre pour le picto indicateur de l'état d'ouverture / fermeture
    // Mais on le cache au cas où la fonctionnalité serait désactivée
    this.uiBlocTitleToggle = $('<span></span>')
        .addClass('ui-bloc-title-toggle ui-icon ui-icon-pin-s')
        .appendTo(this.uiBlocTitle)
        .hide();
},

// La fonction _init est appelée à la construction ET à la réinitialisation du widget
_init : function() {
    var self = this;

    // On renseigne le texte du titre avec la valeur présente dans les options
    self.uiBlocTitle.children('span:first').text(self.options.title);

    // On enlève l'événement click sur le bloc titre
    // En cas de réinitialisation, cela évite d'ajouter un nouvel événement click
    // à notre titre qui en a déjà potentiellement un
    self.uiBlocTitle.unbind('click');

    if (self.options.togglable) {
        // On ajoute l'événement click au bloc titre si le bloc est ouvrable / fermable
        self.uiBlocTitle.click(function(event) {
            self.toggle(event);
            return false;
        }).css('cursor', 'pointer'); // On modifie le curseur au survol du titre

        // On affiche le picto indicateur de l'état d'ouverture / fermeture
        // précédemment crée dans _create
        self.uiBlocTitleToggle.show();

        if (!self.options.opened) {
            // Si le bloc est initialisé avec le paramètre opened à false, on ferme le bloc
            self._close();
        } else {
            // Si le bloc est initialisé avec le paramètre opened à true, on ouvre le bloc
            self._open();
        }
    } else {
        // Le bloc n'est pas togglable
        // On réinitialise le curseur au survol du titre
        self.uiBlocTitle.css('cursor', 'auto');
        // On cache le picto indicateur de l'état d'ouverture / fermeture
        self.uiBlocTitleToggle.hide();
        // On ouvre le bloc
        self._open();
    }
},

toggle : function() {
    var self = this;

    if (!self.options.togglable) {
        return self;
    }

    if (self.options.opened) {
        if (false === this._trigger('beforeClose')) {
            return false;
        }

        self._close();

        this._trigger('close');
    } else {
        if (false === this._trigger('beforeOpen')) {
            return false;
        }

        self._open();

        this._trigger('open');
    }
    self.options.opened = !self.options.opened;

    return self;
},

_close: function() {
    this.uiBlocContainer.children().not(this.uiBlocTitle).hide();
    this.uiBlocTitleToggle.removeClass('ui-icon-pin-s').addClass('ui-icon-pin-w');
},

_open: function() {
    this.uiBlocContainer.children().show();
    this.uiBlocTitleToggle.removeClass('ui-icon-pin-w').addClass('ui-icon-pin-s');
}

A noter que la méthode _title − elle encapsulait la création et l'initialisation du bloc titre − a été purement et simplement détruite.

 

destroy

Maintenant que l'on a bien séparé ce qui était de la création et de l'initialisation, la méthode destroy revient à défaire ce que fait _create.

Allons-y gaiement.

// La fonction destroy ramène l'élément du DOM, sur lequel est basé notre widget,
// dans l'état où il était avant la création du widget.
// Elle défait ce que _create a fait
destroy: function() {
    // On réaffiche l'élément éventuellement caché
    // On enlève les classes css propres au widget
    // Et on sort l'élément du container
    this.element.show()
        .removeClass('ui-bloc-content')
        .insertBefore(this.uiBlocContainer);

    // On détruit le container
    // Ce qui détruit par ricochet tous les autres éléments créés par notre widget
    this.uiBlocContainer.remove();

    // On appelle la méthode originale du framework
    // Elle supprime l'instance du widget qui a été stocké en data dans l'élément
    $.Widget.prototype.destroy.apply(this);

    return this;
},

 

option

Cette méthode sert aussi bien de setter que de getter. Elle peut traiter une seule option ou plusieurs en même temps.

  • option() : renvoie le JSON des options de notre widget
  • option(String key) : renvoie la valeur de l'option key
  • option(String key, value) : met à jour l'option key avec la valeur value dans le JSON d'options de notre widget
  • option(JSON {key1 : value1, key2 : value2}) : fusionne le JSON passé en paramètre avec le JSON d'options de notre widget 

En mode setter, l'implémentation par défaut de la méthode option dans le framework jQuery UI se contente de modifier la ou les options. Si ces modifications impliquent une mise à jour de votre widget, c'est à vous de surcharger option pour répercuter ces modifications − en n'oubliant pas d'appeler la méthode originale. En fait, la méthode option du framework appelle 2 autres méthodes : _setOptions quand le paramètre est un JSON et _setOption quand le premier paramètre est une chaîne. _setOptions appelle elle-même _setOption pour chaque clé du JSON. Ce n'est donc pas option qu'il faut surcharger mais _setOption.

// Surcharge de la méthode _setOption qui est appelée par la méthode option
// qui permet de modifier des options de notre widget
_setOption: function(key, value){
    var self = this;

    // On appelle la méthode originale du framework qui modifie le tableau d'options
    $.Widget.prototype._setOption.apply(self, arguments);

    if ($.inArray(key, ['title', 'togglable', 'opened']) != -1) {
        // Si l'option modifiée est une des 3 options title, togglable, opened
        // On appelle la méthode d'initialisation
        self._init();
    }
},

Notre ancienne méthode title − elle agissait comme un setter / getter  sur l'option title − ne sert plus à rien : Poubelle !

 

enable / disable

Par défaut dans le framework jQuery UI, ces deux méthodes font deux choses :

  • Elles modifient l'option disabled respectivement à false et à true
  • Elles suppriment / ajoutent une classe css ui-state-disabled sur notre élément

Si notre élément de base du widget servait lui-même de container aux éléments créés par le widget, l'aspect visuel du widget pourrait simplement être changé via la classe ui-state-disabled.

Mais avant d'aller plus loin il nous faut définir ce que signifie "désactivé notre widget". Si notre widget était juste un bouton, ça pourrait être un simple grisage et la désactivation du clic. Dans notre cas je propose :

  • Grisage du bloc (titre et contenu)
  • Désactivation du clic sur le titre dans le cas où le bloc serait togglable

Plutôt que de redéfinir enable et disable, nous allons utiliser l'option disabled gérée nativement. On va donc revenir dans _setOption pour le grisage.

_setOption: function(key, value){
    var self = this;

    $.Widget.prototype._setOption.apply(self, arguments);

    if ($.inArray(key, ['title', 'togglable', 'opened']) != -1) {
        self._init();
    } else if (key === 'disabled') {
        // L'option disabled a été modifiée
        // On ajoute ou supprime, en fonction du cas, la classe ui-state-disabled au container
        // Dans le framework css de jQuery UI, la classe ui-state-disabled grise un élément
        if (value) {
            this.uiBlocContainer.addClass('ui-state-disabled');
        } else {
            this.uiBlocContainer.removeClass('ui-state-disabled');
        }
    }
},

Pour la désactivation du clic sur le titre on va se contenter de modifier l'événement click définit dans _init.

self.uiBlocTitle.click(function(event) {
    // Si le widget est disabled, il ne se passe rien au click
    if (!self.options.disabled) {
        self.toggle(event);
        return false;
    }
});

 

La démo !

Comme promis la démo de toutes ces nouvelles fonctionnalités.

 

La suite : héritage

Sixième chapitre : héritage
3 commentaires
  • Commentaire par molokoloco
    Vendredi 26 août 2011 22:12
    Hello,

    Très bon code, seulement je ne sais pas si je souhaite manipuler autant de "this".
    "this" a pas mal d'incohérence en JavaScript cf. :
    http://code.google.com/p/molokoloco-coding-project/wiki/JavascriptBase#THIS_is_what_?
    Ensuite, sur la base du code d'un plugin jQuery 1.6, j'ai été inspiré par les "data" que l'on peut associer sur les éléments DOM.
    http://jsfiddle.net/molokoloco/DzYdE/ (exemple)
    Un autre argument, c'est que cela permet à d'autre plugin de venir lire les infos associées avec n'importe quel noeud html (toggled, id, ...) et donc de mélanger plus facilement les scripts...

    Non ?
  • Commentaire par Gilles FELIX
    Lundi 29 août 2011 09:31
    Bonjour,

    Heureux d'avoir un commentaire, qui plus est constructif !!

    Pour le this je ne suis pas tout à fait d'accord. On peut certes se perdre très vite dans les méandres des contextes d'exécutions de fonctions en javascript mais c'est autant un problème qu'une force. Le this permet de modifier l'instance, sinon il faudrait passer l'objet en paramètre, le modifier puis le renvoyer en fin de fonction pour pouvoir le réassignier en sortie de fonction.

    Pour le 2ème point je suis 100% d'accord mais là tu empiètes sur de potentiels futurs tutos :-) C'est toujours le même problème : fixer les prérequis implicites, les limites de coding pour rester lisible, le tout en fonction de l'objectif. Promis je rajouterais une couche sur les des data.

    A+

    Gilles FELIX
  • Commentaire par Mathieu ROBIN
    Lundi 29 août 2011 10:27
    @molokoloco : en fait, this semble un mot clé dangereux parce que certaines notions de Javascript sont mal maîtrisées voire carrément inconnues. En fait, la notion de portée en JS est un peu différente de la notion de portée classique, ce qui rend le mot clé this un poil différent dans sa valeur tout au long d'un code. Forcément, ça complexifie la réflexion parce qu'il faut penser autrement au moment de coder en JS mais ça relève vite ensuite du domaine du réflexe.

    Je regarderai le post plus en détails plus tard. Pas le temps là malheureusement même si ça a l'air particulièrement intéressant.
Laissez votre commentaire :