Développer un plugin jQuery UI : événements

Bookmark and Share
Jeudi 11 août 2011
Posté par Gilles Felix

Développer un plugin jQuery UI : événements
Voici le quatrième chapitre de notre tutoriel. Aujourd'hui, intéressons-nous aux événements.

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

Notre bloc a pris forme. Il est maintenant ouvrable et fermable.

Imaginons qu'un autre développeur utilise le plugin et qu'il veuille déclencher une action à chaque fois que le bloc s'ouvre ou se ferme. En l'état, il serait obligé de modifier directement le code source du plugin (dans les méthodes _close et _open) pour y implanter ses actions. Une option pas franchement intelligente : à la prochaine mise à jour du plugin, il faudra qu'il refasse la même manipulation.

Facilitons la vie de ce pauvre développeur. Deux choix s'offrent à nous.

 

Ajoutons des callbacks

Les callbacks sont des fonctions javascript passées en paramètre, destinées à être exécutées lors d'un événement précis.

Ajoutons donc deux options à notre widget.

options: {
    title: 'Titre',
    togglable: true,
    opened : true, 
    open : null, // Variable de type callback à déclencher à l'ouverture
    close : null // Variable de type callback à déclencher à la fermeture
},

Modifions nos méthodes _open et _close pour tenir compte de ces options.

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

    // On vérifie si l'option close est bien une fonction
    if ($.isFunction(this.options.close)) {
        // Exécution de la fonction close avec comme contexte l'instance de notre widget (this)
        this.options.close.call(this); 
    }
},

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

    // On vérifie si l'option open est bien une fonction
    if ($.isFunction(this.options.open)) {
        // Exécution de la fonction open avec comme contexte l'instance de notre widget (this)
        this.options.open.call(this); 
    }
},

Et pour finir, modifions l'initialisation de notre bloc.

$('#monbloc1').bloc({
    title: 'Hello bloc 1',
    open: function() {
        // this représente l'instance de notre widget
        alert('Ouverture > ' + this.options.title);
    },
    close: function() {
        alert('Fermeture > ' + this.options.title);
    }
});

Je ne vous fais pas de démo mais ayez confiance, ça marche parfaitement.

Vous l'avez peut-être compris, la deuxième solution à ma préférence.

 

Ajoutons des triggers

Cette solution est légèrement différente. Plutôt que de déclencher les actions de l'intérieur du plugin quand les événements surviennent, envoyons juste une information à l'élément de notre widget comme quoi l'événement a eu lieu. L'avantage est qu'on n'est pas obligé de définir l'action à l'initialisation du widget. On peut le faire a posteriori aussi simplement que l'on intercepte un événement click sur un bouton (ce n'est pas tout à fait vrai, c'est également possible avec la première solution en utilisant le setter $('#monbloc').('open', function() {...}); mais c'est un peu plus compliqué et moins intuitif).

Pour envoyer le signal de l'événement, il existe la méthode trigger avec jQuery. Le framework jQuery UI, dans sa grande bonté, fournit la méthode _trigger qui en fait un peu plus. Que fait-elle de plus ?

  • Elle ajoute le nom de votre widget en préfixe du nom de l'événement (si vous déclenchez un événement nommé close, l'événement réellement propagé sera nomduwidgetclose). Pas de conflit possible avec d'autres événements pouvant être déclenchés par d'autres plugins ou par le framework.
  • Elle crée automatiquement un objet $.Event si vous n'en fournissez pas un.
  • And last but not least, elle vérifie si une option de type callback du même nom que votre événement sans le préfixe du widget existe. Si c'est le cas, elle l'exécute avec, comme contexte, l'élément de votre widget.

Mise en application, toujours dans nos deux méthodes _open et _close.

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

    // Envoi du signal de fermeture
    // _trigger accepte 3 paramètres, les deux derniers étant optionnels : 
    // - le nom de l'événement
    // - l'objet événement
    // - des données additionnelles envoyées aux fonctions interceptant l'événement
    this._trigger('close');
},

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

    // Envoi du signal d'ouverture
    this._trigger('open');
},

Le code d'initialisation de notre bloc fourni dans la première solution basée sur les callbacks fonctionne toujours. Mais, maintenant, on peut aussi faire ça :

$('#monbloc1').bloc({
    title: 'Hello bloc 1',
});
$('#monbloc1').bind('blocopen', function() {
    // this représente l'élément sur lequel est appliqué notre widget bloc
    alert('Ouverture > ' + $(this).text());
});
$('#monbloc1').bind('blocclose', function() {
    alert('Fermeture > ' +  $(this).text());
});

 

Osons l'interruptionnabilité !!

Je sens qu'il vous reste encore quelques neurones pour une dernière étape dans ce chapitre. L'utilisateur de notre plugin peut maintenant être averti de l'ouverture et de la fermeture du bloc. Bien ! Mais, ce qu'il veut, lui, c'est pouvoir arrêter l'ouverture et la fermeture s'il le décide.

_close: function() {
    // Si l'événement beforeClose retourne false, on arrête la fermeture
    if (false === this._trigger('beforeClose')) {
        return;
    }

    this.uiBlocContainer.children().not(this.uiBlocTitle).hide();
    this.uiBlocTitleToggle.removeClass('ui-icon-pin-s').addClass('ui-icon-pin-w');

    this._trigger('close');
},

_open: function() {
    // Si l'événement beforeOpen retourne false, on arrête l'ouverture
    if (false === this._trigger('beforeOpen')) {
        return;
    }

    this.uiBlocContainer.children().show();
    this.uiBlocTitleToggle.removeClass('ui-icon-pin-w').addClass('ui-icon-pin-s');

    this._trigger('open');
},

MAJ du 17/08/2011

Petit bug dans l'ajout des événements beforeClose et beforeOpen : si les événements renvoient false, les méthodes − respectivement  _close ou _open − doivent renvoyer false aussi et non pas simplement return;. De cette façon, la méthode toggle peut, elle aussi, tester le retour de _open et _close et s'arrêter s'il le faut.

toggle : function() {
    var self = this;

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

    if (self.options.opened) {
        // Si _close renvoie false, arrêt de l'exécution de toggle 
        if (!self._close()) {
            return;
        }
    } else {
        // Si _open renvoie false, arrêt de l'exécution de toggle 
        if (!self._open()) {
            return;
        }
    }
    self.options.opened = !self.options.opened;

    return self;
},

MAJ du 24/08/2011

Le patch du 17 août corrigeait un bug mais il en a introduit un autre. Les méthodes _open et _close ne faisaient aucun return final, toggle interprétait ça comme un false et s'arrêtait. Résultat : le premier toggle marchait mais les suivants non. Je ne vous fais pas l'affichage du bout de code : ajouter simplement return true; à la fin des méthodes _open et _close.

 

Corrigeons un effet de bord

Je ne vous cache rien. En l'état, ce plugin a un petit bug. La méthode _close est appelée lors de la fermeture du bloc, mais également à l'initialisation si l'utilisateur a demandé à ce que le bloc soit fermé par défaut. S'il a, en plus, ajouté des options close ou beforeClose pour exécuter des callbacks aux déclenchements de ces événements, ces callbacks s'exécuteraient aussi à la création du bloc. C'est tout de même un peu ballot. Corrigeons rapidement ça.

_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._title();

    if (this.options.togglable && !this.options.opened) {
        // Le paramètre true est là pour indiquer à la méthode _close qu'elle est appelée par la création du widget
        this._close(true);
    }
},

_close: function(isCreate) {
    // Pas d'événement si c'est la création du widget
    if (!isCreate && false === this._trigger('beforeClose')) {
        return;
    }

    this.uiBlocContainer.children().not(this.uiBlocTitle).hide();
    this.uiBlocTitleToggle.removeClass('ui-icon-pin-s').addClass('ui-icon-pin-w');

    if (!isCreate) {
        this._trigger('close');
    }
},

 

La démo, la démo !!!

Juste une précision avant la démo. Quand vous interceptez vos événements, le nom de l'événement est en minuscule. Dans notre cas, il faut donc intercepter blocbeforeopen et non blocbeforeOpen.

 

La suite : fonctionnalités avancées

Passons à la cinquième étape, les fonctionnalités avancées
Aucun commentaire
Laissez votre commentaire :