Ok, in my code I have for example, this:
$('.follow').click(function(){
var myself = $(this);
var data = {
id: this.getAttribute('data-id')
}
$.post('/users/setFriend', data, function(msg){
myself.text(msg);
myself.attr('data-status-friends', (myself.attr('data-status-friends').toLowerCase() == 'follow') ? 'following' : 'follow');
});
})
However, i put a class of 'auth' on certain elements that if the user is logged out, run this bit of JS:
$('.auth').click(function(e){
e.preventDefault();
alert('not logged in');
});
This works for the majority of elements, but with the above POST, it seems to still action the POST. How can I definitively cancel the events fired by other bits of code if .auth is clicked?
I don't think we should talk about propagation or defaultAction preventing here. The point is that you create two different series of event handlers: one attached to the .follow elements, another - to the .auth elements. Of course, if an element has two classes, clicking it will trigger both handlers automatically - and they both will be attached to this element (hence no propagation).
The most simple solution here, I think, is to remove click handler from an element when you assign .auth class to it.
Or, alternatively, you can check $(this).hasClass('auth') condition within the .follow handler function - and return false immediately if that's the case.
Rather than stopping the events, I think a better approach would be either to simply not put events into elements which aren't supposed to be doable without logging in, or track the auth state in your JS code with a variable and check that before doing actions. The latter would probably be a better approach to use if you are building a client-side MVC style application.
You could have two issues going on here with different sets of solutions.
The listeners are attached to different elements in reverse of what they should be
The listeners are being attached to the same element
Different Elements
Your handlers are out of order, swap them so that e.stopPropogation() is on the inner(child) element and the $.post() call is on the outter(parent) element.
Same Element
If the listeners are on the same element, neither e.stopPropogation() nor e.preventDefault() will do what you wish as the event listeners will still fire on the same element.
stopPropogation()
Description: Prevents the event from bubbling up the DOM tree,
preventing any parent handlers from being notified of the event.
Propagation according to the DOM Level 2 Spec will still execute all listeners on the same element but not events attached to the parent:
If the capturing EventListener wishes to prevent further processing of
the event from occurring it may call the stopProgagation method of the
Event interface. This will prevent further dispatch of the event,
although additional EventListeners registered at the same hierarchy
level will still receive the event.
preventDefault()
Description: If this method is called, the default action of the event
will not be triggered.
Default actions are are based on which element is being acted upon (W3C). For a link this would be the redirect to the href="" value, input element to focus it, etc. This is not what you desire as you are most likely not the 'default behavior'.
One option is to attach the handler that calls $.post to an element that is higher on the DOM.
$('.follow').parent().click(function(e){e.stopPropogation()})
You might have to alter your target HTML so that you have an inner(child) and outter(parent) element to attach your events to. The goal being to have the $.post handler as the outer(parent) and the inner(child) handler cancel the event.
Another option is to add a check to see if the other class is present on your element.
$('.follow').click(function(){
var myself = $(this);
if(!$(this).hasClass('.auth')){
var data = {
id: this.getAttribute('data-id')
}
$.post('/users/setFriend', data, function(msg){
myself.text(msg);
myself.attr('data-status-friends', (myself.attr('data-status- friends').toLowerCase() == 'follow') ? 'following' : 'follow');
});
}
});
Related
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
I want to be able to capture all events that are both created and dispatched and fire some callback when that happens.
Also, I would like to be able to fire a callback anytime an event is paired with an event listener.
Problems include: dynamically added elements, events whose propagation or bubbling is prevented, and custom events that are generated dynamically. I imagine there would need to be a prototyping of dispatchEvent or something, but I am unsure. Is this even possible?
Some event basics:
Events are dispatched "on" a DOM object (usually an element) that is the event target.
Events can firstly propagate down to child elements in a capture phase. This phase is rarely used since it wasn't supported by some widely used browsers until recently.
Events can secondly propagate up to parent elements in a bubbling phase. This phase is commonly used.
Some events don't propagate, they have neither a capture or bubble phase (e.g. focus, blur and submit events). Some events that propagate in some browsers don't propagate in others.
DOM elements that respond to events have an event handler. It can be set to listen for particular events and call a listener function when that event reaches the element, either during capture, bubbling or if the element is an event target.
Listeners can cancel propagation, e.g. a click event on a span inside a link can cancel propagation so the link doesn't get the click
Given the above, it is a practical impossibility to "capture all events" using the Events API. It would require establishing a listener for every event type on every element and be impossible to capture custom events because you have to know about them to set an appropriate listener.
I imagine there would need to be a prototyping of dispatchEvent or something
dispatchEvent is a method of an Event instance, it's not specified to be a constructor (there is no requirement for it to have an internal [[Construct]] method) so not practical to use as such. Browsers aren't required to implement prototype inheritance for host objects (though most do), and the implementation details of host objects and methods are largely hidden, so this is not an option.
You might try extending the Event API, but you really should not mess with host objects.
It seems that you are concerned about dynamically added elements. There is a strategy called "event delegation", where you work out the events you need to listen for, then setup listeners as close to the event targets as you can on an element that doesn't change (e.g. a table element if you are dynamically adding and removing table rows, or a container div for other elements) for the specific event types you need to respond to.
You can also have the functions that are modifying the DOM dispatch custom events to add listeners or whatever.
If you really want to do this, then you can override addEventListener to keep track of events being registered and fired.
var myEventManager = (function() {
var old = EventTarget.prototype.addEventListener,
listeners = [],
events = [];
EventTarget.prototype.addEventListener = function(type, listener) {
function new_listener(listener) {
return function(e) {
events.push(e); // remember event
return listener.call(this, e); // call original listener
};
}
listeners.push([type, listener]); // remember call
return old.call(this, type, new_listener(listener)); // call original
};
return {
get_events: function() { return events; },
get_listeners: function() {return listeners; }
};
}());
However, there are uncountable reasons not to do this, not least the fact that you will quickly run out of memory as you record thousands of events such as mouse moves. This will also not capture event listeners set in ways such as elt.onclick. Nor of course will it catch listeners set up via the old IE attachEvent API. Most importantly, it will not help with you that events that are generated and listened for internally, such as a mouse click on a check box. (A complete solution would also require handling removeEventListener.)
You can also override createEvent and dispatch in similar fashion, but again, that will capture only events that are explicitly created or dispatched in the JS code.
If you really want to do what you seem to be wanting to, I guess you need to fork Chrome.
I need to click on a document to call some function, but the problem is that when I click on some element that want it doesnt react, so the code:
<body>
<div class="some_element">
some element
</div>
</body>
and js:
$(document).click(function(){
//something to happen
})
and now if I click on the div with class="some_element" the document.click event will be called, but I need to call that event ONLY when I click on the document; or it is possible the make this element an exception?
More detailed:
$('#forma').click(function(e){
e.stopPropagation();
$('#assignment_type_list').slideUp();
})
Lets say #forma - its a parent element of those element, so when I click on the page I want to slideUp someElement and:
$('#assignment_type_select, #assignment_type_label').click(function(){
$('#assignment_type_list').slideToggle();
})
this is the elements when they are clicked the other element is toggled, but the problem is that when I click on this elements the $('#forma').click - also executes, because its parent and the e.stopPropagation() - doesn't help.
All this stopPropagation stuff is right, though this'll cause your script to throw errors on older versions of a certain browser. Guess which one? a cross-browser way:
$('#elem').click(function(e)
{
e = e || window.event;//IE doesn't pass the event object as standard to the handler
//event would normally work, but if you declared some event variable in the current scope
//all falls to pieces, so this e || window.event; thing is to be preferred (IMO)
if (e.stopPropagation)//check if method exists
{
e.stopPropagation();
return;
}
e.cancelBubble = true;//for IE
});
However, you wanted to check if the element that was actually clicked, is the one you need. The problem with that is, that the way the event is passed through the DOM. In W3C browsers the event is first passed to the document, and then clambers down to the element that was actually clicked (propagates through the dom). By contrast IE dispatches its events on the element itself, and then sends it up to the document (except for the change event triggered by select elements... to add insult to injury). What this effectively means is that a click event that is registered in to body element in W3C browsers might be on its way to a checkbox of sorts, or it could be a click inside an empty div. Again, in IE, when a click event reaches the body tag, it could have been dispatched too any element on the page. So it may prove useful in your case to google: event delegation, or turn to jQuery's .delegate() method.
Or check the event object to see if the event is allowed to propagate through or not:
var target = e.target || e.srcElement;//target now holds a reference to the clicked element
The property names neatly show the difference between the bubbling model and the propagating one: in the first case (srcElement), the event is coming from a source element in the dom. In the W3C propagating model, the event is cought while it's headed for a target element somewhere in the dom. Look at it like a heat-seeking missile (w3c) versus a shower of debris after the target was shot down (IE, always the destructive one, and in this case often to late to respond to the events, and therefore to late to handle them:P)
One way to do it is to check for the event's target.
$('html').click(function(event){
if (event.target != this){
}else{
//do stuff
}
});
Here's a working fiddle
Elements on the document are part of the document, so if you click "some_element" in the document, it is obvious that event registered on document will be fired/triggered. If you dont want to execute code which was for "document" then first get the element OR "event source" which originates this event, and check if it was "some_element" in your question above.
As Jquery Mobile keeps some pages in the DOM when navigating around, a page may be visited multiple times when going back and forth.
If I'm binding to a page like below and inside this binding perform all my page logic, which includes "nested element bindings":
// listener for the page to show:
$(document).on('pagebeforeshow.register', '#register', function() {
// stuff
// page event bindings:
$(document).on('click.register', '.registerSubmitter', function(e) {
// do something
});
});
Going back and forth causes my nested binding to be attached multiple times.
Right now trying to work around this like so (doesn't work...):
$(document).on('click', '.registrySubmitter', function(e) {
if ( $(this).attr('val') != true ) {
$(this).attr('val') == true;
// do something
}
});
So I'm only allowing the first binding to pass and then I block every other binding attempt that comes along.
While this works, it's far from optimal.
Question:
How and when should event bindings be properly unbound/offed? Is there a general way (kill all) or do I have to do this binding per binding? Maybe more importantly: Is it better performance-wise to do a binding once and keep it or bind/unbind when the user comes to/leaves the page?
Thanks for input!
EDIT:
So I'm namespacing all my events and then listen for pageHide like so:
$(document).on('pagehide.register', '#register', function(){
$(document).off('.registryEvents');
});
While this seems to unbind, it also fires when ever I close a custom dialog/selectmenu on the page, so I'm loosing my bindings before leaving the page. So partial answer, I should use off(), but how to bind to the page really being left vs. opening and closing a select menu?
When you use .on() like that, you are delegating the event handling to the document element, meaning you can setup that delegated event binding anytime you want because the document element is always available.
I've got two suggestions:
Use the pageinit or pagecreate event to only run the page-specific bindings when pages are added to the DOM and initialized. Using this method I would not delegate the event bindings within the pageinit or pagecreate event handlers because when they fire, all the elements on the pseudo-page are in the DOM:
.
$(document).on('pageinit', '#register', function() {
//note that `this` refers to the `#register` element
$(this).find('.registerSubmitter').on('click', function(e) {
// do something
});
});
Bind the delegated event handlers once and don't worry about when pages are actually in the DOM:
.
//this can be run in the global scope
$(document).on('click.register', '.registerSubmitter', function(e) {
// do something
});
Basically when you bind an event using delegation like you are, the actual CPU hit of adding the event handler is less but each time an event is dispatched (of any kind that bubbles) it has to be checked if it matches the delegated event handler's selector.
When you bind directly to elements it generally takes more time to do the actual binding because each individual element has to be bound to rather than binding once to the document element like with event delegation. This however has the benefit that no code runs unless a specific element receives a specific event.
A quick blurb from the documentation:
Triggered on the page being initialized, after initialization occurs.
We recommend binding to this event instead of DOM ready() because this
will work regardless of whether the page is loaded directly or if the
content is pulled into another page as part of the Ajax navigation
system.
Source: http://jquerymobile.com/demos/1.1.0/docs/api/events.html
Assume I get a table element with ID="emTab", how do I call JS to click it?
Thanks.
document.getElementById("emTab").onclick = function() {
// your code goes here
};
See element.onclick
To trigger click event
document.getElementById("emTab").click();
See element.click
The click method is intended to be
used with INPUT elements of type
button, checkbox, radio, reset or
submit. Gecko does not implement the
click method on other elements that
might be expected to respond to
mouse–clicks such as links (A
elements), nor will it necessarily
fire the click event of other
elements.
Non–Gecko DOMs may behave differently.
When a click is used with elements
that support it (e.g. one of the INPUT
types listed above), it also fires the
element's click event which will
bubble up to elements higher up the
document tree (or event chain) and
fire their click events too. However,
bubbling of a click event will not
cause an A element to initiate
navigation as if a real mouse-click
had been received.
Cross browser way
If you can use jQuery then it would be
$("#emTab").trigger("click");
Firing events cross-browser - http://jehiah.cz/archive/firing-javascript-events-properly
its simple using JQuery
$('#emTab').click(functionToCall);
while in JS
document.getElementById('emTab').onclick = function() {};
for details on DOM events:
http://www.howtocreate.co.uk/tutorials/javascript/domevents