Pesquisa de site

javascript - Como criar um plugin jQuery com métodos?


Estou tentando escrever um plugin jQuery que forneça funções/métodos adicionais para o objeto que o chama. Todos os tutoriais que li online (estive navegando pelas últimas 2 horas) incluem, no máximo, como adicionar opções, mas não funções adicionais.

Aqui está o que estou tentando fazer:

//formatar div para ser um contêiner de mensagem chamando o plugin para essa div

$("#mydiv").messagePlugin();
$("#mydiv").messagePlugin().saySomething("hello");

ou algo nesse sentido. Aqui está o que se resume: eu chamo o plugin e, em seguida, chamo uma função associada a esse plugin. Não consigo encontrar uma maneira de fazer isso, e já vi muitos plugins fazerem isso antes.

Aqui está o que eu tenho até agora para o plugin:

jQuery.fn.messagePlugin = function() {
  return this.each(function(){
    alert(this);
  });

  //i tried to do this, but it does not seem to work
  jQuery.fn.messagePlugin.saySomething = function(message){
    $(this).html(message);
  }
};

Como posso alcançar algo assim?

Obrigado!

Atualização 18 de nov. de 2013: Eu mudei a resposta correta para a de Hari seguindo os comentários e votos a favor.

Resposta #1

De acordo com a página de Criação de Plugins do jQuery (http://docs.jquery.com/Plugins/Authoring), é melhor não poluir os namespaces jQuery e jQuery.fn. Eles sugerem este método:

(function( $ ){

    var methods = {
        init : function(options) {

        },
        show : function( ) {    },// IS
        hide : function( ) {  },// GOOD
        update : function( content ) {  }// !!!
    };

    $.fn.tooltip = function(methodOrOptions) {
        if ( methods[methodOrOptions] ) {
            return methods[ methodOrOptions ].apply( this, Array.prototype.slice.call( arguments, 1 ));
        } else if ( typeof methodOrOptions === 'object' || ! methodOrOptions ) {
            // Default to "init"
            return methods.init.apply( this, arguments );
        } else {
            $.error( 'Method ' +  methodOrOptions + ' does not exist on jQuery.tooltip' );
        }    
    };


})( jQuery );

Basicamente, você armazena suas funções em um array (com escopo para a função envolvente) e verifica se há uma entrada se o parâmetro passado for uma string, revertendo para um método padrão ("init" aqui) se o parâmetro for um objeto (ou nulo).

Então você pode chamar os métodos assim...

$('div').tooltip(); // calls the init method
$('div').tooltip({  // calls the init method
  foo : 'bar'
});
$('div').tooltip('hide'); // calls the hide method
$('div').tooltip('update', 'This is the new tooltip content!'); // calls the update method

A variável "arguments" do Javascript é um array de todos os argumentos passados, portanto ela funciona com comprimentos arbitrários de parâmetros de função.

Resposta #2

Aqui está o padrão que usei para criar plugins com métodos adicionais. Você o usaria assim:

$('selector').myplugin( { key: 'value' } );

ou, para invocar um método diretamente,

$('selector').myplugin( 'mymethod1', 'argument' );

Exemplo :

;(function($) {

    $.fn.extend({
        myplugin: function(options,arg) {
            if (options && typeof(options) == 'object') {
                options = $.extend( {}, $.myplugin.defaults, options );
            }

            // this creates a plugin for each element in
            // the selector or runs the function once per
            // selector.  To have it do so for just the
            // first element (once), return false after
            // creating the plugin to stop the each iteration 
            this.each(function() {
                new $.myplugin(this, options, arg );
            });
            return;
        }
    });

    $.myplugin = function( elem, options, arg ) {

        if (options && typeof(options) == 'string') {
           if (options == 'mymethod1') {
               myplugin_method1( arg );
           }
           else if (options == 'mymethod2') {
               myplugin_method2( arg );
           }
           return;
        }

        ...normal plugin actions...

        function myplugin_method1(arg)
        {
            ...do method1 with this and arg
        }

        function myplugin_method2(arg)
        {
            ...do method2 with this and arg
        }

    };

    $.myplugin.defaults = {
       ...
    };

})(jQuery);

Resposta #3

Que tal esta abordagem:

jQuery.fn.messagePlugin = function(){
    var selectedObjects = this;
    return {
             saySomething : function(message){
                              $(selectedObjects).each(function(){
                                $(this).html(message);
                              });
                              return selectedObjects; // Preserve the jQuery chainability 
                            },
             anotherAction : function(){
                               //...
                               return selectedObjects;
                             }
           };
}
// Usage:
$('p').messagePlugin().saySomething('I am a Paragraph').css('color', 'red');

Os objetos selecionados são armazenados na closure messagePlugin, e essa função retorna um objeto que contém as funções associadas ao plugin, em cada função você pode realizar as ações desejadas nos objetos atualmente selecionados.

Você pode testar e brincar com o código aqui.

Editar: Código atualizado para preservar o poder da encadeabilidade do jQuery.

Resposta #4

O problema com a resposta atualmente selecionada é que você não está realmente criando uma nova instância do plugin personalizado para cada elemento no seletor como você pensa que está fazendo... você na verdade está criando apenas uma única instância e passando o próprio seletor como o escopo.

Veja este fiddle para uma explicação mais detalhada.

Em vez disso, você precisará percorrer o seletor usando jQuery.each e instanciar uma nova instância do plugin personalizado para cada elemento no seletor.

Veja como:

(function($) {

    var CustomPlugin = function($el, options) {

        this._defaults = {
            randomizer: Math.random()
        };

        this._options = $.extend(true, {}, this._defaults, options);

        this.options = function(options) {
            return (options) ?
                $.extend(true, this._options, options) :
                this._options;
        };

        this.move = function() {
            $el.css('margin-left', this._options.randomizer * 100);
        };

    };

    $.fn.customPlugin = function(methodOrOptions) {

        var method = (typeof methodOrOptions === 'string') ? methodOrOptions : undefined;

        if (method) {
            var customPlugins = [];

            function getCustomPlugin() {
                var $el          = $(this);
                var customPlugin = $el.data('customPlugin');

                customPlugins.push(customPlugin);
            }

            this.each(getCustomPlugin);

            var args    = (arguments.length > 1) ? Array.prototype.slice.call(arguments, 1) : undefined;
            var results = [];

            function applyMethod(index) {
                var customPlugin = customPlugins[index];

                if (!customPlugin) {
                    console.warn('$.customPlugin not instantiated yet');
                    console.info(this);
                    results.push(undefined);
                    return;
                }

                if (typeof customPlugin[method] === 'function') {
                    var result = customPlugin[method].apply(customPlugin, args);
                    results.push(result);
                } else {
                    console.warn('Method \'' + method + '\' not defined in $.customPlugin');
                }
            }

            this.each(applyMethod);

            return (results.length > 1) ? results : results[0];
        } else {
            var options = (typeof methodOrOptions === 'object') ? methodOrOptions : undefined;

            function init() {
                var $el          = $(this);
                var customPlugin = new CustomPlugin($el, options);

                $el.data('customPlugin', customPlugin);
            }

            return this.each(init);
        }

    };

})(jQuery);

E um violino funcional.

Você notará como, no primeiro fiddle, todos os divs são sempre movidos para a direita pelo mesmo número exato de pixels. Isso ocorre porque existe apenas um objeto de opções para todos os elementos no seletor.

Usando a técnica escrita acima, você notará que no segundo fiddle, cada div não está alinhada e é movida aleatoriamente (excluindo a primeira div, já que seu randomizador está sempre definido como 1 na linha 89). Isso ocorre porque agora estamos instanciando corretamente uma nova instância de plugin customizado para cada elemento no seletor. Cada elemento tem seu próprio objeto de opções e não é salvo no seletor, mas na própria instância do plugin customizado.

Isso significa que você poderá acessar os métodos do plugin personalizado instanciado em um elemento específico no DOM a partir de novos seletores jQuery e não será obrigado a armazená-los em cache, como você faria no primeiro fiddle.

Por exemplo, isso retornaria um array de todos os objetos de opções usando a técnica do segundo exemplo. Retornaria indefinido no primeiro.

$('div').customPlugin();
$('div').customPlugin('options'); // would return an array of all options objects

É assim que você teria que acessar o objeto options no primeiro fiddle, e isso retornaria apenas um único objeto, não uma lista deles:

var divs = $('div').customPlugin();
divs.customPlugin('options'); // would return a single options object

$('div').customPlugin('options');
// would return undefined, since it's not a cached selector

Eu sugeriria usar a técnica acima, não a da resposta atualmente selecionada.

Resposta #5

Use o jQuery UI Widget Factory.

  • Escrevendo Plugins com Estado usando a Fábrica de Widgets do jQuery UI
  • Como Usar a Fábrica de Widgets

Exemplo:

$.widget( "myNamespace.myPlugin", {

    options: {
        // Default options
    },
 
    _create: function() {
        // Initialization logic here
    },
 
    // Create a public method.
    myPublicMethod: function( argument ) {
        // ...
    },

    // Create a private method.
    _myPrivateMethod: function( argument ) {
        // ...
    }
 
});

Inicialização:

$('#my-element').myPlugin();
$('#my-element').myPlugin( {defaultValue:10} );

Chamada de método:

$('#my-element').myPlugin('myPublicMethod', 20);

(É assim que a biblioteca jQuery UI é construída.)

Resposta #6

Uma abordagem mais simples é usar funções aninhadas. Então você pode encadeá-las de forma orientada a objetos. Exemplo:

jQuery.fn.MyPlugin = function()
{
  var _this = this;
  var a = 1;

  jQuery.fn.MyPlugin.DoSomething = function()
  {
    var b = a;
    var c = 2;

    jQuery.fn.MyPlugin.DoSomething.DoEvenMore = function()
    {
      var d = a;
      var e = c;
      var f = 3;
      return _this;
    };

    return _this;
  };

  return this;
};

E aqui está como chamá-lo:

var pluginContainer = $("#divSomeContainer");
pluginContainer.MyPlugin();
pluginContainer.MyPlugin.DoSomething();
pluginContainer.MyPlugin.DoSomething.DoEvenMore();

Tenha cuidado, no entanto. Você não pode chamar uma função aninhada até que ela tenha sido criada. Então você não pode fazer isso:

var pluginContainer = $("#divSomeContainer");
pluginContainer.MyPlugin();
pluginContainer.MyPlugin.DoSomething.DoEvenMore();
pluginContainer.MyPlugin.DoSomething();

A função DoEvenMore nem sequer existe porque a função DoSomething ainda não foi executada, a qual é necessária para criar a função DoEvenMore. Para a maioria dos plugins jQuery, você realmente terá apenas um nível de funções aninhadas e não dois, como mostrei aqui.
Apenas certifique-se de que, ao criar funções aninhadas, você defina essas funções no início da função pai antes de qualquer outro código da função pai ser executado.

Finalmente, note que o membro "this" é armazenado em uma variável chamada "_this". Para funções aninhadas, você deve retornar "_this" se precisar de uma referência à instância no cliente chamador. Você não pode simplesmente retornar "this" na função aninhada porque isso retornará uma referência para a função e não para a instância jQuery. Retornar uma referência jQuery permite que você encadeie métodos intrínsecos do jQuery no retorno.

Resposta #7

Eu consegui isso do jQuery Plugin Boilerplate

Também descrito no jQuery Plugin Boilerplate, reprise

// jQuery Plugin Boilerplate
// A boilerplate for jumpstarting jQuery plugins development
// version 1.1, May 14th, 2011
// by Stefan Gabos

// remember to change every instance of "pluginName" to the name of your plugin!
(function($) {

    // here we go!
    $.pluginName = function(element, options) {

    // plugin's default options
    // this is private property and is accessible only from inside the plugin
    var defaults = {

        foo: 'bar',

        // if your plugin is event-driven, you may provide callback capabilities
        // for its events. execute these functions before or after events of your
        // plugin, so that users may customize those particular events without
        // changing the plugin's code
        onFoo: function() {}

    }

    // to avoid confusions, use "plugin" to reference the
    // current instance of the object
    var plugin = this;

    // this will hold the merged default, and user-provided options
    // plugin's properties will be available through this object like:
    // plugin.settings.propertyName from inside the plugin or
    // element.data('pluginName').settings.propertyName from outside the plugin,
    // where "element" is the element the plugin is attached to;
    plugin.settings = {}

    var $element = $(element), // reference to the jQuery version of DOM element
    element = element; // reference to the actual DOM element

    // the "constructor" method that gets called when the object is created
    plugin.init = function() {

    // the plugin's final properties are the merged default and
    // user-provided options (if any)
    plugin.settings = $.extend({}, defaults, options);

    // code goes here

   }

   // public methods
   // these methods can be called like:
   // plugin.methodName(arg1, arg2, ... argn) from inside the plugin or
   // element.data('pluginName').publicMethod(arg1, arg2, ... argn) from outside
   // the plugin, where "element" is the element the plugin is attached to;

   // a public method. for demonstration purposes only - remove it!
   plugin.foo_public_method = function() {

   // code goes here

    }

     // private methods
     // these methods can be called only from inside the plugin like:
     // methodName(arg1, arg2, ... argn)

     // a private method. for demonstration purposes only - remove it!
     var foo_private_method = function() {

        // code goes here

     }

     // fire up the plugin!
     // call the "constructor" method
     plugin.init();

     }

     // add the plugin to the jQuery.fn object
     $.fn.pluginName = function(options) {

        // iterate through the DOM elements we are attaching the plugin to
        return this.each(function() {

          // if plugin has not already been attached to the element
          if (undefined == $(this).data('pluginName')) {

              // create a new instance of the plugin
              // pass the DOM element and the user-provided options as arguments
              var plugin = new $.pluginName(this, options);

              // in the jQuery version of the element
              // store a reference to the plugin object
              // you can later access the plugin and its methods and properties like
              // element.data('pluginName').publicMethod(arg1, arg2, ... argn) or
              // element.data('pluginName').settings.propertyName
              $(this).data('pluginName', plugin);

           }

        });

    }

})(jQuery);

Artigos relacionados