I've just noticed something odd with Ajax and Drupal 7. It seems each javascript events is fired multiple times after Ajax has been used on a page.

I've tested it with a clean install and the examples module, and here are the steps to replicate it:

- clean minimum install of Drupal 7.7
- download and enable ajax_example module
- add the following javascript in the "simplest example" page:

$('div').click(function() {
  alert('clicked on '+$(this).attr('id'));
});

- navigate to "examples/ajax_example/simplest"
- click anywhere on the page: you will see one event being fired for each div tag
- then try selecting one of the options in the example. This will trigger the ajax stuff.
- after that, try clicking anywhere on the page: at least 3 or 4 events are fired for each div!

I've tried it with many several events and browser and everything is fired multiple times after an Ajax action.

Is this a bug or am I perhaps missing something?

Thank you,

Hubert.

Comments

Hubert_r’s picture

Ah, ok, I suspect it has to do with behaviors. Admitedly I don't quite get how it works, but I've just read that they applied when content is dynamically added. That might explain why these events are fired all over the place after Ajax has refreshed part of the page.

Here is how I added the javascript to the ajax example module:

function ajax_example_simplest($form, &$form_state) {
   // added this line:
   drupal_add_js('sites/all/modules/examples/ajax_example/test.js');
   ...

and then I created test.js

(function($) {

  Drupal.behaviors.examples = {
    attach: function() {
			
			$('div').click(function() {
				console.log('clicked on '+$(this).attr('id'));
			});
			
	}
  };

})(jQuery);

Is there a way to avoid the behavior to be triggered several time? Or something?

Thanks!

Hubert_r’s picture

One more observation and then I will stop my monologue:

if I do this ugly hack (as a test):

drupal_add_js(
   "jQuery(document).ready(function () { 
       jQuery('div').click(function() {
	  console.log('clicked on '+jQuery(this).attr('id'));
       });
     });", 
   array('type' => 'inline'),
);

instead of the following two steps:

drupal_add_js('sites/all/modules/examples/ajax_example/test.js');
// and then in test.js
(function($) {
   $('div').click(function() {
	console.log('clicked on '+$(this).attr('id'));
   });
})(jQuery);

Then I don't get the "multiple events firing after Ajax" issue, which suggests it definitely has to do with behaviors.

Hubert_r’s picture

Found the solution...

I should have done:

(function($) {
   $('div').once().click(function() {
     console.log('clicked on '+$(this).attr('id'));
   });
})(jQuery);

That is, adding the ".once()" jquery function to make sure the stuff happens only once, and not everytime the behavior gets triggered.

Still has to figure out how behaviors actually work :-)

quano1’s picture

when you using D7 ajax API, it call detachBehaviors then attachBehaviors again automatically
it explain why your behaviors call 2^(click times) because your behaviors have only attach:function () and doesn't have detach:function()
at your situation, you can do something like this:

Drupal.behaviors.custom = {
attach: function(context, settings) {
 $('.yourclass').click(function() { /*do something*/});
}
detach: function(context, settings, trigger) { //this function is option
  $('.yourclass').unbind(); //or do whatever you want;
}

you can find more information about detach in misc/drupal.js and about drupal ajax in misc/ajax.js
p/s: my english is not good very much, hope you can understand it

Jaypan’s picture

To elaborate on your code, it should look like this:

Drupal.behaviors.custom = {
attach: function(context, settings) {
$('.yourclass').once("myModule", function()
{
  $(this).click(function() { /*do something*/});
});
}
detach: function(context, settings, trigger) { //this function is option
  $('.yourclass').unbind(); //or do whatever you want;
}

Using the $.once() function ensures that handlers are not attached multiple times.

quano1’s picture

no.
once means you can only trigger the event one times.
sometimes, if you trigger the event after an ajax call, you will trigger 2 times and 3 times if 2 ajax calls, etc...

p/s: ah, you bind click() inside once(), i haven't tried that. but i thing this has no more lucks if you don't have detach

Jaypan’s picture

If you use $.once() you don't need to detach the handler. With your method, the handlers are detached and reattached everytime, which adds a lot of overhead.

quano1’s picture

really! thanks.
seem you have a lot of experience about Drupal ajax and views.
can you help me at this post: https://drupal.org/node/2018527
plz..

Jaypan’s picture

Sorry, I rarely use Views and cannot help you with that.

quano1’s picture

i think views is the most useful module in Drupal
why you don't use it

Jaypan’s picture

Views is essentially an SQL query builder, and is a great module for those who want/need it, but I write my SQL queries by hand, so I don't need it.

quano1’s picture

ah ha, you're right. i didn't thing there're someones write their query and ignore views. maybe this is another way for my situation

ManasiG’s picture

Hi,

Even I am facing this issue, but unbinding it or detaching it doesnt seem to be a solution for my scenario.
This is because, I may need to click on the button(or any trigger) again at some other time, then will "behaviours" come to know that it has to attach the event again and show the alert once?

Thanks!

Jaypan’s picture

Events should never be added to an element more than one time. If you attach the event multiple times, it will fire as many times as it has been attached. This is why you use $.once().

ManasiG’s picture

Hi ,

Thanks for the reply. I am not adding the event multiple times but want it to fire at different times. Let me explain you a little.
I have a div tag and say a button. On click of button, ajax fires and replaces the div tag with html fieldset tag.

Now this fieldset has 2 buttons in it(say Add and Delete). Now jquery comes into picture. When I click on Add button, I want another fieldset tag to be added, but the event get fired twice and 2 fieldset tags gets added. Again when clicked on Add button, instead of adding 1 fieldset tag, it adds 2 again. So now, total fieldset tags should be 3 but they are 5.

I have tried using jquery unbind, it partially solves my problem, when clicked Add button for the first time, it adds only 1 fieldset tag which is correct, but when I again click on it, it doesnt do anything.

So, I think even .once() will work it in same way. From what I have read about it, it will work on initial loaded elements. Do you think it will work on dynamically added elements from ajax response?

Thanks!!

Jaypan’s picture

It doesn't matter how the element is loaded, .once() will work on whatever element it is applied to.

You should probably show your code.

nithinkolekar’s picture

@Jaypan
even with once() and detach , ajax call is getting multiplied each time. You can check it at http://198.58.126.77/elec/des-candidates by clicking on the text below image.
js file
http://198.58.126.77/elec/sites/all/themes/simplicity/js/cust.js?ox0aae

Jaypan’s picture

I think you may be using $.once() incorrectly.

This:

$('.ajax_button', context).once().click(function(e) {
  // Click code
});

Should be this:

$('.ajax_button', context).once(function() {
  $(this).click(function(e) {
    // click code
  });
});
nithinkolekar’s picture

tried that too, still not working.
could d3js code is the problem? because that element is part of d3js svg and I dont see any *processed class is added to that element.
also tried http://codekarate.com/blog/drupal-7-prevent-duplicating-javascript-behav... 's comment suggestion

if (context == document) {
.
.

but so success.

Jaypan’s picture

I don't know what d3js is, nor do I know why you have the context==document included there. Also, you have Drupal.behaviors twice in the file. JS should only have one Drupal.behaviors declaration per file.

Strip it down to its bare base until you have something working, then build it up. Right now you've got too much in there adding too many variables to debug your issue.

nithinkolekar’s picture

why you have the context==document included there

"
thats because at http://codekarate.com/blog/drupal-7-prevent-duplicating-javascript-behav... its mentioned as one of the solution for this particular issue. Few lines from that site by Pierre Buyle :
"So if you want to balance stability and performance, always uses context AND jQuery.once(). Not just one of them."

you have Drupal.behaviors twice in the file

there are none I think and I assume you means Drupal.attachBehaviors();. When that line is commented ,ajax is working fine and not getting multiplied in subsequent clicks. There isn't any official doc which mentions when should/avoid using Drupal.attachBehaviors(); , but some of the code at SO is having this line but without any internal info.

Thanks Jaypan for the help on this issue.

Jaypan’s picture

thats because at http://codekarate.com/blog/drupal-7-prevent-duplicating-javascript-behav... its mentioned as one of the solution for this particular issue. Few lines from that site by Pierre Buyle :

"So if you want to balance stability and performance, always uses context AND jQuery.once(). Not just one of them."

I actually have to disagree with him. Using context == document will mask the issue you are facing, in that context only equals document on initial page load, and therefore code inside it will only ever be executed a single time. If the element you are attaching listeners to exists in the DOM on page load, the listener will be attached. But Drupal's behaviors framework is meant to be used to attach JS not just on page loads, but also on AJAX loads. On an AJAX load, context will not equal document, and therefore listeners will not be attached to any values that were inserted into the DOM using AJAX. The solution he proposes is a band-aid solution to a mistake that has been made somewhere else. It does not fix the initial problem, and will lead to problems if you start working with AJAX on your system.

there are none I think and I assume you means Drupal.attachBehaviors()

The file you linked to has changed. Now there is only Drupal.behaviors.listload, but previously there was another Drupal.behaviors declaration in the file as well.

Looking at your code as it stands now, it will not attach the event listener more than once. If you are still seeing this, it means one of two things:

1) You are seeing cached code, whether it be a Drupal cache, or a browser cache, or some other cache. Or:

2) You are attaching more than one bit of JS that is attaching the event listener. Maybe you have similar code in another file.

You may want to read this for a better understanding of D7's JS framework: https://www.jaypan.com/tutorial/high-performance-javascript-using-drupal...

psegarel’s picture

I haven't found any info in the docs, if anyone does, a link would be welcome :) In any case, try adding context as a parameter to your jQuery object, this should prevent the object being added multiple times after an Ajax call.

     Drupal.behaviors.yourCustomName = {
           attach:function (context, settings) {
                   $('div', context ).click(function() {
	                  console.log('clicked on '+$(this).attr('id'));
                         });
                      }
          }
nithinkolekar’s picture

The file you linked to has changed.

I had saved old js as cust.js.old which had messy commented code which looks like multiple behavior sections. I cleaned it and tidy it for readability and saved as active js file.

After reading the your link https://www.jaypan.com/tutorial/high-performance-javascript-using-drupal... about attachBehavior like

This should be called anytime new content is inserted into the DOM, such as after an AJAX load

Should it be mandatory to have Drupal.attachBehavior even though click event element .ajax-button is not a part of ajaxly loaded content (see image for info)?.
Even if we have Drupal.attachBehavior without any context then isn't once() function should take care of parsing *-processed class on sunsequent ajax call which triggers Drupal.attachBehavior?. But Once() function is not adding such class like .ajax-button-processed on page load when it supposed to. am I right here?
image:( attached from #2459953: files placeholder for drupal.org forum posts)
ajaxload-behavior

Jaypan’s picture

Should it be mandatory to have Drupal.attachBehavior even though click event element .ajax-button is not a part of ajaxly loaded content (see image for info)?.

Drupal.attachBehaviors() is to attach new even listeners to ajax loaded content. Therefore the element that is clicked is irrelevant, it's the newly inserted code that matters. Due to the hook events in Drupal core, it should generally not be assumed that what is inserted into the DOM will not have content that needs JS listeners applied. Therefore, any time new content is inserted into the DOM, that content should be passed through Drupal.attachBehaviors() after it has been inserted.

So the fact that the link that is clicked to trigger the AJAX load is not part of the newly loaded content, is irrelevant to whether or not Drupal.attachBehaviors() should be called.

Even if we have Drupal.attachBehavior without any context

You should never be calling Drupal.attachBehaviors() without passing it any context. That only happens on page load, and the whole DOM gets parsed. When you call Drupal.attachBehaviors() yourself, you should always pass it the (parent of) the new content, so that only that content is parsed for new HTML that requires event listeners to be attached.

nithinkolekar’s picture

I am almost near to end this bug and found that original issue is with d3js collapse event which collapses the child elements but won't load child elements by ajax, instead it is loading saved data inside Drupal.settings which is available at client side as CDATA.

So my final query is (lets assume it could be anything not just d3js as you are not familiar with it)
how to forcefully trigger Drupal.attachBehaviors when elements are re-added/re-visibled?
or
Isn't browser's job to remember the event attached to element and re-attach when elements are added again to DOM?(just a thought)

raoofabdul’s picture

how about using a FLAG as shown below?

Here is the custom FLAG i am using to prevent multiple calls

Drupal.behaviors.custom.click_set
Drupal.behaviors.custom = {
    attach: function(context, settings) {
        if(!Drupal.behaviors.custom.click_set){
            $('.yourclass').click(function() { 

                /*do something*/

            });
            Drupal.behaviors.custom.click_set = true;
        }
    }
};
Jaypan’s picture

That will work, but the jQuery $.once() method is the Drupal way of doing it.

nithinkolekar’s picture

attaching the gif of whats happening

svg-clickevent-lost-gif

ManasiG’s picture

Hi,
Below is the function attached on click of 'ADD' button

Drupal.behaviors.webform_custom_1 = {
        attach: function (context, settings) {
            $(".addRow", context).once('newRow', function () {
                $(".addRow", context).click(function (event) {
                    event.preventDefault();
                    
                    var $id = $('#pdfContainer fieldset:last-of-type input:first-of-type').attr("id");
                    
                    $lastChar = parseInt($id.substr($id.length - 1), 10);
                    
                    $lastChar = $lastChar + 1;
                    $newRow = "<fieldset>\
            <legend>PDF Container</legend>\
            <label for='firstNm_" + $lastChar + "' style='margin-top:25px;'>First Name:</label>\
            <input type='text' id='firstNm_" + $lastChar + "'/></br>\
            <label for='lastNm_" + $lastChar + "'>Last Name:</label>\
            <input type='text' id='lastNm_" + $lastChar + "'/></br>\
            <input type='button' value='Add' id='add_" + $lastChar + "' class='addRow'/>\
            <input type='button' value='Delete' id='del_" + $lastChar + "' class='delRow'/>\
            </fieldset>";

                    $('#pdfContainer').append($newRow);
                    
                    return false;

                });
            });
        }
    };

I already have one fieldset created on my page.
Issue is, when I click on 'ADD' button on my page, this event get triggered and a new fieldset gets added, which is perfectly as I want it to behave.
But, in this newly added fieldset, I have 'ADD' button again, and on click of this 2nd 'ADD' button, I want a new fieldset to get added, which doesn't happen.

Could you please help me in this?
Thanks!