Improving an accordion script - Part 3

In this final post on improving a basic accordion script, we will be taking our final code from part two, and updating it to a be a jquery plugin. You can check out part one if you want to start at the beginning to see how we got here.

So, I tend to start with the same base jQuery template when I create a new plugin. That way my coding is more consistent across plugins. If you would like a copy of the boilerplate code, you can get it from this gist. Here it is:

( function( $, document, window, undefined ) {
 'use strict';

var Constructor = function( elm, options, id ) {
   this.el = elm;
   this.$el = $(elm);
   this.init( options );
};

Constructor.prototype = {
   init: function( options) {
     this.options = $.extend( true, {}, $.fn.pluginName.defaults, options );
   },
   destroy : function ( ) {}
};

jQuery.fn.pluginName = function( options ) {
  switch ( typeof options ) {
    case 'number': //if it takes a number if not remove
      Constructor = $(this).data( 'Constructor' );
      if ( Constructor !== null ) {
        //do something here
      }
      break;
   case 'string':
      Constructor = $(this).data( 'Constructor' );
      if ( Constructor !== null ) {
        switch( options ) {
          case 'init':
            Constructor.init();
            break;
          case 'destroy':
            Constructor.destroy();
            break;
        }
     }
     break;
  default:
      return this.each( function () {
        new Constructor( $( this ), options );
      });
  }
};
jQuery.fn.pluginName.defaults = {
   option1 : '',
   option2 : ''
};

}(jQuery, document, window));

This sets up a basic jquery plugin. I simply do a Find/Replace on the word "Constructor" and replace that with the constructor function name and replace "pluginName" with the name of the plugin. And we are ready to go.

Now, let's rip apart our final code from part two and put it into the boilerplate code. I will call the constructor function "Accordion" and the plugin "accordion" and replace those values in the code above. We can take our settings object and place that in our plugin code defaults so that they look like this:

jQuery.fn.accordion.defaults = {
  classes : { // CSS class names used by the script
    item : 'accordion-item',
    header : 'accordion-header',
    content : 'accordion-content',
    selected : 'accordion-expanded',
    expandAll : 'accordion-expand-all',
    collapseAll : 'accordion-collapse-all'
  },
  events : 'click.accordion dblclick.accordion touchend.accordion'
};

Note: I removed the "selector" option and the "root" class option. These will be handled by the selector that the plugin is called against.

Now lets copy over our other functions into the plugin and we are pretty close to done. We can remove the setSelector() function since, again, that is handled by the plugin. The next function is the addEvents() function.

addEvents :  function () {
  var acc = accordion.settings;
  acc.selector.on( acc.events, checkEvents( evt ) );
}

We will need to point to the plugin element for the selector and use our new options object instead of the settings object we had before.

addEvents :  function () {
  this.$el.on( this.options.events, this._.checkEvents( evt ) );
}

Next up the checkEvents() function:

checkEvents : function ( evt ) {
  var sel = accordion.settings.selector,
      cls = accordion.settings.classes,
      $this = $(evt.target), $p = $this.parent();

  if ($this.hasClass('.' + cls.expandAll)) {
    sel.find('.' + cls.expanded).removeClass('.' + cls.expanded);
  }
  if ($this.hasClass('.' + cls.collapseAll)) {
    sel.find('.' + cls.collapsed).removeClass('.' + cls.collapsed);
  }
  if ($this.hasClass('.' + cls.header)) {
    $p.toggleClass('.' + cls.expanded);
  }
}

OK. Again we have to change up the code to point to our new options object and selector. Otherwise the code stays the same.

checkEvents : function ( evt ) {
  var sel = this.$el,
      cls = this.options.classes,
      $this = $(evt.target), $p = $this.parent();

  if ($this.hasClass('.' + cls.expandAll)) {
    sel.find('.' + cls.expanded).removeClass('.' + cls.expanded);
  }
  if ($this.hasClass('.' + cls.collapseAll)) {
    sel.find('.' + cls.collapsed).removeClass('.' + cls.collapsed);
  }
  if ($this.hasClass('.' + cls.header)) {
    $p.toggleClass('.' + cls.expanded);
  }
}

And here is the updated removeEvents() function:

removeEvents : function() {
  this.$el.off( this.options.events, this._.checkEvents( evt ) );
}

Pretty simple huh? We now have a working jQuery plugin just by changing a few variables and copy/pasting our previous code. This is why I said it would be easier if we made it an object first.

Also, I will keep the plugin constructor call to accept only a standard options object or a string. The string can be either "init" or "destroy" to invoke those functions and that's it.

Here is a look at our final code:

(function($, document, window, undefined) {
  'use strict';

  var Accordion = function( elm, options, id ) {
    this.el = elm;
    this.$el = $(elm);
    this.init( options );
  };

  Accordion.prototype = {
    _ : {   // private functions
      addEvents :  function () {
        this.$el.on( this.options.events, this._.checkEvents( evt ) );
      },

      checkEvents : function ( evt ) {
        var sel = this.$el,
          cls = this.options.classes,
          $this = $(evt.target), $p = $this.parent();

        if ($this.hasClass('.' + cls.expandAll)) {
          sel.find('.' + cls.expanded).removeClass('.' + cls.expanded);
        }
        if ($this.hasClass('.' + cls.collapseAll)) {
          sel.find('.' + cls.collapsed).removeClass('.' + cls.collapsed);
        }
        if ($this.hasClass('.' + cls.header)) {
          $p.toggleClass('.' + cls.expanded);
        }
      },

      removeEvents : function() {
        this.$el.off( this.options.events, this._.checkEvents( evt ) );
      }
    },  // end private methods

  init : function() {
    this.options = $.extend( true, {}, $.fn.accordion.defaults, options );
    this._.addEvents();
  },

  destroy : function() {
    this._.removeEvents();
  }
};

jQuery.fn.pluginName = function( options ) {
  switch ( typeof options ) {
    case 'string':
      Accordion = $(this).data( 'Accordion' );
      if ( Accordion !== null ) {
        switch( options ) {
          case 'init':
            Accordion.init();
            break;
          case 'destroy':
            Accordion.destroy();
            break;
        }
      }
      break;
    default:
      return this.each( function () {
        new Accordion( $( this ), options );
      });
  }
};

jQuery.fn.pluginName.defaults = {
  classes : { // CSS class names used by the script
    item : 'accordion-item',
    header : 'accordion-header',
    content : 'accordion-content',
    selected : 'accordion-expanded',
    expandAll : 'accordion-expand-all',
    collapseAll : 'accordion-collapse-all'
  },
  events : 'click.accordion dblclick.accordion touchend.accordion'
};

}(jQuery, document, window));

And that's it! We have a working jQuery accordion plugin. We have come a long way from the starting code but we now have a very flexible plugin.

Note: I have continued to work on this plugin for a while adding in a lot of different options and modifying the code. You can fork it or download it from my GitHub repository.

That about does it.
'Til next time!