ruk·si

jQuery
Plugin Template

Updated at 2012-11-22 15:30

jQuery plugins are components that extend jQuery's functionality by providing extra functions for $ jQuery super object. jQuery plugins should follow the community standards as does this template. I have included annotations about almost every line of code.

// Following function definition style is called immediately invoked
// function expression a.k.a. IIFE.

// Parenthesis wrapping ensures that if a malformed plugin forgets to
// includea final semicolon ; there aren't errors in production when
// the scripts get concatenated and minimized.

// Taking window, etc... as parameters helps compressing and performance.
// You can refer document with 'doc' etc.

// Taking jQuery helps to avoid conflicts with other libraries that
// use dollar sign $.

// Note that undefined is asked, although it is not given, thus it is
// the 'real' undefined.

/**
 * Plugin Name
 *
 * Plugin description followed by usage examples.
 *
 * @example
 *   // Initialization
 *   $('#branch-list').ruksiPlugin();
 *
 */
(function($, window, document, undefined) {

  // Run in strict mode e.g. throws exceptions on global usage.
  'use strict';

  // Variable that is same for all instances of this plugin.
  // Does not get re-initialized when new instance is created.
  // Useful if you require some communication between different
  // instances of this plugin. Even extending libraries cannot get
  // to this variable by default. You can also save variables to
  // $.fn[] like default options are, they can be extended.
  var sharedVariable = {};

  /**
   * Ruksi Plugin
   *
   * This is comment for plugin constructor.
   * This function is always ran before any function calls.
   * The full plugin is defined by extending the prototype of this
   * function. All extending functions should be commented.
   *
   * @param {Object} element
   * DOM element e.g. div or ul.
   * @param {Object} options
   * Construction options.
   *
   * @constructor
   */
  function RuksiPlugin(element, options) {

    // Raw DOM element object for this instance.
    this.element = element;

    // jQuery wrapped DOM element object.
    // All jQuery functions require jQuery wrapped object.
    this.$element = $(element);

    // Creates final options to be used.
    // Fetches possible options from multiple sources and fills options
    // with default values.
    this.options = this._createOptions(options);

    // Variable that is different for all instances of this plugin.
    this.instanceVariable = {};

  }

  // Make prototype shorter to write.
  var thisProto = RuksiPlugin.prototype;

  // If you would like to inherit some other plugin, pass it's constructor
  // to IIFE and create new instance of it to use as prototype.
  // thisProto = new OtherPlugin();

  /**
   * Create Options
   *
   * Create final options that will be used for this instance.
   *
   * @param {Object|undefined} options
   * Option object given to constructor.
   *
   * @return {Object}
   *
   * @private
   */
  thisProto._createOptions = function( options ) {

    // This function is prefixed with _ to mean
    // that it should not be called outside this plugin a.k.a. private.

    // Merges together:
    // - An empty object, $.extend mangles this.
    // - Default options saved in jQuery.
    // - Options object given to the constructor.
    // - All 'data-*' HTML attributes in the DOM element.

    // You could also validate or manipulate options before returning
    // them.

    var defaults = $.ruksiPlugin.defaultOptions;
    var dataAttributes = this.$element.data();
    return $.extend({}, defaults, options, dataAttributes);

  };

  /**
   * Next follows few functions to understand how to write functions
   * that can be accessed outside this plugin and what breaks jQuery
   * chaining and what does not.
   *
   * As a rule, all functions that return a value should break chaining.
   * This should include all functions that start with 'get'.
   */

  // ALLOWS chaining to continue because returns itself.
  thisProto.echoParameter = function(parameter) {
    console.log(parameter);
    var x = 23;
  };
  thisProto.echoOptions = function() {
    console.log(this.options);
    return;
  };
  thisProto.echoThis = function() {
    console.log(this);
  };
  thisProto.doSomethingToOptions = function() {
    this.options.something = 'was done';
    return this;
  };

  // BREAKS chaining because returns something else than itself.
  thisProto.getParameter = function(parameter) {
    return parameter;
  };
  thisProto.getOptions = function() {
    return this.options;
  };
  thisProto.getElement = function() {
    return this.element;
  };
  thisProto.get$Element = function() {
    return this.$element;
  };

  /**
   * jQuery plugin calling logic.
   *
   * If first parameter is object AND not yet constructed:
   * - This is a constructor call and first parameter is constructor
   *   options.
   *
   * If first parameter is undefined AND not yet constructed:
   * - This is a constructor call and no constructor options are
   *   specified.
   *
   * If first parameter is string AND not yet constructed:
   * - This is a function call but construct the object without
   *   options first.
   *
   * If first parameter is string:
   * - This is a plugin function call and all possible following
   *   parameters are parameters for the plugin function call.
   *
   * @param {String|Object|undefined} parameter
   * Specify what this plugin call is about.
   *
   * @return {Object|*}
   * Matching jQuery objects for chaining if call does not return
   * anything. If plugin function call returns anything, return first
   * value received, even if we will invoke the function on all
   * matching jQuery objects.
   */
  $.fn.ruksiPlugin = function(parameter) {

    // Save all extra arguments after 'parameter' for the possible
    // function call. We could check if parameter is a string but
    // this is more clear.
    var extraArguments = Array.prototype.slice.call(arguments, 1);

    // The action (construction and/or function call) should be done to
    // all elements that match the used jQuery selector.

    // If any actions returns anything except themselves, this function
    // call should return only value of the first function response.
    // That is how jQuery functions work, e.g. '$('div').attr('id')'
    // only returns id of __the first element__ that matches the
    // selector.

    // If the invoked plugin functions do not return anything except
    // themselves, we can return collection of the matching elements
    // to maintain jQuery's famous chaining.

    var firstResponse = undefined;

    // For each jQuery element that was selected...
    // Also return all of the selected jQuery objects to
    // maintain chaining.
    var matchingElements = this.each( function() {

      // DOM element object wrapped in jQuery
      // and jQuery data of our plugin for this specific element.
      var $element = $(this);
      var jqData = $element.data('ruksiPlugin');

      // If jQuery does not have any this plugin specific data saved
      // for this DOM element, the plugin has not been initialize
      // for this specific element.
      if ( ! jqData ) {

        var options = {};

        // If 'parameter' is object, they are options for
        // the constructor.
        if ( $.isPlainObject(parameter) ) {
            options = parameter;
        }

        // Save the new plugin object instance to jQuery data for
        // this DOM element. Also make sure that 'jqData' variable
        // has the freshly created object as we might call it below.
        jqData = new RuksiPlugin(this, options);
        $element.data('ruksiPlugin', jqData);

      }

      // If 'parameter' is a string, this should be a function call.
      // The object was created above if it was not existing before.
      if ( typeof parameter === 'string' ) {

        if ( $.isFunction(jqData[parameter]) ) {

          var response = jqData[parameter].apply(jqData, extraArguments);

          // If function returns anything else than itself
          // and we do not yet have a first response, save it
          // to be returned.
          if (response !== jqData && firstResponse === undefined) {
            firstResponse = response;
          }
        }
        else {
          var message = parameter + ' not function of ruksiPlugin.';
          console.error(msg);
        }

      }

    });

    // Determine what to return to the plugin caller.

    // If no function returned anything else than themselves,
    // return all matching jQuery elements for chaining.

    // If any of the matching jQuery objects returned anything else
    // than themselves on the function call, return the first value
    // that was returned.

    return (firstResponse === undefined ? matchingElements : firstResponse);
  };

  /**
   * Default options for this plugin.
   * If nothing else is found, these values are used.
   * Default options should be saved to the $.fn[] so they can be globally
   * read or modified at runtime e.g if someone wants to change global
   * defaults or extend the plugin.
   *
   * @type {Object}
   */
  $.fn.ruksiPlugin.defaultOptions = {
    variableNamesShouldNotBeReservedWords: 'Will cause problems in IE',
    klass: 'not class because it is reserved'
  };

// Immediate function is called with these arguments...
}(window.jQuery, window, document));

/**
 * Usage
 */
$('body').ruksiPlugin(); // Optional, will initialize on first func call.
$('body').ruksiPlugin('echoOptions');
$('body').ruksiPlugin('getParameter', 'this is the parameter');

// Functions that do not return anything allow jQuery chaining.
$('div').ruksiPlugin('echoOptions').hide();
$('div').ruksiPlugin('doSomethingToOptions').show();