JQuery plugin does not work when used in multiple places on the same page

I am writing a jQuery plugin for a project that I am working on that is turning from tabbed content on desktop devices to an accordion on mobile devices. I used the jQuery Boilerplate ( https://github.com/jquery-boilerplate/jquery-boilerplate/blob/master/dist/jquery.boilerplate.js ) as the source template for my plugin.

The plugin is called for 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 the class ".tabs2accordion" on the page, but it starts to work incorrectly as soon as another element with the same class is added to the page. I created the code for the base code to demonstrate the problem. To show the problem, with a window size> 768px, try clicking on any of the titles and see how the content changes below each time the title is clicked. Then uncomment the HTML block and try clicking on the headers again.

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

I tried scrolling each item with the class "tabs2accordion" as follows:

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

But this also did not fix the problem.

Any ideas?

+8
javascript jquery jquery-plugins jquery-boilerplate
source share
2 answers

I have not used jQuery Boilerplate, but I believe that the problem here in your variable is 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 the plugin.

In the constructor for the plugin, the first line reads plugin= this; . Since the plugin not defined, it declares a variable in the global scope of the window object.

The fix is ​​to pass a link to the plugin object when setting hook $().on() . Past data is available in event handlers through the event parameter, which is passed in the data property.

Here is the solution ( 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 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" }); }); 
+6
source share

I rewrote your code following the jQuery Plugin creation standard.

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

Basically, I did two things:

  • Disabled from using data attributes for styling (switches to using the .active class .active )
  • Disconnected from using this everywhere, as it causes a whole wave of binding problems ...

$.fn.tabs2Accordion goes through all the selectors and applies $.tabs2Accordion . It also returns a selector for the chain (this is the standard in jQuery).

Then all internal methods are expressions of functions that are in the same scope as all your old this variables. This greatly simplifies the code, since you can reference these variables without passing them as a parameter or without having .bind( this ) somehow.

Finally, the old init() function has disappeared. 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' }) }) 
+1
source share

All Articles