HiveBrain v1.2.0
Get Started
← Back to all entries
patternjavascriptModerate

Optimize this jQuery tab controller further

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
thistabfurthercontrollerjqueryoptimize

Problem

How do I optimize this code further? Goal is to edit "js-tab" in one place. This is what I tried to follow.

Full code here.

var ToggleTab = {

init: function() {
    this.tab = $(".js-tab");
    this.bindEvents();
},

bindEvents: function() {
    this.tab.on("click", this.toggleTab);
    //this.tab.on("click", $.proxy(this.toggleTab, this));
},

toggleTab: function(e) { 
    var showTab = $(this).data("tab-content");
    e.preventDefault();

    $(".js-tab").each(function() {
        if ($(this).data("tab-content") === showTab) {
           $(".js-tab-" + showTab).removeClass("hide");                
        } else {
            $(".js-tab-" + $(this).data("tab-content")).addClass("hide");
        }
    });
}

};

$(function() {

    ToggleTab.init();

});

Solution

I've always been partial to writing JavaScript classes that take a root element:

function TabController() {
    // Maintain reference to "this" in event handlers
    this.toggleTab = this.toggleTab.bind(this);
}

TabController.prototype = {

    $element: null,

    constructor: TabController,

    init: function(element) {
        this.$element = $(element).on("click", ".js-tab", this.toggleTab);

        return this;
    },

    toggleTab: function(event) {
        var that = this,
            showTab = event.target.getAttribute("data-tab-content");
        event.preventDefault();

        this.$element.find(".js-tab").each(function() {
            var tabContent = this.getAttribute("data-tab-content");

            if (tabContent === showTab) {
                that.$element.find(".js-tab-" + showTab).removeClass("hide");                
            } else {
                that.$element.find(".js-tab-" + tabContent).addClass("hide");
            }
        });
    }
};


Plus, I like to replace jQuery API calls that wrap cross browser native DOM methods -- e.g. $(foo).data("bar") most times can be replaced with element.getAttribute("data-bar").

And to use:

// using document.documentElement means you don't need a jQuery dom-ready event handler
var tabs = new TabController().init(document.documentElement);


While you technically only need one tabber per page, making this an instantiable class allows you to unit test your JavaScript.

describe("TabController", function() {

    var element, tabs, event;

    function FauxEvent(type, target) {
        this.type = type;
        this.target = target;
    }

    FauxEvent.prototype.preventDefault = function() {};

    describe("toggleTab", function() {

        beforeEach(function() {
            element = document.createElement("div");
            tabs = new TabController().init(element);
        });

        it("shows a tab", function() {
            element.innerHTML = [
                'Tab 1',
                ''
            ].join("");

            event = new FauxEvent("click", element.firstChild);

            tabs.toggleTab(event);

            expect($(element.lastChild).hasClass("hide")).toBe(false);
        });

    });

});


This makes your code truly modular. You could go a step further and make the CSS class names and data attributes customizable:

function TabController() {
    // Maintain reference to "this" in event handlers
    this.toggleTab = this.toggleTab.bind(this);

    this.options = {
        classPrefix: "js-tab",
        hiddenClass: "hide",
        dataAttribute: "data-tab-content"
    };
}

TabController.prototype = {

    $element: null,

    options: null,

    constructor: TabController,

    init: function(element, options) {
        this.$element = $(element).on("click", "." + this.options.classPrefix, this.toggleTab);
        jQuery.extend(this.options, options || {});

        return this;
    },

    toggleTab: function(event) {
        var that = this,
            showTab = event.currentTarget.getAttribute(this.options.dataAttribute);
        event.preventDefault();

        this.$element.find("." + this.options.classPrefix).each(function() {
            var tabContent = this.getAttribute(that.options.dataAttribute);

            if (tabContent === showTab) {
                that.$element.find("." + that.options.classPrefix + "-"
                    + showTab).removeClass(that.options.hiddenClass);
            } else {
                that.$element.find("." + that.options.classPrefix + "-"
                    + tabContent).addClass(that.options.hiddenClass);
            }
        });
    }
};


Some sample HTML


    Tab 1
    Tab 2

    Content A

    Content B

    var tabs = new TabController().init(document.documentElement);


JSFiddle: http://jsfiddle.net/CL8Ns/

Code Snippets

function TabController() {
    // Maintain reference to "this" in event handlers
    this.toggleTab = this.toggleTab.bind(this);
}

TabController.prototype = {

    $element: null,

    constructor: TabController,

    init: function(element) {
        this.$element = $(element).on("click", ".js-tab", this.toggleTab);

        return this;
    },

    toggleTab: function(event) {
        var that = this,
            showTab = event.target.getAttribute("data-tab-content");
        event.preventDefault();

        this.$element.find(".js-tab").each(function() {
            var tabContent = this.getAttribute("data-tab-content");

            if (tabContent === showTab) {
                that.$element.find(".js-tab-" + showTab).removeClass("hide");                
            } else {
                that.$element.find(".js-tab-" + tabContent).addClass("hide");
            }
        });
    }
};
// using document.documentElement means you don't need a jQuery dom-ready event handler
var tabs = new TabController().init(document.documentElement);
describe("TabController", function() {

    var element, tabs, event;

    function FauxEvent(type, target) {
        this.type = type;
        this.target = target;
    }

    FauxEvent.prototype.preventDefault = function() {};

    describe("toggleTab", function() {

        beforeEach(function() {
            element = document.createElement("div");
            tabs = new TabController().init(element);
        });

        it("shows a tab", function() {
            element.innerHTML = [
                '<a class="js-tab" data-tab-content="panel-1">Tab 1</a>',
                '<div class="js-tab-panel-1 hide"></div>'
            ].join("");

            event = new FauxEvent("click", element.firstChild);

            tabs.toggleTab(event);

            expect($(element.lastChild).hasClass("hide")).toBe(false);
        });

    });

});
function TabController() {
    // Maintain reference to "this" in event handlers
    this.toggleTab = this.toggleTab.bind(this);

    this.options = {
        classPrefix: "js-tab",
        hiddenClass: "hide",
        dataAttribute: "data-tab-content"
    };
}

TabController.prototype = {

    $element: null,

    options: null,

    constructor: TabController,

    init: function(element, options) {
        this.$element = $(element).on("click", "." + this.options.classPrefix, this.toggleTab);
        jQuery.extend(this.options, options || {});

        return this;
    },

    toggleTab: function(event) {
        var that = this,
            showTab = event.currentTarget.getAttribute(this.options.dataAttribute);
        event.preventDefault();

        this.$element.find("." + this.options.classPrefix).each(function() {
            var tabContent = this.getAttribute(that.options.dataAttribute);

            if (tabContent === showTab) {
                that.$element.find("." + that.options.classPrefix + "-"
                    + showTab).removeClass(that.options.hiddenClass);
            } else {
                that.$element.find("." + that.options.classPrefix + "-"
                    + tabContent).addClass(that.options.hiddenClass);
            }
        });
    }
};
<ul>
    <li class="js-tab" data-tab-content="content-a"><a href="#">Tab 1</a></li>
    <li class="js-tab" data-tab-content="content-b"><a href="#">Tab 2</a></li>
</ul>

<div class="js-tab-content-a">
    Content A
</div>
<div class="js-tab-content-b">
    Content B
</div>

<script type="text/javascript">
    var tabs = new TabController().init(document.documentElement);
</script>

Context

StackExchange Code Review Q#47806, answer score: 12

Revisions (0)

No revisions yet.