How to create a jQuery plugin with methods?

191,963

Solution 1

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.

Solution 2

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

Solution 3

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.

Solution 4

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.

Solution 5

jQuery has made this a lot easier with the introduction of the Widget Factory.

Example:

$.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 ) {
        // ...
    }

});

Initialization:

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

Method calling:

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

(This is how the jQuery UI library is built.)

Share:
191,963

Related videos on Youtube

Yuval Karmi
Author by

Yuval Karmi

Updated on March 07, 2022

Comments

  • Yuval Karmi
    Yuval Karmi about 2 years

    I'm trying to write a jQuery plugin that will provide additional functions/methods to the object that calls it. All the tutorials I read online (have been browsing for the past 2 hours) include, at the most, how to add options, but not additional functions.

    Here's what I am looking to do:

    //format div to be a message container by calling the plugin for that div

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

    or something along those lines. Here's what it boils down to: I call the plugin, then I call a function associated with that plugin. I can't seem to find a way to do this, and I've seen many plugins do it before.

    Here's what I have so far for the 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);
      }
    };
    

    How can I achieve something like that?

    Thank you!


    Update Nov 18, 2013: I've changed the correct answer to that of Hari's following comments and upvotes.

  • Yuval Karmi
    Yuval Karmi almost 15 years
    this seems like a non-standard way of doing things - is there anything simpler than this, like chaining functions? thank you!
  • Yuval Karmi
    Yuval Karmi almost 15 years
    also, how would i pass more than one argument under the arg - as an object, perhaps? (i.e. $('selector').myplugin('mymethod1', {arg1 : 'arg1value', arg2 : 'arg2value}) ). thank you!
  • Yuval Karmi
    Yuval Karmi almost 15 years
    one potential problem(?) that i noticed: when creating the new plugin new $.myplugin(this, options, arg), you pass 3 arguments, while the plugin itself seems to only expect 2 (i.e $.myplugin = function( options, arg)) - is this done on purpose? if not, how do i fix it? thanks!
  • Yuval Karmi
    Yuval Karmi almost 15 years
    i am having a bit of a hard time understanding what this would look like. Assuming i have code that needs to be executed the first time this is run, i'll have to first initialize it in my code - something like this: $('p').messagePlugin(); then later in the code i'd like to call the function saySomething like this $('p').messagePlugin().saySomething('something'); will this not re-initialize the plugin and then call the function? what would this look like with the enclosure and options? thank you very much. -yuval
  • tvanfosson
    tvanfosson almost 15 years
    @yuval -- typically jQuery plugins return jQuery or a value, not the plugin itself. That's why the name of the method is passed as an argument to the plugin when you want to invoke the plugin. You could pass any number of arguments, but you'll have to adjust the functions and the argument parsing. Probably best to set them in an anonymous object as you showed.
  • tvanfosson
    tvanfosson almost 15 years
    @yuval -- a transliteration mistake when extracting the code from an actual plugin I have. I've corrected it.
  • tvanfosson
    tvanfosson almost 15 years
    Sort of breaks the chainability paradigm of jQuery, though.
  • Dragouf
    Dragouf almost 13 years
    maybe this should be the best answer
  • w00t
    w00t almost 13 years
    This is great - I only wonder why jQuery seems to favor calling the methods by name as in the .plugin('method') pattern?
  • w00t
    w00t almost 13 years
    Every time you call messagePlugin() it will create a new object with those two functions, no?
  • adrian.andreas
    adrian.andreas over 12 years
    This is the method I use. You can also call the methods staticly via $.fn.tooltip('methodname', params);
  • Hari Honor
    Hari Honor over 12 years
    For this interested in a more javascript class-like version of the jQuery plugin template have a look here: club15cc.com/post/11690384272/… Amongst other things, it improves on the jquery.com version by allowing for class properties which maintain state been method calls.
  • aditya
    aditya over 12 years
    @tvanfosson: I've created a jsfiddle using the above template. However, a get() method doesn't return the value of an internal variable. Could you tell me what I'm doing wrong? jsfiddle.net/aditya/FCwRs
  • tvanfosson
    tvanfosson over 12 years
    @aditya (1) the variable isn't defined at the point in the function that you are invoking the get() function. (2) you don't want to define the variable in the function or it will get reinitialized each time the function is invoked. It's probably better to define it on the plugin itself and reference it. See this updated fiddle: jsfiddle.net/JxqHF
  • tvanfosson
    tvanfosson over 12 years
    @aditya or better yet, store it as data on the element: jsfiddle.net/CmmrS
  • aditya
    aditya over 12 years
    @tvanfosson In that case, isn't stackoverflow.com/q/6871820/56121 a cleaner way of doing it using jQuery .data() rather than adding static vars to your function?
  • tvanfosson
    tvanfosson over 12 years
    @aditya that's what I did in my second fiddle. It would really depend on whether it's a global plugin option or an option specific to a particular instance of a plugin. If global, like the defaults, adding it to the plugin definition is appropriate. If it's specific to that instance, storing it as data on the element is a better way to go. The approach for functions could be done either using the array technique or inline within the function definition. Either way they're scoped local to the plugin.
  • Kevin Beal
    Kevin Beal almost 12 years
    I'm using this approach myself. Thanks!
  • GusDeCooL
    GusDeCooL over 11 years
    What is the meaning of ; in your first line? please explain to me :)
  • tvanfosson
    tvanfosson over 11 years
    @GusDeCooL it just makes sure that we're starting a new statement so that our function definition isn't interpreted as an argument to someone else's poorly formatted Javascript (i.e, the initial paren isn't taken as a function invocation operator). See stackoverflow.com/questions/7365172/…
  • Kalzem
    Kalzem about 11 years
    When inside a "myplugin_method", how can I have access to the var of the plugin ?
  • tvanfosson
    tvanfosson about 11 years
    @BabyAzerty - not quite sure what you mean. Any variable declared within the $.myplugin function should be available (within scope) of any functions also defined within it.
  • Kalzem
    Kalzem about 11 years
    In the $.myplugin.defaut, I have 2 var (data and height). How can I have access to these var within the plugin's methods ? I tried "height", "myplugin.height","$.myplugin.height" none work...
  • tvanfosson
    tvanfosson about 11 years
    @BabyAzerty defaults.data or $.myplugin.defaults.data, similarly with height, although, these actually get stuff into options so you could use options.data and options.height if you want the, possibly, user-overridden versions.
  • Kalzem
    Kalzem about 11 years
    I tried accessing options inside my plugin methods, but I cannot get any variables (data nor height). Here is a fiddle jsfiddle.net/uWxcf
  • Joshua Bambrick
    Joshua Bambrick almost 11 years
    The main issue with this approach is that it cannot preserve chainability following $('p').messagePlugin() unless you call one of the two functions it returns.
  • Courtney Maroney
    Courtney Maroney almost 11 years
    This does not work. If you invoke the plugin on two different containers, the internal variables get overridden (namely _this)
  • ivkremer
    ivkremer almost 11 years
    Very handy architecture. I also added this line before calling the init method: this.data('tooltip', $.extend(true, {}, $.fn.tooltip.defaults, methodOrOptions));, so now I can access to options whenever I want after the initialization.
  • streetlogics
    streetlogics over 10 years
    For any like me who first said "where did the arguments variable come from" - developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… - I've been using JS forever and never knew that. You learn something new everyday!
  • István Ujj-Mészáros
    István Ujj-Mészáros over 10 years
    @tvanfosson What do you think about my aproach? Do you know any drawback? stackoverflow.com/a/18455244/504270
  • tvanfosson
    tvanfosson over 10 years
    cf. your comment. The only problem I see here is arguably a misuse of the event system. It's atypical to use events purely to invoke a function; it seems like overkill and may be easily broken. Normally, you'd use events in a publish-subscribe fashion, e.g., a function publishes that some condition "A" has occurred. Other entities, interested in "A", listen for the message that "A" has happened, then do something. You seem to be using it as push "command" instead, but assuming there is only one listener. You'd want to be careful that your semantics aren't broken by (others) adding listeners.
  • István Ujj-Mészáros
    István Ujj-Mészáros over 10 years
    @tvanfosson Thanks for your comment. I understand that it is not common technique and it can cause problems if someone accidentally add an event listener, but if it is named after the plugin, then it is very unlikely. I don't know about any performance related issues, but the code itself seems to be much simpler to me than with the other solutions, but I may be missing something.
  • Maciej Gurban
    Maciej Gurban over 10 years
    Assuming I define my configuration defaults (which can be overwritten by passing object on plugin init), how do I go about accessing their values? Or do I have to create methods/getters for each property?
  • daniel.sedlacek
    daniel.sedlacek about 10 years
    This is very bad architecture. It is error prone, can not be strong typed (with TypeScript) or checked for integrity at compile time. There is a reason why we don't write code in strings.
  • Yarin
    Yarin about 10 years
    @daniel.sedlacek a) "very bad architecture" - it's jQuery's standard widget architecture b) "checked for integrity at compile time" - JavaScript is a dynamic language c) "TypeScript" - wha?
  • daniel.sedlacek
    daniel.sedlacek about 10 years
    a) that's argumentum ad populum, b) every better JS IDE has code completion or linting, c) google it
  • mystrdat
    mystrdat about 10 years
    That is pure delusion, Mr. Sedlacek.
  • Peter Krauss
    Peter Krauss about 10 years
    @CMS, Please, I have another problem here at Stackoverflow, with chain method; but even using your recipe (returning the selectedObjects variable), it not works, see here a jsfiddle.
  • Stephen Collins
    Stephen Collins about 10 years
    @DiH, I'm with you on this one. This approach seems great, but it doesn't give you access to your global settings from anywhere other than init.
  • Kevin Jurkowski
    Kevin Jurkowski about 10 years
    There's a major problem with this technique! It doesn't create a new instance for every element in the selector like you think you're doing, instead it creates only a single instance attached to the selector itself. View my answer for a solution.
  • dalemac
    dalemac about 10 years
    Thanks, this helped me a lot, particularly introducing the .data() method to me. Very handy. FWIW you can also simplify some of your code by using anonymous methods.
  • jakabadambalazs
    jakabadambalazs almost 10 years
    @KevinJurkowski - Yes and no - You are correct that this method does not create a new instance for each element but it is not always (I'd say most of the time not) necessary to have a separate instance for each element. @HariKaramSingh - QUESTION: Why would you have methods object scoped to the wrapping function as opposed to having it inside the plugin body?
  • Hari Honor
    Hari Honor almost 10 years
    Not my decision. That code is from jQuery's site (or least the version from several years ago!). jQuery didn't seem concerned with having an object-based state as is typical in OOP. That's why I made this more complex but stateful version: gist.github.com/Club15CC/1300890 It's been a LONG time since I've looked at this so please be kind!
  • Frondor
    Frondor over 8 years
    As stated in mozilla documentation: You should not slice on arguments because it prevents optimizations in JavaScript engines (V8 for example). Instead, try constructing a new array by iterating through the arguments object. More information.
  • Dex Dave
    Dex Dave over 7 years
    possibly the best post I have come across yet about creating jQuery plugins as a beginner - thanks ;)
  • Sakthi Karthik
    Sakthi Karthik over 7 years
    Hi Gopal Joshi , Please give next level jquery plugin creation. we expect from your needful answer.
  • Gopal Joshi
    Gopal Joshi over 7 years
    Hello @SakthiKarthik, Off cource I will publish new tutorial soon in my blog
  • Gopal Joshi
    Gopal Joshi over 7 years
    Hi @SakthiKarthik, You may refer new article on next level jquery plugin here sgeek.org/…
  • Airn5475
    Airn5475 about 7 years
    Per docs: This system is called the Widget Factory and is exposed as jQuery.widget as part of jQuery UI 1.8; however, it can be used independently of jQuery UI. How is $.widget used without jQuery UI?
  • Alex G
    Alex G about 7 years
    jQuery chainability is not working using this method... $('.my-elements').find('.first-input').customPlugin('update'‌​‌​, 'first value').end().find('.second-input').customPlugin('update', 'second value'); returns Cannot read property 'end' of undefined. jsfiddle.net/h8v1k2pL
  • Alex G
    Alex G about 7 years
    Your method breaks jQuery chaining: $('.first-input').data('pluginName').publicMethod('new value').css('color', red); returns Cannot read property 'css' of undefined jsfiddle.net/h8v1k2pL/1
  • Qian Chen
    Qian Chen about 6 years
    How should methods access the options?
  • jslearner07
    jslearner07 over 5 years
    I am also using the same approach. Just wondering if we can default one method? Is it possible? Ex $('p').messagePlugin() should call saySomething directly. However 2nd method needs to be called as given below. $('p').messagePlugin().anotherAction(). Thanks.
  • Paul Swetz
    Paul Swetz over 5 years
    Fail: does not allow pluginContainer.MyPlugin.DoEvenMore().DoSomething();
  • CrandellWS
    CrandellWS over 5 years
    @AlexG given this example you would add return $element so in this example you would change it to plugin.foo_public_method = function() {/* Your Code */ return $element;} @Salim thanks for helping me ... github.com/AndreaLombardo/BootSideMenu/pull/34
  • rickvian
    rickvian over 3 years
    hi guys, how do i call myplugin_method1(arg) in the example?
  • user3700562
    user3700562 about 3 years
    Thank you for this SANE approach. Creating jQuery plugins correctly is just way too ridiculously complicated. This really is a major weakness of jQuery and of Javascript as a whole with it's prototype weirdness.