Event Handling on multiple instances of the same class - need clarification - javascript

I need some guidance in understanding why these functions are doing what they are doing...
1.) On my web page, I have three different panels that utilize a Slider function, which creates an unordered list that has slider functionality using next and previous anchor links. See the code below:
function Slider(id) {
var _this = this;
this.id = id;
if (!id) return false;
this.index = 0;
this.slider = $(this.id);
this.length = this.slider.children().length;
this.width = $(this.id).outerWidth();
this.totalWidth = this.length * this.width;
$(id).addClass('slideWrapper').wrap('<div class="slideViewport">').after('<div class="sliderNav">PreviousNext</div>').css({
'width': this.totalWidth
}).children().addClass('slide').css({
'width': this.width
});
$('.slideViewport a.next').click(function(e) {
e.preventDefault();
return _this.next();
});
$('.slideViewport a.prev').on(function(e) {
e.preventDefault();
return _this.prev();
});
}
If I try to run more than one of these Slider instances on a page, clicking a .next anchor will cause the clicked element and any of the elements below it to more to the next list element in their slideshows. Going to the second panel would cause all but the first to run, the third panel causes all but the first and second to run, etc. I would have expected the handler to only run for the event that I clicked on, rather than all instances of the class after it, since I am using this in my event. Any explanation as to what is going on here would be immensely helpful.
2.) Now, I've been trying to make it such that all of the Slider events DO run next() when I click on any a.next anchor on the page, rather than just run an event for the one whose anchor I have clicked. I have figured out that this code works:
$('.slideshow').on("click", "a.next", function(e) {
e.preventDefault();
return _this.prev();
});
But truth me told, I'm not really sure why this is working. My understanding is that JQuery is only looking to see if the a.next anchor is clicked, and will then pass the handling function to the $('.slideshow') selector, which makes me assume that it is selecting all instances of $('slideshow') and running the .next() function for all of them. Is that the right way to think about it?
3.) Why does the following snippet of code cause all of the slideshows to run the next() function twice, as opposed to once? I don't really like that this isn't returning anything, so I don't really want to use this particular bit of code, but I'd just like to understand it a little bit better...
$('.slideViewport a.next').on("click", function(e) {
e.preventDefault();
$('.slideshow').each(function() {
_this.prev();
}
});
Help understanding any of this code would be much appreciated. I would really like to have a better understanding of what is going on in the DOM in terms of propagation in this scenario, but everything I've tried to read has just made me feel more confused. Thanks!

This bit of code attaches a click handler to all .slideshow elements in the document.
$('.slideshow').click(function(e) {
e.preventDefault();
return _this.prev();
});
What you might have wanted was to attach the handler only to the .slideshow elements that are descendants of the slider:
this.slider.find('.slideshow').click(function(e) {
// ... handle event here ...
});
Now, about your on() statement:
$('.slideshow a.next').on("click", function(e) {
e.preventDefault();
$('.slideshow').each(function() {
_this.prev();
}
});
What you've done here is bind a click event handler to all the .slideshow a.next elements, and what the handler does is run _prev() on all of them. But you already have another handler bound to the .slideshow element from when you called $('.slideshow').click(). When the "on" handler is finished, the event continues to propagate up the DOM tree, triggering all of the click handlers on its way up, including the one you bound to the .slideshow element. And of course that handler also calls _prev().
If you want to stop event propagation, you have two choices.
call e.stopPropagation()
return false from your event handler (this tells jQuery to stop propagation and prevent the default action).
You may ask yourself, "what is the difference between click() and on('click', ...). The on() method lets you use Event Delegation. Basically, that means using a single event handler attached to one DOM node to handle events on a lot of descendant elements.
As an example, imagine you have a div with some arbitrary number of images in it, and you need to do something whenever an image is clicked. You can either (a) bind a click event handler to each image or (b) bind a handler to the div that will handle all the click events for all the images as those events bubble up the DOM tree.
$('#imageDiv').on('click', 'img', function(evt) {
// ... "this" is the image that was clicked ...
});
Delegation has the added benefit that you can add and remove images from the container, and the delegate will continue to work.

Related

Method for triggering certain click event using .trigger() but suppressing all others

My situation is that I am trying to trigger a single event using the jQuery .trigger() method. However the element I am triggering has multiple click event listeners.
Actually finding what these listeners are and what they trigger from the source code is probably not viable as its included in the sites main JS file and its all minified and pretty much unreadable.
At the moment I know that the element when clicked performs some kind of ajax call and loads more data into the DOM of the page (which is what i want to trigger), however it also displays an overlay (which is what I want to suppress temporarily).
As its just an overlay there are workaround I can make; using a display:none on it straight after click etc. However it would be much more elegant if i could somehow suppress all click events on this element except the desired event.
Any ideas if this is actually possible? And if so how I would go about it?
You need to register your own event at the top of the event chain. And cancel the event chain in your event. Here is a solution with writing a custom jquery extention.
$.fn.bindFirst = function (which, handler) {
var $elm = $(this);
$elm.unbind(which, handler);
$elm.bind(which, handler);
var events = $._data($elm[0]).events;
var registered = events[which];
registered.unshift(registered.pop());
events[which] = registered;
}
$("#elm").bindFirst("click", function(e) {
// edit: seems like preventing event does not work
// But your event triggers first anyway.
e.preventDefault();
e.stopPropagation();
});
Reference:
https://gist.github.com/infostreams/6540654
EDIT:
https://jsfiddle.net/8nb9obc0/2/
I made a jsFiddle and it seems like event preventing does not work in this example. There might be another solution.

Not calling click event

I have kind of strange problem.
I'm trying to add a couple of events to som DOM elements (all existing, some initially hidden:
$self.on("focus", function () {
$self.next().css("display", "inline-block");
});
$self.on("blur", function () {
$(this).next().hide();
});
$self.parent().find(".icon-ok").on("click", function() {
console.log("icon.ok")
});
You can see the relevant part of the DOM here (self is the span user-name):
Later on, the element eventually because visible and I can click on it. However, the event handler is never called. If I remove the blur event, than the click event works. However, I need both.
What's going on here?
How can I fix it?
Looks like the blur cancels out the click (due to event order) but using mousedown instead of blur may help you get both.
UPDATE: Added code based on comment
$self.parent().find(".icon-ok").on("mousedown", function() {
console.log("icon.ok")
});
Your problem might be the classic delegation problem, where in the element is not available in the DOM when the event is bound.
Delegate the event and see if that solves your problem.
$self.on("click", ".icon-ok", function() {
console.log("icon.ok")
});
User $self if that element is visible or any closest ancestor that you can find which is always present in the DOM.

Best practices for where to add event listeners

On my page, the user clicks on an element in order to edit it. To facilitate this, I assign the class editable to all such elements.
How should I listen for clicks on all these elements? Currently, I'm doing this:
document.body.addEventListener("click", (event) => {
if (event.target.classList.contains("editable")) {
// do stuff
}
});
The alternative would be to set a listener on every element, like this:
const editables = document.getElementsByClassName("editable");
for (const editable of editables) {
editable.addEventListener("click", editElement);
}
It seems to me that the first way must be better for performance, since it's only one element being listened on, but is it possible to degrade performance by attaching all such events to the body element? Are there any other considerations (e.g. browser implementations of event handling) that I'm neglecting which would suggest doing it the second way?
Short answer: definitely do it the first way. Event delegation is way more performant, but requires extra conditionals in your code, so it's basically a complexity versus performance tradeoff.
Longer Answer: For a small number of elements, adding individual event handlers works fine. However, as you add more and more event handlers, the browser's performance begins to degrade. The reason is that listening for events is memory intensive.
However, in the DOM, events "bubble up" from the most specific target to the most general triggering any event handlers along the way. Here's an example:
<html>
<body>
<div>
<a>
<img>
</a>
</div>
</body>
</html>
If you clicked on the <img> tag, that click event would fire any event handlers in this order:
img
a
div
body
html
document object
Event delegation is the technique of listening to a parent (say <div>) for a bunch of event handlers instead of the specific element you care about (say <img>). The event object will have a target property which points to the specific dom element from which the event originated (in this case <img>).
Your code for event delegation might look something like this:
$(document).ready(function(){
$('<div>').on('click', function(e) {
// check if e.target is an img tag
// do whatever in response to the image being clicked
});
});
For more information checkout Dave Walsh's blog post on Event Delegation or duckduckgo "event delegation".
NOTE ON CODE SAMPLE IN OP: In the first example, target.hasClass('editable') means that the specific thing clicked on must have the class editable for the if block to execute. As one of the commenters pointed out, that's probably not what you want. You might want to try something along these lines instead:
$(document).on('click', function(e) {
if ($(e.target).parents(".editable").length) {
// Do whatever
}
});
Let's break that down a bit:
$(e.target) - anything that on the page that was clicked converted to jQuery
.parents(".editable") - find all the ancestors of the element clicked, then filter to only include ones with the class "editable"
.length - this should be an integer. If 0, this means there are no parents with "editable" class
Another plus point for the first method
I was using the second (alternative) method that you have mentioned I noticed that when the ajax loaded... the newly created elements were not listening the event. I had to redo the for loop after ajax every time.
With the first method which looks like following in my code also works with ajax.
document.addEventListener('click', function (e) {
if (hasClass(e.target, 'classname')) {
// do stuff
}
}, false);
So first one is better

Document click not in elements jQuery

Using jQuery how does one detect clicks not on specific elements, and perform an action consequently?
I have the following JavaScript
$('#master').click(function() {
$('#slave').toggle();
});
$(document).not('#master','#slave').click(function() {
$('#slave').hide();
});
and I cannot see where I am going wrong logically. You can see a live example here
Since you're binding to the click event on the document, you can use event.target to get the element that initiated the event:
$(document).click(function(event) {
if (!$(event.target).is("#master, #slave")) {
$("#slave").hide();
}
});
EDIT: As Mattias rightfully points out in his comment, the code above will fail to identify click events coming from descendants of #master and #slave (if there are any). In this situation, you can use closest() to check the event's target:
$(document).click(function(event) {
if (!$(event.target).closest("#master, #slave").length) {
$("#slave").hide();
}
});
Does this code do what you want? (not entirely sure if I understood correctly)
$('body').on('click', '*:not( #master, #slave )', function() {
$('#slave').hide();
});
http://jsfiddle.net/gZ4Hz/8/
Event delegation has long been supported natively by jQuery. The difficulty lies in creating the appropriate selector. Originally, delegate was used, but more recently the delegate form of on should be used.
The purpose of event delegation is to listen to events on child elements and invoke the bound event handlers on those elements as though they had been bound to the child element, instead of the parent. This means that instead of binding handlers to every element in the DOM, you're binding a handler to every element in the initial selection (document is a single element). This also makes for a simple way to use a single selector to bind to an ever changing set of elements, as new elements will propagate their events to document whether or not they existed when the initial event handler was bound:
$(document).on('click', '*:not(#master, #master *, #slave, #slave *)', function (e) {
//this will reference the clicked element
});
Additionally, note that I not only said the elements must not be #master or #slave, they must not be children of #master or #slave either.
Another thought, it may not be working because your browser may not be rendering body at 100% height; Try adjusting your base css to fix height of body and then a couple other thoughts.
e.stopPropagation(): Prevents the event from bubbling up the DOM tree, preventing any parent handlers from being notified of the event.
So if you change the first click code to the following:
$('#master').click(function(e) {
e.stopPropagation();
$('#slave').toggle();
});
Then you could change the call sign of the second too:
$("body, body *").not('#master, #slave').click(function(e) {
$('#slave').hide();
});
And that should cover it. Give it a try! or see this jsFiddle
Fredrik's answers works for elements already present in the document, but it didn't work for elements fetched by ajax calls.
I tried the following and it works for me. Sharing the code for future ajax coders.
$(document).on('click',function(event) {
if (!$(event.target).closest("#selector").length) {
if ($('#selector').is(":visible"))
$('#selector').slideUp();
}
});
Would have posted it as a comment but I don't have enough reputation for that.
$('.clickable-row').on("click",function(){
window.location = $(this).data('href');
return false;
});
$("td > a").on("click",function(e){
e.stopPropagation();
});
or
jQuery(document).ready(function($) {
$('.clickable-row').on("click",function(){
window.location = $(this).data('href');
return false;
});
$("td > a").on("click",function(e){
e.stopPropagation();
});
});

jQuery different binds on containing div and button inside

So I have a button inside a list row that is used to delete the row from the page (calls ajax stuff to delete the object represented by the row, but that's not important for my question). The whole row is bound to a click event which would redirect to another page.
In other words, the containing row is click bound and the inner button is click bound, which is causing me problems since clicking the inner button also triggers the containing row click event (as it should).
I've tried binding a hover event for all delete buttons that unbinds the row click on mouseover, and rebinds it on mouseout, like this pseudocode below:
$('.delete-button').hover(
function() {
$('.list-row').unbind();
$('.delete-button').bind('click', function() { /* delete action */ });
},
function() {
$('.delete-button').unbind();
$('.list-row').bind('click', function() { /* list row action */ });
}
);
This isn't working very well, and I'm convinced there is a better way to approach it. Should I take the button out of the containing list-row? It's way easier to have it in there since my list row contains custom attributes that have data I need for the ajax calls and I can just var rid = $('.delete-button).parent().attr('row-id'); to get the data, but I'm not opposed to change :)
Thanks!
In your click event handler for the button, you need to call e.stopPropagation(). This will prevent the event from bubbling up the DOM tree. More info here: http://api.jquery.com/event.stopPropagation/
edit: you already accepted (thanks!), but maybe this code snippet would help explain some of the concepts better:
$('.list-row').click(function() {
/* list row action */
});
$('.delete-button').click(function(e) {
// die, bubbles, die
e.stopPropagation();
// if you also need to prevent the default behavior for the button itself,
// uncomment the following line:
// e.preventDefault();
// note that if you are doing both e.stopPropagation() AND e.preventDefault()
// you should just `return false;` at the end of the handler (which is jQuery-
// sugar for doing both of these at once)
/* delete action */
})
There's a few ways of approaching this. As #jmar777 has already said you may attach an altered event to the click handler on the button, stopping propagation.
If you want to do this with the same function as you're applying to the div then you can approach it as such:
if($(event.target).is("input")) {
event.stopPropagation();
}
Another approach is to actually not bind the click event to the button, for any time the browser supports clicks on the containing element. As you will always trigger that, then you don't actually need the button to handle it too! This does require you to handle IE6 etc a little differently from everything else though...
Let your handler function return false

Categories