Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to create a jQuery plugin with methods?

According to the jQuery Plugin Authoring page (http://docs.jquery.com/Plugins/Authoring), it's best not to muddy up the jQuery and jQuery.fn namespaces. They suggest this method:

(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 );

Basically you store your functions in an array (scoped to the wrapping function) and check for an entry if the parameter passed is a string, reverting to a default method ("init" here) if the parameter is an object (or null).

Then you can call the methods like so...

$('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

Javascripts "arguments" variable is an array of all the arguments passed so it works with arbitrary lengths of function parameters.


Here's the pattern I have used for creating plugins with additional methods. You would use it like:

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

or, to invoke a method directly,

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

Example:

;(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);

What about this approach:

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');

The selected objects are stored in the messagePlugin closure, and that function returns an object that contains the functions associated with the plugin, the in each function you can perform the desired actions to the currently selected objects.

You can test and play with the code here.

Edit: Updated code to preserve the power of the jQuery chainability.


The problem with the currently selected answer is that you're not actually creating a new instance of the custom plugin for every element in the selector like you think you're doing... you're actually only creating a single instance and passing in the selector itself as the scope.

View this fiddle for a deeper explanation.

Instead, you'll need to loop through the selector using jQuery.each and instantiate a new instance of the custom plugin for every element in the selector.

Here's how:

(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);

And a working fiddle.

You'll notice how in the first fiddle, all divs are always moved to the right the exact same number of pixels. That is because only one options object exists for all elements in the selector.

Using the technique written above, you'll notice that in the second fiddle, each div is not aligned and is randomly moved (excluding the first div as it's randomizer is always set to 1 on line 89). That is because we are now properly instantiating a new custom plugin instance for every element in the selector. Every element has its own options object and is not saved in the selector, but in the instance of the custom plugin itself.

This means that you'll be able to access the methods of the custom plugin instantiated on a specific element in the DOM from new jQuery selectors and aren't forced to cache them, as you would be in the first fiddle.

For example, this would return an array of all options objects using the technique in the second fiddle. It would return undefined in the first.

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

This is how you would have to access the options object in the first fiddle, and would only return a single object, not an array of them:

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

I'd suggest using the technique above, not the one from the currently selected answer.