A jQuery plugin boilerplate

I created this boilerplate for a jQuery plugin I’m working on, and you can use it to kick start your own jQuery plugin development. The features of my boilerplate are:

  • Plugin can be customised using options
  • Setters/Getters for options
  • Private and public methods
  • Callback hooks
  • Plugin instances can be destroyed

The boilerplate

Here is the bare bones, uncommented boilerplate source code:

/**
 * A jQuery plugin boilerplate.
 * Author: Jonathan Nicol @f6design
 */
;(function($) {
  var pluginName = 'demoplugin';

  function Plugin(element, options) {
    var el = element;
    var $el = $(element);

    options = $.extend({}, $.fn[pluginName].defaults, options);

    function init() {
      // Add any initialization logic here...
            
      hook('onInit');
    }

    function fooPublic() {
      // Code goes here...
    }

    function option (key, val) {
      if (val) {
        options[key] = val;
      } else {
        return options[key];
      }
    }

    function destroy() {
      $el.each(function() {
        var el = this;
        var $el = $(this);

        // Add code to restore the element to its original state...

        hook('onDestroy');
        $el.removeData('plugin_' + pluginName);
      });
    }

    function hook(hookName) {
      if (options[hookName] !== undefined) {
        options[hookName].call(el);
      }
    }

    init();

    return {
      option: option,
      destroy: destroy,
      fooPublic: fooPublic
    };
  }

  $.fn[pluginName] = function(options) {
    if (typeof arguments[0] === 'string') {
      var methodName = arguments[0];
      var args = Array.prototype.slice.call(arguments, 1);
      var returnVal;
      this.each(function() {
        if ($.data(this, 'plugin_' + pluginName) && typeof $.data(this, 'plugin_' + pluginName)[methodName] === 'function') {
          returnVal = $.data(this, 'plugin_' + pluginName)[methodName].apply(this, args);
        } else {
          throw new Error('Method ' +  methodName + ' does not exist on jQuery.' + pluginName);
        }
      });
      if (returnVal !== undefined){
        return returnVal;
      } else {
        return this;
      }
    } else if (typeof options === "object" || !options) {
      return this.each(function() {
        if (!$.data(this, 'plugin_' + pluginName)) {
          $.data(this, 'plugin_' + pluginName, new Plugin(this, options));
        }
      });
    }
  };

  $.fn[pluginName].defaults = {
    onInit: function() {},
    onDestroy: function() {}
  };

})(jQuery);

…and with liberal comments:

/**
 * A jQuery plugin boilerplate.
 * Author: Jonathan Nicol @f6design
 */
;(function($) {  
  // Change this to your plugin name. 
  var pluginName = 'demoplugin';
  
  /**
   * Plugin object constructor.
   * Implements the Revealing Module Pattern.
   */
  function Plugin(element, options) {
    // References to DOM and jQuery versions of element.
    var el = element;
    var $el = $(element);

    // Extend default options with those supplied by user.
    options = $.extend({}, $.fn[pluginName].defaults, options);

    /**
     * Initialize plugin.
     */
    function init() {
      // Add any initialization logic here...

      hook('onInit');
    }

    /**
     * Example Public Method
     */
    function fooPublic() {
      // Code goes here...
    }

    /**
     * Get/set a plugin option.
     * Get usage: $('#el').demoplugin('option', 'key');
     * Set usage: $('#el').demoplugin('option', 'key', value);
     */
    function option (key, val) {
      if (val) {
        options[key] = val;
      } else {
        return options[key];
      }
    }

    /**
     * Destroy plugin.
     * Usage: $('#el').demoplugin('destroy');
     */
    function destroy() {
      // Iterate over each matching element.
      $el.each(function() {
        var el = this;
        var $el = $(this);

        // Add code to restore the element to its original state...

        hook('onDestroy');
        // Remove Plugin instance from the element.
        $el.removeData('plugin_' + pluginName);
      });
    }

    /**
     * Callback hooks.
     * Usage: In the defaults object specify a callback function:
     * hookName: function() {}
     * Then somewhere in the plugin trigger the callback:
     * hook('hookName');
     */
    function hook(hookName) {
      if (options[hookName] !== undefined) {
        // Call the user defined function.
        // Scope is set to the jQuery element we are operating on.
        options[hookName].call(el);
      }
    }

    // Initialize the plugin instance.
    init();

    // Expose methods of Plugin we wish to be public.
    return {
      option: option,
      destroy: destroy,
      fooPublic: fooPublic
    };
  }

  /**
   * Plugin definition.
   */
  $.fn[pluginName] = function(options) {
    // If the first parameter is a string, treat this as a call to
    // a public method.
    if (typeof arguments[0] === 'string') {
      var methodName = arguments[0];
      var args = Array.prototype.slice.call(arguments, 1);
      var returnVal;
      this.each(function() {
        // Check that the element has a plugin instance, and that
        // the requested public method exists.
        if ($.data(this, 'plugin_' + pluginName) && typeof $.data(this, 'plugin_' + pluginName)[methodName] === 'function') {
          // Call the method of the Plugin instance, and Pass it
          // the supplied arguments.
          returnVal = $.data(this, 'plugin_' + pluginName)[methodName].apply(this, args);
        } else {
          throw new Error('Method ' +  methodName + ' does not exist on jQuery.' + pluginName);
        }
      });
      if (returnVal !== undefined){
        // If the method returned a value, return the value.
        return returnVal;
      } else {
        // Otherwise, returning 'this' preserves chainability.
        return this;
      }
    // If the first parameter is an object (options), or was omitted,
    // instantiate a new instance of the plugin.
    } else if (typeof options === "object" || !options) {
      return this.each(function() {
        // Only allow the plugin to be instantiated once.
        if (!$.data(this, 'plugin_' + pluginName)) {
          // Pass options to Plugin constructor, and store Plugin
          // instance in the elements jQuery data object.
          $.data(this, 'plugin_' + pluginName, new Plugin(this, options));
        }
      });
    }
  };

  // Default plugin options.
  // Options can be overwritten when initializing plugin, by
  // passing an object literal, or after initialization:
  // $('#el').demoplugin('option', 'key', value);
  $.fn[pluginName].defaults = {
    onInit: function() {},
    onDestroy: function() {}
  };

})(jQuery);

Usage

The first thing you’ll need to do is change the name of the plugin, which is hardcoded as ‘demoplugin’, to something a little more descriptive. After that you should be able to follow the comments in the source code to customise the plugin.

Here are examples of how an end-user would interact with the plugin:

// Initialize plugin with default options.
$('#element').demoplugin();

// Initialize plugin with user defined options.
$('#element').demoplugin({
  option1: 2000,
  option2: 'value',
  callback1: function () { ... }
});

// Call a public method, no arguments.
$('#element').demoplugin('publicFunctionName');

// Call a public method with arguments.
$('#element').demoplugin('publicFunctionName', 'arg1', 'arg2');

// Get a plugin option.
$('#element').demoplugin('option', 'key');

// Set a plugin option after plugin initialization.
$('#element').demoplugin('option', 'key', value);

// Destroy plugin.
$('#element').demoplugin('destroy');

Setup options

It is very useful to be able to customise jQuery plugins on a per instance basis. The boilerplate implements default settings which can be overwritten when the plugin is initialized:

// Default plugin options.
$.fn[pluginName].defaults = {
  color: '#ff0000',
  height: 200
};
$('#element').demoplugin({
  color: '#0000ff',
  height: 400
});

Options can also be overwritten after the plugin is initialized, using the option method:

$('#element').demoplugin('option', 'color', '#00ff00');

An option value can be fetched using the same method by simply omitting the third parameter:

var color = $('#element').demoplugin('option', 'color');

Public and private methods

Private functions reduce the chance of your users inadvertently calling the plugin’s utility methods. I’m a fan of the Revealing Module pattern, so that’s how I chose to implement private/public functionality in my boilerplate.

function Plugin(element, options) {
  // This method is public.
  function fooPublic(param1, param2) {
    // ...
  }

  // This method is private.
  function fooPrivate() {
    // ...
  }

  // Expose methods of Plugin we wish to be public.
  return {
    fooPublic: fooPublic
  };
}

Public functions can be called by your users like so:

$('#element').demoplugin('fooPublic', 'val1', 'val2');

Callback hooks

Callback hooks are incredibly powerful. They allow your users to listen for events in your plugin’s behaviour and respond accordingly. Hooks are implemented as functions in the default options object:

$.fn[pluginName].defaults = {
  onSlideshowFinished: function() {}
};

Each function is empty to begin with, since the callback’s behaviour will be defined by the user. This is done by overwriting the callback function when setting the plugin’s options. The scope of the callback is the DOM element the plugin is attached to, so in the following example the DOM element would be hidden.

$('#element').demoplugin({
  onSlideshowFinished: function() {
    $(this).hide();
  }
});

To trigger a hook, call the plugin’s hook method:

hook('onSlideshowFinished');

There are two callbacks implemented by default in the boilerplate: onInit and onDestroy.

Destroying a plugin instance

Your users can remove an instance of the plugin by calling its destroy method:

$('#element').demoplugin('destroy');

The plugin stores all its functionality in its DOM element’s data object. By default the destroy method simply removes the stored data values. You will probably need to customise destroy to restore the DOM element to its original state and remove any event handlers your plugin has created.

Credit

My boilerplate takes inspiration from a number of sources, most notably the jQuery plugin authoring guidelines, Stefan Gabos’ boilerplate, and Zeno Rocha and Addy Osmani’s jQuery Boilerplate.

8 thoughts on “A jQuery plugin boilerplate

  1. James Vassie says:

    Firstly thanks a lot Jonathan for taking the time to put this together, I’ve found it well documented and very informative!

    I had a couple of queries regarding best practices, on things I hadnt found in the boilerplate:

    What do you think is the best way to handle errors in a plugin? Take for example a basic slideshow, with two options, slides per row, and number of rows. If the number of slides in the slideshow is less than slides per row * number of rows, what would be ideal? Obviously, in such a simple case, the plugin shouldnt ‘fail’, and just work off perhaps some default parameters. On a slightly more edge case though, if there were no slides at all, then the slider obviously can’t function. Other plugins I have seen output a message to the console and just return; is there a better method you know of please?

  2. Jonathan says:

    @James I won’t pretend to be an expert at error handling, but for what it’s worth I think your approach should depend on the consequences of the error.

    If you can recover from the error by falling back to default configuration settings, as in your first example, then that makes good sense. The plugin may not work exactly as the user intended, but it will still work.

    In cases where the plugin can’t continue you could throw a JS error, like I do in my boilerplate if the user attempts to call a nonexistent public method. That’s arguably an extreme approach since it will stop other scripts on the page from executing, but that might be preferable to failing silently because it alerts the user to the fact that they are using the plugin incorrectly.

    Or, as you suggested you could log an error to the console. This is more subtle than throwing an error, and I think in most cases it’s probably preferable. One thing worth considering: console.log() will throw an error in some browsers, so if you use it in production code you need to implement a cross-browser wrapper, something like Paul Irish’s log(): http://paulirish.com/2009/log-a-lightweight-wrapper-for-consolelog/

  3. hannenz says:

    Thanks a lot. This is exactly what I have been seeking for! Very straight forward, robust and well documented. I have read several jQuery books and not until now i have found such a comprehensive solution that gets it right to the point.
    I will use this one for my projects from now on :)

  4. Jonathan says:

    @hannenz I’m glad you’ve found my boilerplate useful :)

  5. parsa says:

    Thanks for the great boilerplate. What’s the best practice for passing arguments to hooked callbacks (arguments about state of plugin)?

  6. maiis says:

    Great, thanks !

    @parsa: i did that like this for me:
    function hook(hookName,data) {
    if (options[hookName] !== undefined) {
    options[hookName].call(el,data);
    }
    }

    and then call: hook(‘myhook’,data)

  7. Jonathan says:

    @parsa: I needed to do this myself today, and this is how I did it:

    function hook(hookName, args) {
    if (options[hookName] !== undefined) {
    options[hookName].apply(el, args);
    }
    }

    and then call: hook(‘myhook’, [param1, param2, param3, etc]);

    And your callback function looks like this:

    myhook: function(param1, param2, param3, etc) {}

    I chose this approach because it’s very close to passing parameters in the usual fashion, with no need to parse them in your callback.

    When I have a moment I’ll update the boilerplate.

  8. cihan says:

    Thank you for publishing this very useful boilerplate. I’ve got trouble about hook func. When i set plugin for 2 divs with callback functions, it cant hook to function, but for 1 div there is no problem.

    i set callback in defaults

    $.fn[pluginName].defaults = {
    onAnyHook: function(){}
    };

    then i set plugin with callback

    $(“div1”).myplugin({
    onAnyHook: function(){}
    });

    $(“div2”).myplugin({
    onAnyHook: function(){}
    });

Comments are closed.