jQuery attr method can set click handler and inner text - javascript

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.

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.

Using a function to update a value within the data() method of jQuery sets the variable equivalent to the anonymous function, not the return value

I answered this question: Manipulate Custom Values with jQuery
With this jQuery:
$('img').attr('u', function(i,u) {
/* i is the index of the current image among all the images returned by the selector,
u is the current value of that attribute */
return u.slice(0, -1) + (parseInt(u.replace(/\D/g,''), 10) + 1);
});
Link to answer, JS Fiddle demo from that answer.
But then I felt I should show how to do it 'properly,' using the custom data-* attributes allowed under HTML5 (rather than the invalid, albeit functional, custom attributes), so I adapted the HTML to:
<img src="http://placekitten.com/400/500" class="className" click="" id='button4' data-u="button6" data-r="button5" data-l="ele1" data-d="" />
(No, I've got no idea what the click attribute's meant to do, or why it's there, so I left it alone.)
And tested the following jQuery:
$('img').data('u', function(i,u) {
/* i is the index of the current image among all the images returned by the selector,
u is the current value of that attribute */
return u.slice(0, -1) + (parseInt(u.replace(/\D/g,''), 10) + 1);
});
$('img').each(function(){
console.log($(this).data('u'));
});
JS Fiddle demo.
Now, with the data() method I realise that the attribute wouldn't be updated, which is why I used the console.log() to confirm the updated value, the output, however, is the anonymous function itself, not the value that I expected to be returned from that function. I realise this is unlikely to be a bug, and is probably the expected behaviour, but is there a way to use an anonymous function to update the attributes in the same way as, for example, that used within attr(), text(), etc..?
The difference between data and many of the other jquery functions (such as attr and many others) is that data can store any type of object. attr can only store string values. Because of this, it is completely valid to want to store a function using data.
If the jquery team were to make a similar signature for data, they would need to somehow distinguish between wanting to store the function and wanting to evaluate the function. It would likely get too confusing so they just did not include the ability to execute the function.
I think the best you can do is to use each.
Basically your expectation is wrong.
jQuery's .data does not modify the data attributes of the elements at all; it simply associates the data you provide with the element through a mechanism of its own choosing.
The implementation is intentionally left unspecified, and .data does not process this data at all; you put something in, and when you later ask for it that is exactly what you get back. The data is totally opaque from jQuery's perspective.
It's true that .data provides pre-population of an element's associated data from its HTML data- attributes as a convenience feature, but that is not its main mission. And of course the opaqueness of the data is still upheld in this case: when you ask for data, you get back exactly what was specified in the HTML.
If you need this capability, you can easily add it:
$.fn.fdata = function( name, callback ) {
return this.each( function( i, element ) {
var $element = $(element);
var data = callback( i, $element.data(name) );
$element.data( name, data );
});
};
Now you can use $(sel).fdata( name, callback ); and do what you want in the callback.
It may be tempting to extend the existing $().data() method to add the callback capability, but as other pointed out, this would break any other code that depends on being able to store a function reference as data.
Of course, it's also possible that merely adding this .fdata() method could break other code - if some other code on your page also tries to use the same method name in its own plugin. So it may be wiser to make this a simple function instead. The code is almost identical either way:
function updateData( selector, name, callback ) {
$(selector).each( function( i, element ) {
var $element = $(element);
var data = callback( i, $element.data(name) );
$element.data( name, data );
});
}

Is there a way to perform a branch of jQuery chaining only if a condition is true

I'm probably about a 7 or 8 on proficiency with jQuery (on a scale of 1-10), so I'm not sure if this even makes sense, but I'd like to know if anyone knows of a jQuery function or possibly a plugin which allows a branch of jQuery to only be executed if a given condition is true. Otherwise, I'd love to hear if someone thinks the concept is flawed in some way (EDIT and how it is flawed)
While one could control attachment of various events using normal JavaScript syntax similar to this:
var desiredElement = $('.parent') // find the parent element
.hover(overFunction,offFunction) // attach an event while I've got the parent in 'scope'
.find('.child-element'); // then find and return the child
if (booleanVar1) { // if one condition
desiredElement.click(clickFunction1); // attach one event
} else if (booleanVar2) { // or if a different condition
desiredElement.click(clickFunction2); // attach a different event
} else { // otherwise
desiredElement.click(clickFunction3); // attach a default event
}
$('.parent').find('.other-child') // (or $('.parent .other-child')
.css(SomePredefinedCssMapping)
.hide()
//...
I was wondering if there is a way to do it all in jQuery or if there is a good reason not to... something perhaps like this:
$('.parent') // find the parent element
.hover({overFunction,offFunction}) // attach an event while I've got the parent in 'scope'
.find('.child-element') // then find the child
.when(booleanVar1) // if one condition
.click(clickFunction1) // attach one event
.orWhen(booleanVar2) // or if a different condition
.click(clickFunction2) // attach a different event
.orElse() // otherwise
.click(clickFunction3) // attach a default event
.end()
.end()
.find('.other-child')
.css(SomePredefinedCssMapping)
//...
Note: I think this is syntactically correct, assuming the booleans and functions are defined appropriately, but I'm pretty sure I've gotten the intent across pretty clearly
the proposed jQuery seems a little neater to me (??) agree/disagree? - so here are my questions:
Is there some part of native jQuery that basically already does this?
Is there an extension already out there that allows this type of thing?
Is it harder to do than I am thinking? (I'd think something like keeping the current element set if the condition is true, pushing an empty element set if condition is false, then popping the element set back out for each or condition would do it, just like the end() method pops back the previous set after a find() call)
Is there something that makes it significantly less efficient?
EDIT
The question asks how to do this with method chaining or why it would be unadvisable (specifics preferred). While it doesn't ask for alternatives, such alternatives might be necessary to explain problems with a jQuery chaining approach. Also, since the example above immediately evaluates the booleans, any other solution should do the same.
$('.parent').hover(overFunction,offFunction)
.find('.child-element')
.click( booleanVar ? clickFunction1 :
booleanVar2 ? clickFunction2 :
clickFunction3 )
.end()
.find('.other-child')
.css(SomePredefinedCssMapping)
Couldn't you perform that conditional logic within your handler?
var boolVar1 = true,
boolVar2 = false;
$(".foo").on("click", function(){
if ( boolVar1 ) clickFunction1();
if ( boolVar2 ) clickFunction2();
});

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);
}

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

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."

Categories

Resources