Fire click event for only innermost element in jQuery - javascript

I'm trying to fire a click event on the innermost element in the HTML tree, but since there is a click even tied to its parent container, both events are fired, which is not what I need. I have fixed this before with stopPropagation(), but I can't seem to get it to work today.
jQuery('#parent li').click(function() {
jQuery(this).children('.contained').slideDown();
});
jQuery('.contained').click(function() {
Query(this).slideUp();
});
and let's say here is our HTML:
<ul id="parent">
<li>
click to show more
<p class="contained">click to hide</p>
</li>
</ul>
I believe this won't validate since it has a p contained within an li, but let's ignore that momentarily for simplicity's sake. How can I have the inner element slideUp() without have the parent click even trigger it to slideDown() immediately after?

return false to stop the bubbling:
jQuery('.contained').click(function() {
Query(this).slideUp();
return false;
});
Note that returning false also prevent the default behavior of the event.
Read more here.
Or use the event object's stopPropagation function:
jQuery('.contained').click(function(e) {
Query(this).slideUp();
e.stopPropagation();
});

The answer is to stop the propagation of the event. You can use stopPropagation and its twin, stopImmediatePropagation to do this or you can both stop propagation and prevent the default action by returning false from the handler. The stopPropagation method will prevent event bubbling, that is, the propagation of the event up the DOM tree. The stopImmdiatePropagation will do that but also prevent other handlers at the same level from firing. Returning false is equivalent to using stopPropagation and preventDefault on the event.

Two ways to do it, stop the propagation or combine the two click handlers and adjust code according to event.target
Target method:
jQuery('#parent li').click(function(event) {
var $tgt=jQuery(event.target)
if (!$tgt.is('.contained') ){
jQuery(this).children('.contained').slideDown();
}else{
$tgt.slideUp();
}
});

Related

JQuery selector still working after I remove the class?

I have two jquery functions that work together, one depends on a class, another removes the class.
Once it is removed I would expect the functionality to stop working, but it carries on?
Whats going on?
Here is the fiddle, try it out for yourself.
<div class="container disabled">
Go to Google
</div>
<label>
<input type="checkbox" />Enable link</label>
The JS
$('.disabled > a').click(function (e) {
e.preventDefault();
e.stopPropagation();
alert('should stop working');
});
$('input[type=checkbox]').change(function () {
$('.container').removeClass('disabled');
});
It looks like you want to be using delegated event handlers rather than static event handlers. Let me explain.
When you run a line of code like this:
$('.disabled > a').click(function (e) {
this installs an event handler on any objects that match the selector at that moment in time. Those event handlers are then in place forever. They no longer look at what classes any elements have. So changing a class after you install a static event handler does not affect which elements have event handlers on them.
If you want dynanamic behavior where which elements respond to an event does depend upon what classes are present at any given moment, then you need to use delegated event handling.
With delegated event handling, you attach the event "permanently" to a parent and then the parent evaluates whether the child where the event originated matches the select each time the event fires. If the child no longer matches the select, then the event handler will not be triggered. If it does, then it will and you can add/remove a class to cause it to change behavior.
The general form of delegated event handlers are like this:
$("#staticParent").on("click", ".childSelector", fn);
You ideally want to select a parent that is as close to the child as possible, but is not dynamic itself. In your particular example, you don't show a parent other than the body object so you could use this:
$(document.body).on("click", ".disabled > a", function() {
e.preventDefault();
e.stopPropagation();
alert('should stop working');
});
This code will then respond dynamically when you add remove the disabled class. If the disabled class is present, the event handler will fire. If it is not present, the event handler will not fire.
Working demo: http://jsfiddle.net/jfriend00/pZeSA/
Other references on delegated event handling:
jQuery .live() vs .on() method for adding a click event after loading dynamic html
jQuery .on does not work but .live does
Should all jquery events be bound to $(document)?
JQuery Event Handlers - What's the "Best" method
jQuery selector doesn't update after dynamically adding new elements
Changing the class after the event handler is bound has absolutely no effect as the event handler is not suddenly unbound, it's still bound to the same element.
You have to check for the class inside the event handler
$('.container > a').click(function (e) {
if ( $(this).closest('.container').hasClass('disabled') ) {
e.preventDefault();
e.stopPropagation();
}
});
$('input[type=checkbox]').change(function () {
$('.container').toggleClass('disabled', !this.checked);
});
FIDDLE
When the selector runs, it gets a list of elements including the one in question and adds a click event handler to it.
Then you remove the class - so any subsequent jQuery selectors wouldn't get your element - but you have already attached the event so it will still fire.
The selector you have used runs on the line you declared it - it isn't lazily initialized when clicks happen.

jQuery stopPropagation is ignored

My goal is to prevent all the click events (hiding/showing of elements in the HTML when clicked) unless a certain condition is met (the user has a certain word in an input element).
So i tried to add that logic to the click handler of the document or "html" but the click handler of the other element fired first because of bubble up.
So i tried attaching that logic to "*", and now that click handler fires first- but propagates it to the the other element too, ignoring stopPropagation, preventDefault and return false.
$(document).ready(function(){
$("*").click(function(event){
if ($("#user").val() !== "admin"){
console.log("1");
event.stopPropagation();
event.preventDefault();
return false;
}
});
$("#user").click(function(event){
console.log("2");
// do something
});
});
Why "2" is written to the console after "1" when there shouldn't be any further propagation because of return false/stopPropagation?
How else can i achieve my goal using jQuery?
Thanks!
stopPropagation() prevents the event propagating any further up the ancestor tree. However, it doesn't prevent the remaining event handlers on the current from being fired.
To do this (prevent further propagation and prevent any further event handlers on the current element from being fired), you need to call stopImmediatePropagation() (instead, not as well).
Attaching an event handler to every element in this manner, and calling stopImmediatePropagation() (as well as preventDefault()) will prevent all clicks from having an effect; providing no event handlers are bound before (as handlers are executed in order; you can't undo a handler which has already fired).
This doesn't make it nice though, as finding, enumerating over, and attaching a handler to every element is pretty costly.
To make it nicer, your options are either:
Attach a click event to document, and simply preventDefault() and sacrifice stopImmediatePropagation().
Check the state of #user in each event handler; you can ease the pain of this by rolling your own wrapper function;
function checkUserState(then) {
return function () {
if ($("#user").val() !== "admin") {
then.apply(this, arguments);
}
};
};
... use like so;
$("#user").click(checkUserState(function(event){
console.log("2");
}));
As noted in the comments, I'm purposefully avoiding the suggestion of using event delegation, as whilst allows attaching only one event handler instead of n, it doesn't allow you to stopPropagation() of events.

jQuery click events overriding each other

I have the following jQuery:
$('.io-sidebar-section').click(function () {
console.log('Section Clicked');
$(this).next().fadeToggle('fast',function(){});
});
$('.io-sidebar-section-advanced-toggle').click(function(){
$(this).parent().next().children('.io-sidebar-link-advanced').fadeToggle('fast',function(){});
});
the advanced toggle is inside of a sidebar section. When I click on the advanced toggle, it executes the sidebar section click.
How can I seperate these two out?
You can use the stopPropagation method of the event object inside the click event handler for the child element:
$('.io-sidebar-section-advanced-toggle').click(function(e){
e.stopPropagation();
$(this).parent().next().children('.io-sidebar-link-advanced').fadeToggle('fast',function(){});
});
From the jQuery docs, here's what stopPropagation does:
Prevents the event from bubbling up the DOM tree, preventing any
parent handlers from being notified of the event.
As mentioned in the comments, if you prefer you can alternatively use return false in the event handler (in this particular case, as far as I can tell anyway - it will also cause preventDefault which may not be what you want to happen). My personal preference is to use stopPropagation but it's completely up to you.
You can avoid events bubbling up by using jQuery's bind function and preventBubble argument.
http://api.jquery.com/bind/

Issue regarding live event

I was just reading http://api.jquery.com/event.stopPropagation/
Since the .live() method handles
events once they have propagated to
the top of the document, it is not
possible to stop propagation of live
events
I was a bit confused with this statement, Can someone please explain me the same with some example?
Live method binds a handler to the document, and identifies which element triggered the event from the event.target property.
So the actual handler is at the top (in terms of hierarchy).
The stopPropagation stops the bubbling from going up the DOM hierarchy, but since the handler is at the top already (in the .live case) there is no upper place to bubble to ..
example attempt ..
- document
- div
- link
you bind a click event handler to the link (with the bind or click method).
When you click the link, the handler is triggered, but in addition the click event goes up the DOM until it reaches the document, and will also trigger click handlers bound to the div and document. (unless you use the .stopPropagation)
Alternatively if you use the .live method to bind the event handler, it will be bound to the document. If you now click the link, the event (which will not fire right away, since no handler is bound to it) will naturally go up the DOM (triggering the click handlers it encounters). Once it reaches the document it will trigger our own handler. But there is no upper to go, so the stopPropagation is useless at this point.
HTML:
<div id="outer">
<div id="inner">
<span>.live() version</span>
</div>
</div>
<div id="outer2">
<div id="inner2">
<span>.delegate() version</span>
</div>
</div>
JS:
$(function(){
$('#inner2').delegate('span', 'click', function(e){
e.stopPropagation(); // indeed, no alert!
});
$('span').live('click', function(e){
e.stopPropagation();
// we would expect the propagation to stop here, so no alert, right?
});
$('#outer2, #outer').click(function(){ alert("Don't reach me!"); });
});
Example: http://jsfiddle.net/knr3v/2/
.live() only does its magic once the event has already bubbled, so stopping the event from propagating is useless - it's too late, it has already reached the top of the tree and propagated...

How to prevent delegated handlers on a parent without preventing default in jQuery?

Is there any way to prevent a click from an <a> triggering delegated click handlers on its parent, while allowing the the <a>'s default behavior to occur (navigating to the href).
Here's an example that illustrates what I'm asking.
<div class="top">
<div class="middle">
link
</div>
</div>
And my JavaScript:
$(".top").delegate(".middle", "click", function(event) {
alert("failure");
});
$(".top").delegate(".link", "click", function(event) {
// ???
});
In this case, I want to be navigated to google.com when I click the link, but must NOT see the alert("failure") on my way out.
There are a few restrictions to the solution:
All event handlers must be delegated off of $(".top"), as I potentially have thousands of these in the page.
The navigation must be accomplished using browser default behavior, rather than window.location = $(this).attr("href") or similar
Using normal event binding, I could do an e.stopPropagation() in a click handler for the <a>, but that won't work due to the nature of delegation. jQuery provides another method called .stopImmediatePropagation() that describes what I want (preventing other handlers on current element, in this case the element that holds the delegated handlers), but does not actually accomplish it in this case. That might be a bug in .delegate(), I'm not sure.
Returning false from the <a>'s click handler will prevent the other handler from running, but will also do a .preventDefault(), so the browser will not navigate. Basically, I'm wondering what return false; does that e.stopImmediatePropagation(); e.preventDefault(); does not. Based on the docs, they should be equivalent.
For a live demo of the above code, here's a JSFiddle: https://jsfiddle.net/CHn8x/
event.stopImmediatePropagation() is indeed what you're after, but remember that order matters here since .delegate() listens at the same level, so you need to reverse your bindings, like this:
$(".top").delegate(".link", "click", function(event) {
event.stopImmediatePropagation();
});
$(".top").delegate(".middle", "click", function(event) {
if(!event.isPropagationStopped())
alert("failure");
});
Here's a working version of your demo with this change
The order you bound the handlers is the order they will execute, so you need that .link handler to execute and stop the propagation before the other handler runs, checking it with event.isPropagationStopped() or event.isImmediatePropagationStopped().
This normally isn't an issue at different levels, but since .delegate() is listening on the same element, it does matter.

Categories