Improving an accordion script - Part 2

In part one of this, I took and existing javascript file and did some quick updates and optimizations to improve the overall performance of the script. We reduced the number of selectors and removed the animation functions and put those into the CSS.

My teammate and I thought it would be a great idea to expand the concept of the script to make it a more generalized accordion script. And that is what we are going to do in this post.

So the first thing, I wanted to do was to convert the single function into an object with it's own namespace.

var accordion = {
    //our code will go here
};

To do this in the most effective manner I also realized we will need to break about our single function into multiple functions that concentrate on doing a single specific thing making it more DRY.

When I create a JavaScript object I tend to start using a singleton pattern with certain properties and methods. I also tend to wrap all the code up in an iife passing in any objects needed. In this case we definitely need to pass in jQuery. I tend to add the window and document object as well. I also pass in undefined but since I don't set a value it gets set to ( you guessed it ) undefined.

( function ( $, window, document, undefined ) {
    'use strict';
    var accordion = {
        // code here
    };
}) ( jQuery, window, document )();

That's a good starting template. Now, let's get to work on our actual script. Since I tend to end up converting all the way to a full-fledged jQuery plugin, I tend to break my object up into different sub-objects or sections. This makes it easier for me later. In the original script, we cached our initial selector. That's still a good idea but we are going to go about it in an entirely different way. We will add a property to our object to hold this selection and call it in our init() method. Since we are doing that, let's also go ahead and abstract away our class names so we can change them if we need to and not need to change the actual functionality of the script. This is what I ended up with:

var accordion = {

    settings : {
        selector : undefined,
        classes : { // CSS class names used by the script
            root : 'accordion',
            item : 'accordion-item',
            header : 'accordion-header',
            content : 'accordion-content',
            selected : 'accordion-expanded',
            expandAll : 'accordion-expand-all',
            collapseAll : 'accordion-collapse-all'
        }
    },

    _ : {   // private functions
        setSelector : function () {
            var acc = accordion.settings;
            acc.selector = $('.' + acc.classes.root);
        }
    }

    init : function() {
        this._.setSelector(); 
    }
};

See what we've done here? When the init() function is called it sets the selector property. But what is actually going to be selected is now in our settings object. That way we can change the class name in one place and no further modification to the code will be needed. I also went ahead and added all the other classes we need as well.

Yes, this is a lot more code than we needed previously ( var accordion = $('.accordion'); ). But it still executes just as fast and it has laid the groundwork for something a lot more flexable than before.

To initialze our script we just call it onload.

$(function(){
    accordion.init();
});

OK. We have our selection, what else do we need? Well, let's start with our click events. In this we will use jQuery's event delegation and just put a single click event on the accordion container <div>. From there, we will determine what was clicked and do the appropriate action.

So, let's create a private function for creating the click event and then call that in our init(); function.

var accordion = {
    settings : { // settings code from above here
            // new setting to hold our events
            events : 'click.accordion dblclick.accordion touchend.accordion'
    },

    _ : {   // private functions
        setSelector : function () {
            //selector code from above here,
        },
        addEvents :  function () {
            var acc = accordion.settings;
            acc.selector.on( acc.events, checkEvents( evt ) );
        }
        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);
            }
        }
    },

    init : function() {
        this._.setSelector();
        this._.setEvents();
    }
};

Now we have updated our accordion object with a single event and we can modify all of the event types through our settings. Notice the event type all end in .accordion. This namespaces our events and allows us to target them specifically. All in all, much better. The code functions the same as our previous script but is now using our settings object to determine what and where to activate.

Let's add a new public method to "destroy" our accordion object (basically just removing our events ) and call it a day. Since we have namespaced the events, we will only be removing those events. If any other events been added outside of our script, they will not be affected! Got to love that!

Our complete script looks like this now:

( function ( $, window, document, undefined ) {
    'use strict';
    var accordion = {
        settings : {
            selector : undefined,
            classes : { // CSS class names used by the script
                root : 'accordion',
                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'
        },

        _ : {   // private functions
            setSelector : function () {
                var acc =accordion.settings;
                acc.selector = $('.'+ acc.classes.root);
            },
            addEvents :  function () {
                var acc = accordion.settings;
                acc.selector.on( acc.events, checkEvents( evt ) );
            },
            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);
                }
            },

            removeEvents : function() {
                var acc = accordion.settings;
                acc.selector.off( acc.events, accordion._.checkEvents( evt ) );
            }
        },  // end private methods

        init : function() {
            this._.setSelector();
            this._.setEvents();
        },

        destroy : function() {
            this._.removeEvents();
        }
    };  // end accordion object

}) ( jQuery, window, document )();

$(function() { accordion.init(); });

// later if we want to remove accordion functionality
// all we do is call:
// accordion.destroy();

In Part 3, we will go ahead and convert this to jQuery plugin. Converting it to an object first ( in my opinion ) makes it easier. We will move aways from the singleton pattern above and put our methods into the Object prototype.

'Til next time,
-G