Estensioni

Cos'è un estensione?
Un’estensione di jQuery è semplicemente un nuovo metodo da utilizzare per estendere il prototipo (prototype) dell’oggetto jQuery. Quando si estende il prototipo, tutti gli oggetti jQuery ereditano i metodi aggiunti. Pertanto, quando viene effettuata una chiamata jQuery(), viene creato un nuovo oggetto jQuery con tutti i metodi ereditati.

Lo scopo di un’estensione è di eseguire un’azione utilizzando un insieme di elementi, proprio come fanno, per esempio, i metodi fadeOut o addClass della libreria.

È possibile effettuare le proprie estensioni e usarle privatamente nel progetto o è anche possibile pubblicarli in modo che altri potranno usufruire.

Come creare di una estensione di base
Il codice per creare un’estensione di base è il seguente:
(function($){
    $.fn.myNewPlugin = function() {
        return this.each(function(){
            // fare qualcosa
        });
    };
}(jQuery));
L’estensione del prototipo dell’oggetto jQuery si verifica nella seguente riga:
$.fn.myNewPlugin = function() { //...
Il quale è racchiusa in una funzione auto-eseguibile
(function($){
    //...
}(jQuery));
Questo ha il vantaggio di creare un’ambito “privato”, che consente di utilizzare il simbolo del dollaro, senza timore che un’altra libreria utilizzi anche lo stesso segno.

Per ora, internamente l’estensione è:

$.fn.myNewPlugin = function() {
    return this.each(function(){
        // fare qualcosa
    });
};
In esso, la parola chiave this si riferisce all’oggetto jQuery in cui è chiamata l’estensione.
var somejQueryObject = $('#something');

$.fn.myNewPlugin = function() {
    alert(this === somejQueryObject);
};

somejQueryObject.myNewPlugin(); 
// mostra un avviso con  'true'
L’oggetto jQuery di solito contiene diversi riferimenti agli elementi DOM, ed è per questo che sono spesso definiti come una collezione.

Per interagire con l’insieme di elementi, è necessario fare un ciclo, che è facilmente ottenibile con il metodo each():

$.fn.myNewPlugin = function() {
    return this.each(function(){

    });
};
Come gli altri metodi, each() restituisce un oggetto jQuery, permettendo il concatenamento dei metodi utilizzati ($(…).css().attr()…). Per evitare di rompere questa convenzione, l’estensione da creare dovrà ritornare l’oggetto this per consentire di continuare con la catena. Di seguito è riportato un piccolo esempio:
(function($){
    $.fn.showLinkLocation = function() {
        return this.filter('a').each(function(){
            $(this).append(
                ' (' + $(this).attr('href') + ')'
            );
        });
    };
}(jQuery));

// Esempio di utilizzo:
$('a').showLinkLocation();
L’estensione modificherà tutti i link all’interno della collezione di oggetti e aggiungerà il valore attributo del suo attributo href tra parentesi.
<!-- Prima che l'estensione sia chiamata: -->
<a href="page.html">Foo</a>

<!-- Dopo che l'estensione sia chiamata: -->
<a href="page.html">Foo (page.html)</a>
È anche possibile ottimizzare l’estensione:
(function($){
    $.fn.showLinkLocation = function() {
        return this.filter('a').append(function(){
              return ' (' + this.href + ')';
        });
    };
}(jQuery));
Il metodo append permette specificare una funzione di ritorno (callback), e il valore di ritorno determinerà ciò che verrà aggiunto ad ogni elemento. Si noti anche che non viene utilizzato il metodo attr, perché l’API nativa del DOM consente un facile accesso alla proprietà href.

Di seguito è riportato un altro eEsempio di estensione. In questo caso, non è tenuto a fare un ciclo su ogni elemento perché la funzionalità è delegata direttamente in un altro metodo di jQuery:

(function($){
    $.fn.fadeInAndAddClass = function(duration, className) {
        return this.fadeIn(duration, function(){
            $(this).addClass(className);
        });
    };
}(jQuery));

// Esempio di utilizzo:
$('a').fadeInAndAddClass(400, 'finishedFading');
Individuare e valutare estensioni
Uno degli aspetti più popolari di jQuery è la diversità di estensioni che esistono.

Tuttavia, la qualità può variare notevolmente tra le estensioni. Molti sono ampiamente testati e ben mantenute, ma alcuni sono state create in fretta e poi ignorate, senza seguire le buone prassi.

Google è il miglior strumento per trovare le estensioni (anche se il team di jQuery sta lavorando per migliorare la sua repository di estensioni). Una volta trovata l’estensione, si consiglia di consultare la newsletter di jQuery o il canale IRC #jquery per ottenere il parere di altri circa l’estensione.

Assicurarsi che l’estensione sia ben documentata, e che offono esempi di utilizzo. Stare attenti anche con le estensioni che fanno più del necessario, queste possono arrivare a sovraccaricare la pagina. Per ulteriori suggerimenti su come individuare un’estensione mediocre, è possibile leggere l’articolo (in inglese) I segni di un plugin per jQuery scritto male da Remy Sharp (http://remysharp.com/2010/06/03/signs-of-a-poorly-written-jquery-plugin/).

Dopo aver selezionato l’estensione, è necessario aggiungerla alla pagina. In primo luogo, scaricare l’estensione, scompattarla (se necessario) e spostarla nella cartella dell’applicazione. Infine inserirla utilizzando l’elemento script (dopo l’inclusione di jQuery).

Scrivere estensioni
A volte si vuole fare una funzionalità disponibile in tutto il codice, per esempio, un metodo che possa essere chiamato da una selezione il quale realizzi una serie di operazioni.

La maggior parte delle estensioni sono metodi creati all’interno del namespace $.fn. jQuery assicura che un metodo chiamato sull’oggetto jQuery sia in grado di accedere a detto oggetto attraverso this. In contrasto, l’estensione dovrebbe garantire per restituire lo stesso oggetto ricevuto (salvo diversa indicazione).

Di seguito si mostra un esempio:
8.1: Creare una estensione per aggiungere e rimuovere una clesse in un elemento quando accade l'evento hover

// definizione dell'estensione
(function($){
    $.fn.hoverClass = function(c) {
        return this.hover(
            function() { $(this).toggleClass(c); }
        );
    };
})(jQuery);

// utilizzare l'estensione
$('li').hoverClass('hover');
Per ulteriori informazioni sullo sviluppo di estensioni, vedere l’articolo (in inglese) A Plugin Development Pattern di Mike Alsup.
In questo articolo, si sviluppa un’estensione chiamata $.fn.hilight, che fornisce un supporto per l’estensione metadata (se presente) e fornisce un metodo decentrato per impostare opzioni globali o istanze dell’estensione.
8.2: Il modello di sviluppo di estensioni per jQuery spiegato da Mike Alsup
//
// creare una chiusura
//
(function($) {
  //
  // definizione dell'estensione
  //
  $.fn.hilight = function(options) {
    debug(this);
    // generazione delle opzioni principali 
    // prima di interagire
    var opts = $.extend({}, $.fn.hilight.defaults, options);
    // ittera e riformatta ogni elemento
    return this.each(function() {
      $this = $(this);
      // generazione delle opzioni specifiche 
      // per ogni elemento
      var o = $.meta ? $.extend({}, opts, $this.data()) : opts;
      // aggiornamento degli stili per ogni elemento
      $this.css({
        backgroundColor: o.background,
        color: o.foreground
      });
      var markup = $this.html();
      // si chiama la funzione per la formattazione
      markup = $.fn.hilight.format(markup);
      $this.html(markup);
    });
  };
  //
  // funzione privata per la depurazione
  //
  function debug($obj) {
    if (window.console && window.console.log)
      window.console.log('hilight selection count: ' + $obj.size());
  };
  //
  // definire ed esporre la funzione di formattazione
  //
  $.fn.hilight.format = function(txt) {
    return '<strong>' + txt + '</strong>';
  };
  //
  // opzioni predefinite
  //
  $.fn.hilight.defaults = {
    foreground: 'red',
    background: 'yellow'
  };
//
// fine della chiusura
//
})(jQuery);
Scrivere estensioni con mantenimento di stato utilizzando Widget Factory di jQuery UI
Questa sezione è basata, con il permesso dell’autore, dall’articolo Building Stateful jQuery Plugins di Scott Gonzalez.

Mentre la maggior parte delle estensioni per jQuery sono esenti da manutenzione dello stato (stateless in inglese) – vale a dire, estensioni che vengono eseguiti su un solo elemento, essendo l’unica interazione – c’è un grande numero di funzionalità che non si utilizzano nello schema di base dove si sviluppano estensioni.

Per colmare questa lacuna, jQuery UI (jQuery User Interface) ha implementato un sistema più avanzato di estensioni. Questo sistema permette di gestire gli stati e ammette più funzioni da essere esposte in una singola estensione. Questo sistema è chiamato widget factory e fa parte della versione 1.8 di jQuery UI attraverso jQuery.widget, ma può essere utilizzato anche senza dipendere da jQuery UI.

Per dimostrare le capacità di widget factory, si creerà un’estensione che avrà come funzionalità sarà barra di avanzamento.

Per ora, l’estensione permetterà solo d’impostare il valore della barra di avanzamento una volta. Questo sarà fatto chiamando jQuery.widget con due parametri: il nome dell’estensione da creare e un oggetto letterale contenente le funzioni supportate. Quando l’estensione sarà chiamata, una sua istanza verrà creata e tutte le funzioni si eseguiranno nel contesto di tale istanza.

Ci sono due differenze importanti rispetto a una estensione standard per jQuery: in primo luogo, il contesto è un oggetto, non un elemento DOM; in secondo luogo, il contesto è sempre un unico oggetto, mai una collezione.
8.3: Una semplice estensione con mantenimento dello stato utilizzando widget factory di jQuery UI

$.widget("nmk.progressbar", {
    _create: function() {
        var progress = this.options.value + "%";
        this.element
            .addClass("progressbar")
            .text(progress);
    }
});
Il nome dell’estensione deve contenere uno spazio di nomi, in questo caso viene utilizzato nmk. Gli spazi di nomi sono limitati ad un solo livello di profondità – il che significa, per esempio, che non poò essere utilizzato nmk.foo. Come si può vedere nell’esempio, widget factory fornisce due proprietà da utilizzare. La prima, this.element è un oggetto jQuery che contiene esattamente un elemento. Nel caso in cui l’estensione venga eseguita su più di una voce, un’istanza separata dell’estensione verrà creata per ogni elemento e ciascuno avrà il suo propria this.element. La seconda proprietà, this.options, è un insieme di coppie chiave/valore con tutte le opzioni dell’estensione. Queste opzioni possono essere passate all’estensione come segue:

Quando si creano le proprie estensioni si consiglia di utilizzare il proprio spazio di nomi, giacchhé mette in chiaro da dove proviene l’estensione e se fa parte di una collezione più ampia. D’altra parte, lo spazio dei nomi ui è riservato per le estensioni ufficiali di jQuery UI.
8.4: Passare opzioni al widget
$("<div></div>")
    .appendTo( "body" )
    .progressbar({ value: 20 });
Quando si chiama a jQuery.widget si estende jQuery aggiungendo il metodo a jQuery.fn (nello stesso modo come quando si crea una estensione standard). Il nome della funzione che viene aggiunto è basato sul nome che si passa a jQuery.widget, senza lo spazio dei nomi (in questo caso il nome sarà jQuery.fn.progressbar).

Come si mostra sotto, è possibile specificare valori predefiniti per ogni opzione. Questi valori dovrebbero essere basati sull’uso più comune dell’estensione.
8.5: Impostare le opzioni di default per un widget

$.widget("nmk.progressbar", {
    // opzioni predefinite
    options: {
        value: 0
    },

    _create: function() {
        var progress = this.options.value + "%";
        this.element
            .addClass( "progressbar" )
            .text( progress );
    }
});
Aggiungere metodi a un Widget
Ora è possibile inizializzare l’estensione, è necessario aggiungere le capacità di eseguire azioni attraverso metodi definiti nell’estensione. Per definire un metodo nell’estensione è necessario includere la funzione di retrochiamata nell’oggetto letterale da passare a jQuery.widget. I metodi possono anche essere definiti “privati” anteponendo un carattere di sottolineatura al nome della funzione.
8.6: Creare metodi nel Widget
$.widget("nmk.progressbar", {
    options: {
        value: 0
    },

    _create: function() {
        var progress = this.options.value + "%";
        this.element
            .addClass("progressbar")
            .text(progress);
    },

    // crea un metodo pubblico
    value: function(value) {
        // non si passa alcun valore, 
        // agisce quindi come metodo ottenitore (getter)
        if (value === undefined) {
            return this.options.value;
        // passando un valore, agisce quindi 
        // come metodo impostatore (setter)
        } else {
            this.options.value = this._constrain(value);
            var progress = this.options.value + "%";
            this.element.text(progress);
        }
    },

    // crea un metodo privato
    _constrain: function(value) {
        if (value > 100) {
            value = 100;
        }
        if (value < 0) {
            value = 0;
        }
        return value;
    }
});
Per chiamare un metodo su un’istanza dell’estensione, è necessario passare il nome del metodo all’estensione. Se si chiama un metodo che accetta parametri questi devono essere passati subito dopo il nome del metodo.
8.7: Chiamare i metodi in un'istanza dell'estensione
var bar = $("<div></div>")
    .appendTo("body")
    .progressbar({ value: 20 });

// ottiene il valore corrente
alert(bar.progressbar("value"));

// aggiorna il valore
bar.progressbar("value", 50);

// ottiene di nuovo il valore
alert(bar.progressbar("value"));
Eseguire il metodo passando il nome del metodo allla stessa funzione jQuery che viene utilizzata per inizializzare l’estensione può sembrare strano, ma è fatto così per evitare “le contaminazioni” dello spazio di nomi di jQuery, mantenendo, allo stesso tempo, la possibilità di chiamare i metodi in catena o concatenati.
Lavorare con le opzioni del Widget
Uno dei metodi disponibili automaticamente per l’estensione è option. Questo metodo permette di ottenere e impostare opzioni dopo l’inizializzazione e funziona esattamente come i metodi attr e css di jQuery: passando unicamente un nome come argomento il metodo funziona come un getter, invece passando uno o più insiemi di coppie di nomi e valori il metodo funziona come setter. Se utilizzato come metodo getter, l’estensione restituirà il valore corrente dell’opzione corrispondente al nome passato come argomento. D’altra parte, se usato come metodo setter, il metodo _setOption dell’estensione sarà chiamato da ogni opzione che si desidera impostare.
8.8: Risponde quando l'opzione è impostata
$.widget("nmk.progressbar", {
    options: {
        value: 0
    },

    _create: function() {
        this.element.addClass("progressbar");
        this._update();
    },

    _setOption: function(key, value) {
        this.options[key] = value;
        this._update();
    },

    _update: function() {
        var progress = this.options.value + "%";
        this.element.text(progress);
    }
});
Aggiungere funzioni di callback
Uno dei modi più semplici per estendere un’estensione è quello di aggiungere funzioni di callback, in modo che l’utente possa reagire quando lo stato dell’estensione cambia. Di seguito viene riportato come aggiungere una funzione di callback all’estensione creata per indicare quando la barra di progresso raggiunge il 100%. Il metodo _trigger riceve tre parametri: il nome della funzione di callback, l’oggetto evento nativo che inizializza la funzione di callback e una serie di informazioni relative all’evento. Il nome della funzione di retrochiamata (callback in inglese) è l’unico parametro richiesto, ma altre possono essere molto utile se l’utente vuole implementare funzionalità personalizzate.
8.9: Fornire funzioni di (callback) retrochiamata
$.widget("nmk.progressbar", {
    options: {
        value: 0
    },

    _create: function() {
        this.element.addClass("progressbar");
        this._update();
    },

    _setOption: function(key, value) {
        this.options[key] = value;
        this._update();
    },

    _update: function() {
        var progress = this.options.value + "%";
        this.element.text(progress);
        if (this.options.value == 100) {
            this._trigger("complete", null, { value: 100 });
        }
    }
});
Le funzioni di callback sono essenzialmente solo opzioni aggiuntive e possono essere impostate come qualsiasi altra opzione. Ogni volta che una funzione di callback viene eseguita, allo stesso tempo si attiva un evento corrispondente. Il tipo di evento si determina concatenando il nome dell’estensione e il nome della funzione di callback. Questa funzione ed evento ricevono gli stessi parametri: un oggetto evento e una serie di informazioni relative all’evento.

Se l’estensione avrà qualche funzionalità che potrebbero essere annullate dall’utente, il modo migliore è quello di creare funzioni di callback annullabile. L’utente può annullare una funzione di callback o il suo evento associato nello stesso modo che viene annullato qualunque evento nativo: chiamando a event.preventDefault() o usando return false.
8.10: Vincolare a eventi del widget

var bar = $("<div></div>")
    .appendTo("body")
    .progressbar({
        complete: function(event, data) {
            alert( "Funzione di callback" );
        }
    })
    .bind("progressbarcomplete", function(event, data) {
        alert("Eventi bolla e molti gestori di supporto per una flessibilità estrema.");
        alert("Il valore della barra di avanzamento è " + data.value);
    });

bar.progressbar("option", "value", 100);
In profondità: Widget Factory
Quando si chiama a jQuery.widget, questa crea una funzione costruttrice per l’estensione e imposta l’oggetto letterale che viene passato come il prototipo per tutte le istanze dell’l’estensione. Tutte le caratteristiche che automaticamente si aggiungono all’estensione provvengono dal prototipo base del widget, il quale è definito come jQuery.Widget.prototype. Quando si crea un’istanza dell’estensione, questa viene salvata nell’elemento originale del DOM utilizzando jQuery.data, con il nome dell’estensione come parola chiave.

Perché l’istanza dell’estensione è direttamente legata all’elemento DOM, è possibile accedere all’istanza dell’estensione direttamente. Questo permette di chiamare i metodi direttamente nell’istanza dell’estensione invece di passare il nome del metodo come una stringa, dando la possibilità di accedere alle proprietà dell’estensione.

var bar = $("<div></div>")
    .appendTo("body")
    .progressbar()
    .data("progressbar" );

// chiamare un metodo direttamente 
// nell'istanza dell'estensione
bar.option("value", 50);

// accedere alle proprietà 
// nell'istanza dell'estensione
alert(bar.options.value);
Uno dei maggiori vantaggi di avere un costruttore e un prototipo per l’estensione è la facilità di estendere l’estensione. Il fatto di aggiungere o modificare metodi nel prototipo dell’estensione permette di modificarli anche in tutte le istanze dell’estensione. Per esempio, se vogliamo aggiungere un metodo all’estensione della barra di avanzamento per permettere di resettare il progresso a 0%, è possibile farlo aggiungendo questo metodo al prototipo e sarà automaticamente disponibile per essere chiamato da qualsiasi istanza dell’estensione.
$.nmk.progressbar.prototype.reset = function() {
    this._setOption("value", 0);
};
Pulizia
In alcuni casi, ha senso consentire agli utenti di applicare e disapplicare l’estensione. Questo può essere fatto attraverso il metodo destroy. Con questo metodo, è possibile annullare tutto quello che si è fatto con l’estensione. Questo metodo è anche chiamato automaticamente se l’elemento vincolato all’estensione viene rimosso dal DOM (e così è anche possibile usarlo per la raccolta di spazzatura o in inglese “garbage collection”). Il metodo destroy predefinito rimuove il collegamento tra l’elemento DOM e l’istanza dell’estensione.
8.11: Aggiungere un metodo destroy al widget
$.widget( "nmk.progressbar", {
    options: {
        value: 0
    },

    _create: function() {
        this.element.addClass("progressbar");
        this._update();
    },

    _setOption: function(key, value) {
        this.options[key] = value;
        this._update();
    },

    _update: function() {
        var progress = this.options.value + "%";
        this.element.text(progress);
        if (this.options.value == 100 ) {
            this._trigger("complete", null, { value: 100 });
        }
    },

    destroy: function() {
        this.element
            .removeClass("progressbar")
            .text("");

        // chiama alla funzione base destroy
        $.Widget.prototype.destroy.call(this);
    }
});
Conclusione
L’utilizzo di Widget factory è solo un modo per creare estensioni con la manutenzione dello stato. Ci sono modelli diversi che possono essere usati e ognuno di loro ha i suoi vantaggi e svantaggi. Widget factory risolve molti problemi comuni, migliorando significativamente la produttività e il riutilizzo del codice.
Esercizi
Creare una tabella ordinabile
Per questo esercizio, il compito è quello di individuare, scaricare e implementare un’estensione che permetta di ordinare la tabella esistente nella pagina index.html. Quando è pronto, tutte le colonne della tabella dovrebbero essere in grado di essere ordinabili.
Scrivere un'estensione per cambiare il colore di sfondo nelle tabelle
Aprire il file /exercises/index.html nel browser web. Eseguire l’esercizio utilizzando il file /exercises/js/stripe.js. Il compito è quello di scrivere un’estensione chiamata “stripe”, il quale potrà essere chiamata da qualsiasi elemento table e dovrà cambiare il colore di sfondo delle righe dispari nel corpo della tabella. Il colore potrà essere specificato come parametro dell’estensione.
$('#myTable').stripe('#cccccc');
Non dimenticare di restituire la tabella in modo che altri metodi possano essere concatenati dopo la chiamata all’estensione.