James Howell
James Howell

Reputation: 1442

JQuery plugin not working when used in multiple places in a single page

I am writing a JQuery plugin for a project I'm working on which turns from tabbed content on desktop devices to an accordion on mobile devices. I've used JQuery Boilerplate (https://github.com/jquery-boilerplate/jquery-boilerplate/blob/master/dist/jquery.boilerplate.js) as an initial pattern for my plugin.

The plugin is called on any element with the class ".tabs2accordion" as shown here:

 $(".tabs2accordion").tabs2Accordion({state:"desktop"});

The plugin works as expected if there is only one element with ".tabs2accordion" class on a page but starts to malfunction as soon as another element with the same class is added to the page. I've created a codepen of the basic code to demo the issue. To show the issue, on a window size of >768px try clicking any of the titles and observe how the content below changes as each title is clicked. Next uncomment the block of HTML and try clicking on the titles again.

http://codepen.io/decodedcreative/pen/MyjpRj

I have tried looping through each element with the class "tabs2accordion" like this:

$(".tabs2accordion").each(function(){
    $(this).tabs2Accordion({state:"desktop"});
});

But this didn't fix the issue either.

Any ideas?

Upvotes: 8

Views: 943

Answers (2)

justinledouxweb
justinledouxweb

Reputation: 1357

I rewrote your code following jQuery's Plugin creation standard.

http://codepen.io/justinledouxmusique/pen/GZrMgB

Basically, I did two things:

  • Moved away from using data attributes for styling (switched to using an .active class instead)
  • Moved away from using this everywhere, as it bring a whole wave of binding issues...

$.fn.tabs2Accordion loops through all the selectors, and applies $.tabs2Accordion. It also returns the selector for chaining (it's a standard in jQuery).

Then, all the internal methods are function expressions which are in the same scope as all your old this "variables". This simplifies the code greatly as you can refer to those variables without passing them in as a parameter or without having to .bind( this ) somehow.

Finally, the old init() function is gone. Instead, I put the code at the end of the $.tabs2Accordion function.

Hope this helps!

(function ( window, $ ) {
    $.tabs2Accordion = function ( node, options ) {
        var options = $.extend({}, {
                    menuSelector: '.tabs2accordion-menu',
                    tabContentSelector: '.tabs2accordion-content'
                }, options )

        var $element = $( node ),
                $menu = $element.find( options.menuSelector ),
                $tabs = $element.find( options.tabContentSelector ),
                $accordionTriggers = $tabs.find( 'h3' )

        var resizeTabs2Accordion = function () {
            $element.outerHeight( !$element.is( '[data-nested-menu]' )
                ? $element.find( 'div.active' ).outerHeight() + $menu.outerHeight()
                : $element.find( 'div.active' ).outerHeight() )
        }

        var showTabContent = function () {
            var $this = $( this ) // This will be the clicked element

            $menu
                .find( '.active' )
                    .removeClass( 'active' )

            $element
                .find( '.active' )
                    .removeClass( 'active' )

            $( $this.find( 'a' ).attr( 'href' ) )
                .addClass( 'active' )

            $this
                .find( 'a' )
                    .addClass( 'active' )

            resizeTabs2Accordion()

            return false
        }

        var showAccordionContent = function () {
            var $this                   = $( this ),
                    $parent                 = $this.parent(),
                    mobileIsActive  = $parent.data( 'active-mobile' )

            $( '[data-active-mobile]' )
                .not( $parent )
                    .data( 'active-mobile', false )

            $parent
                .data( 'active-mobile', mobileIsActive ? false : true )
        }

        // The equivalent of init()
        $tabs
            .removeClass( 'active' )
            .first()
                .addClass( 'active' )

        $element.addClass( 'loaded' )

        $menu.on( 'click', 'li', showTabContent )

        $( window ).on( 'resize', resizeTabs2Accordion )

        resizeTabs2Accordion()

        console.log( $element )
    }

    $.fn.tabs2Accordion = function ( options ) {
        this.each( function ( index, node ) {
            $.tabs2Accordion( node, options )
        })

        return this
    }
})( window, jQuery )

$( window ).on( 'load', function () {
    $( '.tabs2accordion' ).tabs2Accordion({
        state: 'desktop'
    })
})

Upvotes: 1

Steve H.
Steve H.

Reputation: 6947

I have not used jQuery Boilerplate, but I believe the problem here is with your variable called plugin.

Nowhere in your code do you declare a variable called plugin. When I stop the debugger in Plugin.prototype.showTabContent, I can evaluate window.plugin and it returns the global value for plugin.

In the constructor for Plugin, the first line reads plugin= this;. Since plugin is not defined, it is declaring the variable at global scope on the window object.

The fix is to pass a reference to the plugin object when setting up the $().on() hook. The data passed is available in the event handlers via the event parameter that is passed in the data property.

Here is the solution (at http://codepen.io/shhQuiet/pen/JXEjMV)

(function($, window, document, undefined) {
  var pluginName = "tabs2Accordion",
    defaults = {
      menuSelector: ".tabs2accordion-menu",
      tabContentSelector: ".tabs2accordion-content"
    };

  function Plugin(element, options) {
    this.element = element;
    this.$element = $(this.element);
    this.options = $.extend({}, defaults, options);
    this.$menu = $(this.element).find(this.options.menuSelector),
    this.$tabs = $(this.element).find(this.options.tabContentSelector),
    this.$accordionTriggers = $(this.element).find(this.$tabs).find("h3");
    this._defaults = defaults;
    this._name = pluginName;
    this.init();
  }

  Plugin.prototype = {

    init: function() {
      //Set all the tab states to inactive
      this.$tabs.attr("data-active", false);

      //Set the first tab to active
      this.$tabs.first().attr("data-active", true);

      //If you click on a tab, show the corresponding content
      this.$menu.on("click", "li", this, this.showTabContent);

      //Set the dimensions (height) of the plugin
      this.resizeTabs2Accordion({
        data: this
      });

      //If the browser resizes, adjust the dimensions (height) of the plugin
      $(window).on("resize", this, this.resizeTabs2Accordion);

      //Add a loaded class to the plugin which will fade in the plugin's content
      this.$element.addClass("loaded");

      console.log(this.$element);

    },

    resizeTabs2Accordion: function(event) {
      var contentHeight;
      var plugin = event.data;

      if (!plugin.$element.is("[data-nested-menu]")) {
        contentHeight = plugin.$tabs.filter("[data-active='true']").outerHeight() + plugin.$menu.outerHeight();
      } else {
        contentHeight = plugin.$tabs.filter("[data-active='true']").outerHeight();
      }

      plugin.$element.outerHeight(contentHeight);
    },

    showTabContent: function(event) {
      var $target;
      var plugin = event.data;
      plugin.$menu.children().find("a").filter("[data-active='true']").attr("data-active", false);
      plugin.$tabs.filter("[data-active='true']").attr("data-active", false);
      $target = $($(this).children("a").attr("href"));
      $(this).children("a").attr("data-active", true);
      $target.attr("data-active", true);
      plugin.resizeTabs2Accordion({data: plugin});

      return false;
    },

    showAccordionContent: function(event) {
      var plugin = event.data;
      $("[data-active-mobile]").not($(this).parent()).attr("data-active-mobile", false);

      if ($(this).parent().attr("data-active-mobile") === "false") {
        $(this).parent().attr("data-active-mobile", true);
      } else {
        $(this).parent().attr("data-active-mobile", false);
      }
    }

  };

  $.fn[pluginName] = function(options) {
    return this.each(function() {
      if (!$.data(this, "plugin_" + pluginName)) {
        $.data(this, "plugin_" + pluginName, new Plugin(this, options));
      }
    });
  };

})(jQuery, window, document);

$(window).on("load", function() {
  $(".tabs2accordion").tabs2Accordion({
    state: "desktop"
  });
});

Upvotes: 6

Related Questions