jQuery ".triggerHandler()" vs. ".trigger()" when multiple elements are selected - javascript

The jQuery ".triggerHandler()" mechanism, unlike ".trigger()", only operates on the first element referenced by the jQuery object for which it's called. In other words,
$('.all-over-the-page').triggerHandler("readjust");
will only call the "readjust" handler for the first element with class "all-over-the-page", even if there are many elements on the page with that class. The ".trigger()" method, on the other hand, would affect all of them.
I realize that I can use ".each()" to get around this (or simply write my own substitute that does that for me), but is there some rationale for why the two are different in this respect? It kind-of makes no sense to me. (I understand of course that it almost certainly can't be changed now.)
edit to clarify:
It's probably easier to understand why I'm scratching my head over this if I provide a context in the style of code I've actually got. When I put together code for various "widget" features on a page, that often involves event handlers. A good example is a form of some sort that's got some fields whose relevance is controlled by a checkbox, or radio button, or selector. A common instance of that is the "Shipping Address" checkbox that shows up on a zillion e-commerce sites: if the checkbox is checked, the shipping address is disabled and the billing address is used.
Now consider that some other code may, for its own reasons that are totally independent of the checkbox-control widget, actually do things to the form that may include updating checkbox settings programmatically. In that case, that other widget code may want to use "triggerHandler()" to tell any widgets, "hey I've updated some stuff, so you might want to re-check the current status and adjust if necessary."
Thus, if ".triggerHandler()" would operate on all selected elements, I could use:
$theForm.find('input, select, textarea').triggerHandler('change');
and all those handlers could run and do whatever they need. As I said, it's easy enough to write:
$theForm.find('input, select, textarea').each(function() {
$(this).triggerHandler('change');
});

"...is there some rationale for why the two are different in this respect?"
I think the idea is that triggerHandler() is meant to be a way of invoking the function you as a handler as though it was any other function.
As such, they made triggerHandler() so that the function is only invoked once, it returns the actual return value of the function, and it doesn't affect the DOM with bubbling or default behaviors.
Of course the function may break if they changed the this value to something other than a DOM element, so they just use the first element matched.
If you're wanting to simply use your function, then I'd probably just keep a reference to it and invoke it directly, or as the argument to .each().
$('.element').each( handler_func );
...as long as you don't need the event object.
EDIT: Or if you want the values returned from the invocation, use .map() instead:
var return_values = $('.element').map( handler_func );
EDIT: With respect to the example provided in the updated question, a good solution may be to take advantage of the extraParameters capability of the trigger()[docs] method so that you can tell the handler to preventDefault() and stopPropagation().
$('.elememts').bind( 'click', function( e, was_code_triggered ) {
if( was_code_triggered ) {
e.preventDefault();
e.stopPropagation();
}
// your code
});
// ...
$('.elememts').trigger( 'click', true ); // pass "true" to let handler know
// it wasn't a DOM event
From the .trigger() docs:
"Note the difference between the extra parameters we're passing here and the eventData parameter to the .bind() method. Both are mechanisms for passing information to an event handler, but the extraParameters argument to .trigger() allows information to be determined at the time the event is triggered, while the eventData argument to .bind() requires the information to be already computed at the time the handler is bound."

Related

using $(this) to get data attribute doesnt return the actual result

Im new to javascript/jquery, I've been searching all over the web but haven't got a satisfying answer. (I will delete it if someone can point out a similar question)
In the hmtl I have
Submit
In the console, I tried this
$('.btn-place-order').data("confirm-modal")
--> it returned "myModal"
But when I tried
$(".btn-place-order").on("click", function(e){ $(this).data("confirm-modal"); });
--> it return the whole object [a.btn-place-order]
Why ?
This behavior is exactly correct. If you take a look at the jQuery documentation you will see:
jQuery on:
.on( events [, selector ] [, data ], handler(eventObject) )
Returns: jQuery
jQuery data:
.data( key )
Returns: Object
This means that when you call var myObject = $('.btn-place-order').data("confirm-modal"); will contain the value of the data- attribute.
However, when you call $(".btn-place-order").on("click", function(e){ $(this).data("confirm-modal"); }); you get a jQuery object returned. This jQuery object is the same one that $(".btn-place-order") already returns, which is very important to make jQuery's concept of chaining work.
Chaining allows you to execute several methods in order, without getting the original jQuery object over and over. For example $(".btn-place-order").on('click',...).on('hover',...); would allow you to attach two handlers (a click and a hover) to the same element.
It also wouldn't make sense for on to return anything else, since it just attaches a handler to an element. It really doesn't give you any value just because you attach an event handler.
Now, if you want to take any action when the event is fired, you will need to take that action inside of the handler's callback function. E.g.
$(".btn-place-order").on("click", function(e){ alert($(this).data("confirm-modal");) });
will alert the user of the data-confirm-modal attribute value of the element that was clicked on. However, without the alert() part (i.e. the way your original code was written), the value is just read, but nothing is ever done with it.

jQuery attr method can set click handler and inner text

Reading jQueryUI dialog code, I've found out, jQuery .attr() method has some undocumented behavior:
<button id="btn1">1</button>
<button id="btn2">2</button>
$(function() {
var props = {
text: 'Click it!',
click: function () {
console.log('Clicked btn:', this);
}
};
$('#btn1').attr(props, true); // Changes #btn1 inner text to 'Click it!'
// and adds click handler
$('#btn2').attr(props); // Leaves #btn2 inner text as it is and fires
// click function on document ready
});
Can you explain me how it works? Why should I set true as the second argument after
map of attribute-value pairs?
Can I use this feature in my projects safely?
I'm guessing slightly here because I'm unfamiliar with the jQuery source. jQuery.attr calls jQuery.access, and the comment above the jQuery.access function reads:
// Multifunctional method to get and set values of a collection
// The value/s can optionally be executed if it's a function
Upon further investigation, the text() function also calls jQuery.access:
attr: function( name, value ) {
return jQuery.access( this, jQuery.attr, name, value, arguments.length > 1 );
},
.........
text: function( value ) {
return jQuery.access( this, function( value ) {
......
},
You're using attr to set text and event handlers, which is not what attr is for. However they all seem to be using the same function to get the job done, so the use of undocumented parameters is just incidentally giving you the expected behavior.
I would consider it unreliable to rely on undocumented behavior to accomplish what you're trying to do here, as any future upgrade of jQuery could break your code.
Looking at the jQuery 1.8.2 code, the true parameter eventually arrives in the variable pass at a line that says:
exec = pass === undefined && jQuery.isFunction( value );
which if set, tells jQuery to check the check the value belonging to the key, and if it's a function, call it immediately. Hence click: function(...) will call that function, not register that function.
This appears to be how the .method(function() { ... } versions of various jQuery functions work, i.e. those where instead of passing a specific value for a property, you pass a function which is itself passed the original value, and whose return value is assigned to the relevant property.
Can I use this feature in my projects safely?
I wouldn't, not until it's documented.
This is an interesting one. I can't pretend to tell you /why/ it works, this way, and I think there even might be reason to submit this as a bug report to jQuery, because the behavior that they document is not coming out as I'd expect.
On this page, the following quote appears toward the bottom:
http://api.jquery.com/attr/
"Note: If nothing is returned in the setter function (ie. function(index, attr){}), or if undefined is returned, the current value is not changed. This is useful for selectively setting values only when certain criteria are met."
This led me to do some playing around on jsFiddle: http://jsfiddle.net/mori57/GvLcE/
Note that, contrary to their documentation, Cases 8 and 9 return either null or nothing. Looking at what Alnitak mentioned, it seems to makes sense, as their test /actually/ is only validating that it /is a function/ not what that function returns (.isFunction should only ever return true/false, which is different from saying that the value returned is true/false).
In the end, however, I'd agree with both Alnitak and bcoughlan that this is not functionality you should rely on, though I'd add that, in addition to it being unstable because it may be changed in future releases of jQuery, it is also bad practice to rely on hacks that are reliant on undocumented features because future developers of the code you write today (and that includes you, in 2-4 months!) could very easily forget that is there, or why it's set that way. Far better to be explicit, and use functionality as documented, so that you're clear to yourself and others what your code is intended to do.

What happens if a JavaScript event-listener is called and target element is missing?

For the moment, we're loading site-wide event-listeners from a single common.js file for a Rails project. We're aware of (most of) the trade-offs involved there, and are just trying to mitigate them. Once our basic architecture takes hold, we may move them off to separate files by controller or by view.
For the moment, the quick question is how we can activate them only when necessary, which begs the mangled, pseudo-zen question:
if an event-listener is declared in a forest when nobody is around to hear it, does it still make a sound?
In other words, if one declares a basic listener (i.e., nothing persistent like .live() or .delegate()) in the JavaScript for a given page, and the target element is not actually present on that given page, does anything really happen, other than the few cycles devoted to evaluating it and checking the DOM for the element? Is it active in memory, looking for that element? Something else? It never seems to throw an error, which is interesting, given that in other contexts a call like that would generate a null/nil/invalid type of error.
For instance:
$(document).ready(function () {
$('#element').bind('blur keyup', function);
}
Assume that #element isn't present. Does anything really happen? Moreover is it any better to wrap it in a pre-filter like:
$(document).ready(function () {
if ($('#element')) {
$('#element').bind('blur keyup', function);
}
Or, are the .js interpreters in the browsers smart enough to simply ignore a basic listener declared on an element that's not present at $(document).ready? Should we just declare the initial, simple form above, and leave it at that, or will checking for the element first somehow save us a few precious resources and/or avoid some hidden errors we're not seeing? Or is there another angle I'm missing?
JQuery was designed to work with 0+ selected elements.
If no elements were selected, nothing will happen.
Note that you will never get null when using jQuery selector. For example:
$('#IDontExist') // != null
$('#IDontExist').length === 0 // true (it's ajQuery object with
// zero selected elements).
The docs says:
If no elements match the provided selector, the new jQuery object is "empty"; that is, it contains no elements and has .length property of 0.
$('#element') if results into empty set then jQuery will not do anything.
Since jQuery always returns an object we can can call the methods on an empty set also but internally it will do the checking before applying it's logic.
Even if you want to check if the element exists before attaching the event handler you can use length property of jQuery object.
if ($('#element').length > 0) {
$('#element').bind('blur keyup', function);
}

Where are attached event-handlers 'meta-data' stored? On the "DOM," object, or...?

I've always wondered... so you have a code like this:
$('#click-me');
and you attach it with this:
$('#click-me').click(someFunction);
where is the 'meta-data' that says:
"Hey "jQuery-object #click-me," I will point you to 'someFunction' when you are clicked!"
I know that event-handlers can get destroyed such as my situation with Backbone.js where my events stopped firing due to me re-rendering the entire page, destroying some background functions/objects/Views along the way.. (this is the context as to why I'm asking this question)
NOW, MY QUESTION IS:
where are events 'meta-data' stored and how are they destroyed?
Are they stored within the function that bound it to a function? Are they within the DOM 'meta-data' (if there is one) itself?
I'm trying to learn the intricacies of JavaScript because I'm tired of bugs. In addition to that, I'm wondering if I should watch out for garbage collection that might detach my events and such. Coming from C#, I would say JavaScript with the DOM is really something...
(also, as a side note, how can I access these events and 'debug' them? firefox? chrome?)
UPDATE
To say it in different words, where is the information that connects a DOM element to a certain event stored? DOM? Objects? (or.. does jQuery map it? does JavaScript have a 'meta-data'? it's around that context..
Update : So I misunderstood the question, you wanted to know how events are bound in the context of just javascript and html. My original answer below describes how jquery creates and manages events. It boils down to a call to element.addEventListener.
From the MDN docs you see the eventtarget can be an element, the document, window or an XMLHttpRequest. From the w3 specifications on DOM Events an event target adds, removes and dispatches an event. So even information is probably stored in whatever is encapsulating things like elements, this will be implemented at the browser level.
From the issue you mentioned about copying and then replacing the html from the body erases the events, I'm thinking the browser just gives you the markup (without the event metadata) and then when you replace it, the metadata is gone. ( http://jsfiddle.net/qu9bF/1/)
Original answer: How jquery event handlers work.
Ok so I started digging this, for JQuery 1.4.2 (because I had to use a couple tools, all of which aren't updated)
Take a look first at this:
http://james.padolsey.com/jquery/#v=1.4.2&fn=click
function (fn) {
return fn ? this.bind(name, fn) : this.trigger(name);
}
That is how click is defined, it isn't actually defined in code. JQuery defines this function for all events/handler functions like below, yes! they are created/defined dynamically :
jQuery.each( ("blur focus focusin focusout load resize scroll unload click
dblclick " +
"mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " +
"change select submit keydown keypress keyup error").split(" "),
function( i, name ) {
// the magic happens here..
//each string is added as a function to the prototype
jQuery.fn[ name ] = function( fn ) {
return fn ? this.bind( name, fn ) : this.trigger( name );
};//if a handler is NOT specified then attach an event OR call/trigger it
if ( jQuery.attrFn ) {
jQuery.attrFn[ name ] = true;
}
});
From here on we need to look at bind, now bind() and one() are also defined like this. Search for "Code : bind and one events" here
From here I used chrome with this fiddle http://jsfiddle.net/qu9bF/ to step into the code. The block from c.each(["bind" is how the bind function is defined. The source is minified, but chrome can format it.
From here on the code calls JQuery.events.add, you can find this under the Events section here. This is not the add() that is documented I think
Toward the bottom, this piece of code is what does the magic. It accordingly calls element.addEventListener or attachEvent. See how it adds the on for attachEvent.
// Check for a special event handler
// Only use addEventListener/attachEvent if the special
// events handler returns false
if ( !special.setup ||
special.setup.call( elem, data, namespaces, eventHandle ) === false ) {
// Bind the global event handler to the element
if ( elem.addEventListener ) {
elem.addEventListener( type, eventHandle, false );
} else if ( elem.attachEvent ) {
elem.attachEvent( "on" + type, eventHandle );
}
}
And there you have it! :) I hope it answered both your questions. You can link up to the non-minified versions of jquery source and step through it to figure things out. IMO sometimes IE9's debugger is more intuitive (that's the only thing I use it for), and use the pages I've mentioned to browse through the source sanely.
jQuery stores all the event binding and data cache on the jQuery.cache object. All the DOM nodes which were wrapped with jQuery and had events bound to them or data set will get automatically cleared when you are using jQuery html, empty, remove, replace etc.
That's why it's very important to never use innerHTML or other native DOM methods to insert/replace content that was altered before by jQuery. It will lead to leaks that you won't be able to cleanup unless you reset the jQuery.cache object manually.
There is also an undocumented method jQuery.cleanData which takes a collection of DOM nodes as an argument and it iterates over them and cleans up all their event bindings, data and removes references to these elements from the cache. This one can be useful if you have DOM fragments which were detached from the main DOM tree and there is a risk that they won't get cleaned up properly.
Regular events like click or submit (when not bound by jQuery) are actually just properties ('onclick', 'onsubmit') of the DOM elements themselves.
For jQuery events, the library keeps it's own record when you bind them and looks at it when you trigger them. jQuery puts all data about elements in a standard place, which you can access with $(e).data(). For events, it's just $(e).data('events').
You can unbind jQuery events with $().unbind(), and regular events using the delete keyword to delete the object's property which corresponds to the given event.
jQuery keeps its own map of element event handlers. There's rarely, very rarely, cause to worry about this unless you're abusing the library somehow.

JS Prototype: Avoiding an event handler being bound several times to the same object

So with the new ajax things we have to reinitialize our Javascript event handlers every time an ajax call is made, since an ajax call can result in pretty heavy redrawing of the whole page resulting in uninitialized objects.
Have a look at this jsfiddle:
Javascript eventhandler added multiple times to the same object
This is what I have and it seems to work, but since it is going to be used with everything we have: I wanna make sure that it is the right solution.
E.g. the global defined variable
MyCompany.field.bindedOnfocusSelector = MyCompany.field._focusEventHandler.bindAsEventListener(MyCompany.field);
just feels wrong. And it lacks the possibility to hand more function arguments.
As another poster suggested the prototype $(smth).on(event) I have problems to get it working - I remember problems crossbrowser wise (e.g. on IE 8 things didn't work which worked in Firefox) and even in this simpler example jsFiddle problem with on('focus'):
How about you register an ajax responder, and add the methods after a request has completed
Ajax.Responders.register({
onComplete: function(transport) {
MyCompany.field._initTextInputFields();
}
});
UPDATE
Ok, taking into consideration your comment, how about observing the whole page i.e. body and determining if a input event occurred, ex:
$("#body").on("focus", "input[type=text]:not([readonly])", function(event, element) {
// ....
});
I think this will help you as you only add one observer, and never need to remove it, all your logic can be contained.
PS: note that Event.on is only available in prototype 1.7
UPDATE
ok, what if you just check the click, keyboard won't work now though but i think this is a viable solution
Updated Fiddle

Categories

Resources