Forum Moderators: open

Message Too Old, No Replies

More troubleshooting, what's preventing this .click() function?

         

csdude55

8:27 am on Nov 13, 2022 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member Top Contributors Of The Month



I have a button that looks like:

<button class="button" onClick="console.log('click worked')">Do This</button>


Then in a separate .JS (using jQuery) that loads at the bottom of the page, I have:

$('.button')
.mouseover(function() {
console.log('moused over');
})
.click(function(e) {
console.log('made it to .button click');
});


For some reason that I can't find, when I mouse over or click the button it does not trigger the functions! I get the "click worked" from onClick, but not "moused over" or "made it to .button click". And naturally, there are no errors in the console :-/

Other jQuery functions in the same .JS file work, so it's not an issue of a syntax error in the document.

I also tried copying EVERYTHING to JSFiddle for testing, and the button functions worked there! So it's definitely not an issue of a syntax error, the problem has to be somewhere else that's somehow interfering.

Any suggestions on tracking down exactly where the problem is, other than just deleting things a little at a time and seeing what happens? With no errors in the console, I'm struggling to find it.

csdude55

9:59 pm on Nov 13, 2022 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member Top Contributors Of The Month



I've found the issue, but not the solution. Hopefully this will make sense, and someone can see something that I don't.

I have the main PHP page (let's call it MAIN), then when the user clicks on an icon it uses load() to open a second PHP script (let's call it SECOND). SECOND has the button I'm talking about, <button class="button"></button>.

MAIN has javascript.js at the top and footer.js at the bottom. The footer.js file has functions that aren't critical, I just have them load later so that the page can be usable faster.

SECOND has another script, internal.js.

I originally had the $('.button').click() function in footer.js. Then I discovered that it would run on buttons on MAIN, just not on SECOND.

So I moved the $('.button').click() function to internal.js, and then it worked on SECOND!

Then I moved it to javascript.js (that runs at the top), but then it didn't work. So the issue appears to be that SECOND and internal.js are unable to execute functions set on MAIN.

To confirm this, I added this to footer.js:

var tester = 'made it to footer.js';

Then I added this to internal.js:

if (tester)
console.log(tester + ', made it to internal.js');

When I click to open SECOND, I DO get the console.log message showing that tester was properly set!

Then I took it a step further. There's a function in footer.js called function requiredFields(), so in internal.js I added:

console.log(typeof requiredFields);

and it shows the word "function!"

So apparently internal.js CAN read and execute variables and functions set in javascript.js and footer.js, but for some reason it can't execute the $('.button').click() function?

Fotiman

12:35 pm on Nov 14, 2022 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member Top Contributors Of The Month



The issue here is that this code will execute as soon as it's encountered:

$('.button')
.mouseover(function() {
console.log('moused over');
})
.click(function(e) {
console.log('made it to .button click');
});

So, if it's in footer.js, it will execute before the user has clicked the button to load SECOND, so $('.button') will run against anything with class "button" that is currently accessible in the DOM, and attach handlers to those items. If you load in more items with that class AFTER this runs, those items will not be affected by this unless you explicitly get all of those items and attach handlers to those as well. When you move this to internal.js, it will only attach the event handlers after the user clicks the button to load SECOND. If you move it to the top of the page, there are no DOM elements that will match when this runs, so you won't attach event handlers to any items.
What you might be looking for is "Event Delegation". That is, you want to attach your event handler to a common ancestor element instead of multiple individual elements, and then when the event bubbles up the DOM tree, your single handler will be triggered.
Here's the jQuery documentation about Event Delegation:
[learn.jquery.com...]

csdude55

7:06 pm on Nov 14, 2022 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member Top Contributors Of The Month



By George, @Fotiman, you've done it! LOL

I had to push some wires around to make it work, but finally got this modification to work:

$(document)
.on('mouseover', '.button', function(e) {
console.log('moused over');
})

I'm not sure how I feel about tracking a 'mouseover' on the entire $(document), though. Do you think this is the best way to make it do what I expected?

csdude55

9:51 pm on Nov 14, 2022 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member Top Contributors Of The Month



As an alternative, I could modify footer.js with this:

function buttonClick(e) {
console.log('made it to .button click');
}

$('.button').click(function(e) { buttonClick(e); });

then in internal.js, just add:

$('.button').click(function(e) { buttonClick(e); });

It's not such a pain to just include that line in internal.js (instead of duplicating the entire $('.button') function, which is really a lot more than just console.log()), so that would be acceptable.

Which do you think is better / faster? Using $(document).on('click', '.button', function(e) { ... }); or the above?

Fotiman

3:48 am on Nov 15, 2022 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member Top Contributors Of The Month



If you have $(‘.button’).click(fn) in both places, you’ll possibly end up attaching the click handler to some buttons twice (those that are present both before and after interval.js loads). And the more handlers you add, the more performance can be impacted, when compared to just a single handler. So I would probably recommend attaching just a single listener.

csdude55

8:07 pm on Nov 15, 2022 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member Top Contributors Of The Month



Following that logic, @Fotiman...

I have 11 instances in footer.js of $(element).click(...) (plus 2 .mouseover and 2 .mouseout).

If I'm understanding correctly that $(document).on(...) would be listening to the entire document for any event, would it be faster to process if I placed all of them under that $(document)?

Like this (not tested, please ignore any typos):

$(document)
.on('click', '.button', e => { ... })

.on('click', '#foo', () => { ... })

.on('click', '#bar', () => { ... })
.on('mouseover', '#bar', () => { ... })
.on('mouseout', '#bar', () => { ... });

robzilla

8:33 pm on Nov 15, 2022 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member Top Contributors Of The Month



From the docs:
Attaching many delegated event handlers near the top of the document tree can degrade performance. Each time the event occurs, jQuery must compare all selectors of all attached events of that type to every element in the path from the event target up to the top of the document. For best performance, attach delegated events at a document location as close as possible to the target elements.

[api.jquery.com...]

csdude55

9:36 pm on Nov 15, 2022 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member Top Contributors Of The Month



Does the process I described (combining all of them under $(document)) count as "delegated", or just the one with ".button" as the selector?

Fotiman

12:03 am on Nov 16, 2022 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member Top Contributors Of The Month



The process you described does count as "delegated". Delegated event handlers just means that you're listening for events at some ancestor element where you expect descendants of that element to bubble up the events. Also, with delegated events, you need to specify which target of the event is the one you needed to listen for. The jQuery documentation gives a good example, where a click event on an <a> element bubbles up to it's parent <li>, then it's parent <ul>, and so on.
[learn.jquery.com...]

robzilla

12:55 am on Nov 16, 2022 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member Top Contributors Of The Month



Delegation only applies to on() here, with $('.button').click() there's no delegation but an event listener tied directly to each matching element.

Just wanted to reiterate that it's best to avoid tying event listeners to $(document) whenever there's another candidate closer to the target elements. For example, if all your .button elements were to be contained by a form with the ID "contactForm", it would be more efficient to use $('#contactForm').on('click', '.button', fn). You probably won't really notice, though, unless you have a very large document and/or a ton of event listeners.

csdude55

1:52 am on Nov 16, 2022 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member Top Contributors Of The Month



I set everything up to use infinite scroll, so it IS possible that a page could be suuuuuuper long. And then only really need access to .button one time :-/ So performance could be an issue I guess.

Since .button always appears within a form, I thought that using $('form') would be a good option. But it doesn't work on that second page, presumably because the second form hasn't been added to the DOM yet.

When I load() the second page, though, I use it to replace (I guess?) an empty DIV tag on the first page. So adding the ID of that empty element worked:

$('form, #emptyElement').on('click', '.button', e => { ... });

Is there value to adding return false; to each of these functions? I understand from the docs that doing so would trigger both event.stopPropagation() and event.preventDefault().

I've learned a lot in this thread, I'm glad that I started it!

robzilla

10:21 am on Nov 16, 2022 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member Top Contributors Of The Month



presumably because the second form hasn't been added to the DOM yet.

Yes, the element you tie it to needs to already be on the page. So if the forms have a shared container, that would be a better candidate.

Is there value to adding return false; to each of these functions? I understand from the docs that doing so would trigger both event.stopPropagation() and event.preventDefault().

I think you answered your own question there :-)

csdude55

6:40 pm on Nov 16, 2022 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member Top Contributors Of The Month



I think you answered your own question there :-)

I was initially concerned that doing so would prevent a second mouseover or click on the same page, but in practice I don't think that's the case. So the concept is a little confusing, I'm not sure exactly what it is that I'm stopping?

Meaning, let's say that I have 10 elements with .button, and then I have:

$(document).on('mouseover', '.button', () => { console.log('moused over') });

If I'm understanding correctly, at runtime the browser sees that it needs to listen to the entire document for any mouseover, and if that mouseover is on the element '.button' then it triggers the function.

Now, let's say that I modified the function to:

$(document).on('mouseover', '.button', () => {
console.log('moused over');
return false;
});

// I also tried this for testing
$(document).on('mouseover', '.button', e => {
console.log('moused over');

if (e.stopPropagation) e.stopPropagation();
e.cancelBubble = true;

if (e.preventDefault) e.preventDefault();
else e.returnValue = false;

return false;
});

In practice I see that mousing over any of the .button elements triggers the function, so "return false" didn't change anything that I can see.

So what exactly happens?

robzilla

8:41 pm on Nov 16, 2022 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member Top Contributors Of The Month



They only apply to the event that is triggered. preventDefault [developer.mozilla.org] means that "any default action normally taken by the implementation as a result of the event will not occur." If the event is a click on a link and you call preventDefault() then the browser will not follow that link as it normally would. stopPropagation [developer.mozilla.org] "prevents further propagation of the current event in the capturing and bubbling phases."

So it's basically like saying after this function reaches return false;, I don't want anything else to be affected by this event. It doesn't stop all event processing for the document indefinitely, it only affects the current event. Move the mouse away from the .button, move over it again and that's a new event.

Don't see much value in using it on a mouseover, but maybe there's a use case imaginable. I've used preventDefault mostly on links and buttons, e.g. a form submit button where the form data is actually collected and processed by Javascript and you don't want the form to actually be POSTed.

csdude55

9:19 pm on Nov 16, 2022 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member Top Contributors Of The Month



I gotcha. I regularly set "onSubmit='return false'" on forms because I use onClick to process things (exactly like you described), which appears to do the same thing as stopPropagation and preventDefault.

I'm going to start another thread on this issue, I realize that I'm doing something similar elsewhere for some reason and I might be doing it all wrong :-/