View Full Version : efficient way to assign fallback values...
here's the situation: I'm using jQuery to select a particular DOM element. If the element doesn't exist, I want to assign a "fallback" element.
the first thing I tried was
/* "selector1" et.al. are defined elsewhere */
var element = $( selector1 ) || $( selector2 );however, when jQuery finds no matching selectors, it returns an empty object - so, no matter what, $( selector2 ) is never attempted.
I figured out this much, and it works exactly as desired:
var element = $( selector1 );
if( !element.length ){ element = $( selector2 ); }
/* which _would_ be fine, I guess, except there are quite a few fallbacks to check. */
if( !element.length ){ element = $( selector3 ); }
if( !element.length ){ element = $( selector4 ); }
// etc.I could loop it, too. But am I stuck with this approach? Or is there a better way?
thanks!
further developments...
because the implementation of each selector varies (i.e., one is a straight selector, one uses .closest(), and so forth), putting them in a loop has forced me to rely on eval(). I don't like that. I'd still prefer a quicker implementation of what I describe in my OP (the one || other || somethingelse concept), but I would settle for weeding eval() out of the loop implementation.
Here's the actual code, as it stands now:
var selector = [
"$('[data-container=' + el.attr('data-ajaxtarget') + ']')"
,"$(el.closest('data-container'))"
,"$('[data-container]')"
,"$('body')"
];
for( i in selector ){
var datatarget = eval(selector[i]);
if( datatarget.length ){ break; }
}
## EDIT ##
hmm... loop is 45ms slower (x1000 iterations) than the procedural script.
## EDIT #2 ##
here's another option:
var datatarget = $('[data-container='+el.attr('data-ajaxtarget')+']');
if( !datatarget.length ){
datatarget = $(el.closest( '[data-container]' ));
if( !datatarget.length ){
datatarget = $('[data-container]');
if( !datatarget.length ){ datatarget = $('body'); }
}
}seems to be the fastest option, especially when the first assignment is successful.
in case you're wondering what this is for,
Here's the whole thing (https://gist.github.com/3747742).
^--- please check it out. Not completely satisfied yet, but it works!
I'd love feedback! I'm still refining my javascript, so I expect there are things to be done better.
Documentation is forthcoming. For now, try this (make sure to write some content for the external files):
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>autoXHR</title>
<script src="http://code.jquery.com/jquery-1.8.0.min.js"></script>
<script src="jquery.AT.autoXHR.js">
/**
* @author Adrian Testa-Avila <AT@custom-anything.com>
* @copyright 2012 Adrian Testa-Avila
* @license Creative Commons Attribution-ShareAlike 3.0 Unported <http://creativecommons.org/licenses/by-sa/3.0/>
*/
</script>
<script>$(document).ready(function(){ $('body').ATautoXHR(); });</script>
</head>
<body>
<a href="content1.html" data-autoxhr data-autoxhrto="showresult" >This link will load <content1.html> into the div below.</a>
<div data-autoxhrinto="showresult"></div>
<div data-autoxhrinto>
<a href="somepage.html" data-autoxhr="content2.html">This link loads <content2.html>, not <somepage.html>.</a>
When clicked, the response html will replace all the contents of this div. However,
<span data-autoxhr="content2.html" data-autoxhrinto style="border-bottom: 1px dotted gray;">this span will only replace its own contents.</span>
</div>
</body>
</html>
jscheuer1
09-19-2012, 01:36 PM
If you're looking for efficiency and all of your potential elements will be identified by their id, native javascript would probably be fastest for that part. If not - say your selectors could be any valid jQuery selector, then I'd try this:
var selectors = ['#selector1', '.selector2', 'form :text'], count = -1, $temp, $jqobj;
while(selectors[++count]){
if(($temp = $(selectors[count])).size()){
$jqobj = $temp;
break;
}
}
if(!$jqobj){alert('Nothing Found!');}
else {
// use the $jqobj jQuery Object found
}
That will find the first of the selectors, in the order they appear in the array, that corresponds to at least one element on the page.
Of course, I'm not sure if this would be more efficient than what you've already worked out, you'd have to time that with a large enough number of selectors and loops that it would mean something as far as comparing efficiency.
If you like it, it should be made a function or method rather than remain global code as I have presented it here.
And, as I say, if you're just looking for elements by their id, I can show you what should be a really efficient manner of finding the first of which of those exists on a page.
thanks, John. I discovered after my first post that it's not *just* a matter of selectors - here's the scenario:
---------------------------------------
I'm attaching an event listener to an [arbitrary] element.
Inside that object, there may be one or more elements with a data-autoXHR attribute.
The idea is that this element can be used to trigger an ajax call.
When the user clicks (or, I want to allow for user-chosen events in the next version) the element, the function connected to the event listener is triggered.
The function does the following:
- determines the URL to be loaded.
this can be defined in the data-autoXHR attribute, the element's href element (if present), or a combination of the two.
- determines which element the response should be loaded into. (this is the part I was asking about above.)
a "target" element is defined by a data-autoXHRinto attribute. the function looks for the target element in this order:
1....the trigger element might define the data-autoXHRinto attribute value of the target element
2....if not, start at the trigger element and search up the DOM and see if the trigger element has a parent with data-autoXHRinto
3....if not, start at the element where the event listener is attached and search down the DOM for a child with data-autoXHRinto
4....if not, use the event listener element itself as the target.
- attempts to load the requested URL into the target container.
---------------------------------------
Because finding the target element involves DOM traversal (and not *just* finding a selector, as I'd originally planned), I had to use evil() (whoops, I meant eval() :) ) in the loop, which I didn't like. If you know another method
Can plain js methods easily (&reliably cross-browser) find arbitrary (non-id) attributes?
How easy is DOM traversal (#2 was made very simple by jQuery's .closest() method)?
---------------------------------------
As far as efficiency goes, everything I've tried so far was pretty close. I tested the original , procedural method; a for loop; and the nested ifs (which is what I ended up using). After a thousand iterations of the search process (finding the target on the first step, finding it last, not finding it at all), everything was between 470 - 590ms. I don't think that translates to a real advantage for any particular method, unless someone has a frakin' huge, messy DOM...?
---------------------------------------
If you have the time, I'd really appreciate feedback on the plugin itself (https://gist.github.com/3747742), to.
Thanks again, John!
jscheuer1
09-19-2012, 04:28 PM
With jQuery 1.7+:
$(document).on('click', '*[data-autoXHRinto]', function(e){
var autoXHRinto = this.getAttribute('data-autoXHRinto');
// here you can parse the data-autoXHRinto with regExp to determine what to do
// do that and or return false and/or do an e.preventDefault() on the click
}
That will listen for clicks on the document. If the target is or bubbles to an element with a data-autoXHRinto attribute, the function will be performed upon that element. I would suggest using the id (and/or other selector) of the recipient element to find the into element rather than using the data-autoXHRinto attribute in an ambiguous way (though that could still be workable I think), and making the data-autoXHRinto a little more complex. Perhaps a comma delimited string that the function could turn into an array and perhaps then into an object:
<a href="somepage.htm" data-autoXHRinto="href:, content2.htm, id:, someid">Wahtever</a>
<div id="someid"></div>
Then something like so (untested):
$(document).on('click', '*[data-autoXHRinto]', function(e){
var autoXHRinto = this.getAttribute('data-autoXHRinto').split(/, ?/),
autoXHRintoObj = {}, i = autoXHRinto.length - 2;
while(autoXHRinto[i]{
autoXHRintoObj[autoXHRinto[i].replace(/:$/, '')] = autoXHRinto[i + 1];
i -= 2;
}
if(!autoXHRintoObj.href && this.href){
autoXHRintoObj.href = this.href;
}
if(autoXHRintoObj.href && autoXHRintoObj.id){
$('#' + autoXHRintoObj.id).load(autoXHRintoObj.href);
e.preventDefault();
} else if (autoXHRintoObj.href){
$(this).load(autoXHRintoObj.href);
e.preventDefault();
}
}
Rewrote it. I used elements of your idea, but I wanted to keep the trigger (the element you click on) in control of the options, so I allow three values for data-autoXHR:
1. no value. by default, the script will use the element's href (if present) to define the resource to load.
2. JSON map of options. If a JSON string is used, the script tries to parse options from it. You can specify the resource to load, the target container, and more.
3. resource URI. if not a JSON string, the script treats the value as a URI to load.
Demo page (http://custom-anything.com/front-end-lib/autoXHR).
Also, I started a repo for my javascript stuff. Check it out (https://github.com/customanything/front-end-lib)! Thanks!
jscheuer1
09-22-2012, 03:24 PM
Overall it looks pretty good though I haven't gone over it in full detail. Two things I would draw your attention to:
console.log isn't available in all browsers. Even in some where it is, it's not always. In IE 9 for example, if developer tools hasn't been opened yet for that window, you get an error and the link fires normally. You can open and close developer tools that will work. But if you close IE 9 entirely, open it fresh without developer tools, and run the page, you will see. If you want to use console.log in a production version of the code you have to test for it before using it and refrain from using it when it's not found. Or, you might be able to spoof it at the beginning of your code, something like:
if(typeof console === 'undefined'){
console = {log: function(){}};
}
But that might cause problems, reserved words, or what if it becomes available later? What I've always done is make up my own function:
function logging(msg){
if(typeof console !== 'undefined' && console.log){
console.log(msg);
}
}
And then use that to perform any logging desired. Browsers without console or without console.log will do nothing.
Even when everything is working, there seems to be some problem with nested elements in some browsers:
<a href=somepage.html data-autoxhr=content2.html>This link loads <code>content2.html</code>, not <code>somepage.html</code>.</a>
I'm not sure if that's because bubbling fails or because <code> is a native block level element and therefore technically invalid within an <a> tag. It looks like that because of this bit of code:
})( $( event.target ) )
That you're analyzing the target tag rather than the bubbled to tag. These should be different if it's the nested tag that's clicked on. I believe substituting this for event.target will get the desired element regardless of nesting. But only if this still at that point in the code represents the data-autoxhr element that was clicked on. As far as I can see it does, and it definitely does at the beginning of the highlighted function:
$( this ).on(
options.triggerEvent
,'['+options.data_autoXHR+']'
,function( event ){
console.log('autoXHR: triggered');
event.preventDef . . .
...console.log isn't available in all browsers.
I had wondered... the logging was basically for my own benefit (to help debugging), and I left it there just a an "extra." I could just as easily drop it completely. I'm not sure if the average user would even notice.
Even when everything is working, there seems to be some problem with nested elements in some browsers:
<a href=somepage.html data-autoxhr=content2.html>This link loads <code>content2.html</code>, not <code>somepage.html</code>.</a>
I'm not sure if that's because bubbling fails or because <code> is a native block level element and therefore technically invalid within an <a> tag.I noticed that last night, after I posted the demo. I completely overlooked the possibility of the <code> element being a problem. I think I have narrowed it down to another issue (when I try{} to parse the attribute to see if it's JSON), but I haven't made that fix yet. I'll report back on that.
That you're analyzing the target tag rather than the bubbled to tag. These should be different if it's the nested tag that's clicked on. I believe substituting this for event.target will get the desired element regardless of nesting. But only if this still at that point in the code represents the data-autoxhr element that was clicked on.
Yeah... that was intentional, though I see it may be causing some problems. Here's what I'm trying to achieve:
Basically, there are three elements this script deals with:
1. the "listener" element - that is, whichever element you call .autoXHR() on: that's where the event listener is attached. In my demo, this is the <body>, but it could be any element (the effects would be limited to that element's children; creating the possibility of multiple .autoXHR()'s, possibly with different configuration settings).
2. the "target" element - the element which will be the container for the ajax-loaded content (defined by [data-XHRinto]).
3. the "trigger" element - the element which is clicked (or whatever event you choose) and starts the whole thing off (defined by [data-autoXHR]). This is the event.target element. I wanted all of the ajax-call-specific options to be defined by this element, rather than by the target (which may be targeted by any number of triggers) or the listener (which may contain any number of triggers).
The [data-XHRinto] attribute (on the "target" element) may either be void, or hold an arbitrary (and not necessarily unique) name for the target.
The [data-autoXHR] attribute (on the "trigger" element) may have one of three values:
- JSON string: representing a config object for the trigger (resource, target, data to pass to the server, etc.)
- non-JSON string: interpreted as the URI of the resource to load
- void: script falls back to the [href] value to specify the resource to load.
jscheuer1
09-22-2012, 09:59 PM
I think you're over thinking it. I tried this and it worked fine. The keyword this will at that point always be the element with the the data-autoXHR attribute that was clicked. That's regardless of the context element (body or whatever).
If you are trying to distinguish a data-autoXHR that's within a data-autoXHR, you cannot do it easily with event.target. The script as it is will always use the innermost data-autoXHR element. You could walk up the DOM to see if there is a:
$(this).parents('[data-autoXHR]')
and if so do something additional on that basis and/or upon the basis of its data-autoXHR value. Though of course there will always be one, even if it's an empty jQuery object, so you have to test its .size(), and presumably if it has size (a number greater than 0) pick its .first() or .last() to deal with. Something like:
var parXHR = $(this).parents('[data-autoXHR]');
if(parXHR.size()){
// do something with it/them/one of them here
}
If you are trying to distinguish a data-autoXHR that's within a data-autoXHR, you cannot do it easily with event.target. The script as it is will always use the innermost data-autoXHR element.sorry, I'm a little confused: do you mean that event.target will always (in this case) use the innermost data-autoXHR element?
I though it pointed at the specific element the event originated from...? (that's what I want)
## EDIT ##
I think you're over thinking it. I tried this and it worked fine. The keyword this will at that point always be the element with the the data-autoXHR attribute that was clicked. That's regardless of the context element (body or whatever).I missed that the first time.
I'm still a little uneasy about what this points at, especially in the context of event handlers. I thought this would point at the element that "heard" the event (where the event listener is, the <body> in this case)? You're saying that this points at the element that the event originated from? If that's the case, what is the point of event.target (wouldn't that mean that event.target == this would always be true)?
jscheuer1
09-23-2012, 02:12 AM
With an event function like so:
$('anythingyoucanimagine').on('click', 'anyotherselectoryoucanimagine', function(){
alert(this) // will always be the 'anyotherselectoryoucanimagine' element that was clicked on*
}
*The scenario in this situation is as follows:
For the function to fire at all, $('anythingyoucanimagine') must have been clicked on and that event must have happened either on or on a child of an 'anyotherselectoryoucanimagine' element contained in $('anythingyoucanimagine'). If and only if that happens, the function will fire. When the function fires, this will be the 'anyotherselectoryoucanimagine' element that received the click. It cannot be anything else. It will never be $('anythingyoucanimagine'), and it will never be the child of the 'anyotherselectoryoucanimagine' element, if there was any, that first received the click before it bubbled up to the 'anyotherselectoryoucanimagine' element.
Got that?
P.S. Sorry about my tone. Hope it doesn't get in the way of the information.
$('anythingyoucanimagine').on('click', 'anyotherselectoryoucanimagine', function(){
alert(this) // will always be the 'anyotherselectoryoucanimagine' element that was clicked on*
}what about in this situation?
$('selector').on( 'click','otherselector',function(){
alert( this ); // always 'otherselector' element, got it
var myObj = (function(){
alert( this ); // is this this the same this as that this (up there)?
// and, do I need the self-executing function? or should I just unwrap what I'm doing here?
})();
});
P.S. Sorry about my tone. Hope it doesn't get in the way of the information.?
plenty of info, thanks. didn't read any [negative?] tone.
jscheuer1
09-23-2012, 03:17 AM
In that scenario, this inside the self executer would probably be the window because there's no other context I can see for it. But that's not what you had in the code attached to your demo page when I looked at it and not what I was advising. There it was like so (this function was contained within the the click event function):
var triggerData = (function( t ){
// base settings map
var tD = { args:'',complete:'',resource:'',success:'',target:'' };
// get autoXHR attribute value
var d = t.attr( options.data_autoXHR );
// is it JSON?
try{ d = JSON.parse( d ); }catch(e){ /* no error, just not JSON */ }
// a map means it cont . . .
. . . R: successful (loaded resource <' +tD.resource+ '> to target ' +tD.target.selector+ ')' );
event.preventDefault();
}
return tD;
})( $( event.target ) )
I was only saying that by changing event.target to this that you would always have the data-autoxhr element that was clicked on.
Full code of my working mock up:
/**
* @package front-end-lib
* @subpackage jQuery
* @author Adrian Testa-Avila <AT@custom-anything.com>
* @copyright 2012 Adrian Testa-Avila
* @license Creative Commons Attribution-ShareAlike 3.0 Unported
* <http://creativecommons.org/licenses/by-sa/3.0/>
* @version 0.2
*/
/**
* jQuery plugin: AT.autoXHR
* uses HTML5 data-* attributes to inobtrusively trigger ajax calls.
* @uses jQuery version 1.8 <http://jquery.com>
* @example demo package at <http://custom-anything.com/front-end-lib/autoXHR>
* @todo extended testing
*/
if(typeof console === 'undefined'){
window.console = {log: function(){}};
}
(function( $ ){
$.fn.autoXHR = function( userOptions ){
console.log('autoXHR: setting event listeners...');
var options = {
data_autoXHR: 'data-autoxhr'
,data_XHRinto: 'data-xhrinto'
,triggerEvent: 'click'
};
if( userOptions ){ $.extend( options,userOptions ); }
return this.each( function(){
var listener = this;
$( this ).on(
options.triggerEvent
,'['+options.data_autoXHR+']'
,function( event ){
console.log('autoXHR: triggered');
event.preventDefault();
// get settings for this call
var triggerData = (function( t ){
// base settings map
var tD = { args:'',complete:'',resource:'',success:'',target:'' };
// get autoXHR attribute value
var d = t.attr( options.data_autoXHR );
// is it JSON?
try{ d = JSON.parse( d ); }catch(e){ /* no error, just not JSON */ }
// a map means it contains settings;
// a string is assumed to be a URI for a resource to load
if( typeof d === 'object' ){ $.extend( tD,d ); }
else if( typeof d === 'string' ){ tD.resource = d; }
// set resource
// if resource is empty or a fragment, prepend trigger's href value
if( !tD.resource || /^#/.test( tD.resource ) ){
h = t.attr( 'href' ) || '';
tD.resource = h + tD.resource;
// no resource?
if( !tD.resource ){
console.log('autoXHR: trigger specifies no resource');
return false;
}
}
// set container
// if no target specified, look for:
// - first container _down_ DOM from listener element
// - listener element
if( !tD.target || !tD.target.length ){
tD.target = $( listener ).children( '[' +options.data_XHRinto+ ']' ).first();
if( !tD.target || !tD.target.length ){
tD.target = $( listener );
}
}else{ tD.target = $( tD.target ); }
// no target?
if( !tD.target || !tD.target.length ){
console.log('autoXHR: target container invalid or not specified');
return false;
}
// set success handler
switch( tD.success ){
case 'append':
case 'prepend':
case 'html': var successmethod = tD.success; break;
default: var successmethod = 'html'; break;
}
tD.success = function( response ){
tD.target[successmethod]( response );
console.log('autoXHR: successful (loaded resource <' +tD.resource+ '> to target ' +tD.target.selector+ ')' );
event.preventDefault();
}
return tD;
})( $( this ) )
$.ajax({
'data' : triggerData.args
,'error' : function(x,e){ console.log('autoXHR: failed (' +x.status+ ': ' +x.statusText+ ')'); }
,'global' : false
,'success' : triggerData.success
,'url' : triggerData.resource
});
}
);
console.log('autoXHR: ' +options.triggerEvent+ ' listener set on ' +listener.nodeName);
});
}
})( jQuery );
But that's not what you had in the code...
right, no, it's not. Kinda a separate question.
I was avoiding this specifically because I didn't know what it would refer to inside a self-executable.
I was only saying that by changing event.target to this that you would always have the data-autoxhr element that was clicked on.
Full code of my working mock up:
I made that same change after reading your last post, and yes, it works perfectly.
Now, another separate question: do I need that self-executable? would I be better/worse just creating the triggerData object procedurally? would it even make a difference, in this case?
(The original reason I did it was that I was creating a bunch of temporary vars while building the triggerData object, and I didn't want them "laying around" after I was done - so I used the function to scope them.)
jscheuer1
09-23-2012, 04:51 AM
Because it only needs to be defined once, I'd move that function outside of the:
$( this ).on(
options.triggerEvent
,'['+options.data_autoXHR+']'
,function( event ){
function. Like so:
/**
* @package front-end-lib
* @subpackage jQuery
* @author Adrian Testa-Avila <AT@custom-anything.com>
* @copyright 2012 Adrian Testa-Avila
* @license Creative Commons Attribution-ShareAlike 3.0 Unported
* <http://creativecommons.org/licenses/by-sa/3.0/>
* @version 0.2
*/
/**
* jQuery plugin: AT.autoXHR
* uses HTML5 data-* attributes to inobtrusively trigger ajax calls.
* @uses jQuery version 1.8 <http://jquery.com>
* @example demo package at <http://custom-anything.com/front-end-lib/autoXHR>
* @todo extended testing
*/
if(typeof console === 'undefined'){
window.console = {log: function(){}};
}
(function( $ ){
$.fn.autoXHR = function( userOptions ){
console.log('autoXHR: setting event listeners...');
var options = {
data_autoXHR: 'data-autoxhr'
,data_XHRinto: 'data-xhrinto'
,triggerEvent: 'click'
};
function getTriggerData(t, listener){
// base settings map
var tD = { args:'',complete:'',resource:'',success:'',target:'' };
// get autoXHR attribute value
var d = t.attr( options.data_autoXHR );
// is it JSON?
try{ d = JSON.parse( d ); }catch(e){ /* no error, just not JSON */ }
// a map means it contains settings;
// a string is assumed to be a URI for a resource to load
if( typeof d === 'object' ){ $.extend( tD,d ); }
else if( typeof d === 'string' ){ tD.resource = d; }
// set resource
// if resource is empty or a fragment, prepend trigger's href value
if( !tD.resource || /^#/.test( tD.resource ) ){
h = t.attr( 'href' ) || '';
tD.resource = h + tD.resource;
// no resource?
if( !tD.resource ){
console.log('autoXHR: trigger specifies no resource');
return false;
}
}
// set container
// if no target specified, look for:
// - first container _down_ DOM from listener element
// - listener element
if( !tD.target || !tD.target.length ){
tD.target = $( listener ).children( '[' +options.data_XHRinto+ ']' ).first();
if( !tD.target || !tD.target.length ){
tD.target = $( listener );
}
}else{ tD.target = $( tD.target ); }
// no target?
if( !tD.target || !tD.target.length ){
console.log('autoXHR: target container invalid or not specified');
return false;
}
// set success handler
switch( tD.success ){
case 'append':
case 'prepend':
case 'html': var successmethod = tD.success; break;
default: var successmethod = 'html'; break;
}
tD.success = function( response ){
tD.target[successmethod]( response );
console.log('autoXHR: successful (loaded resource <' +tD.resource+ '> to target ' +tD.target.selector+ ')' );
event.preventDefault();
}
return tD;
}
if( userOptions ){ $.extend( options,userOptions ); }
return this.each( function(){
var listener = this;
$( this ).on(
options.triggerEvent
,'['+options.data_autoXHR+']'
,function( event ){
console.log('autoXHR: triggered');
event.preventDefault();
// get settings for this call
var triggerData = getTriggerData( $( this ), listener );
$.ajax({
'data' : triggerData.args
,'error' : function(x,e){ console.log('autoXHR: failed (' +x.status+ ': ' +x.statusText+ ')'); }
,'global' : false
,'success' : triggerData.success
,'url' : triggerData.resource
});
}
);
console.log('autoXHR: ' +options.triggerEvent+ ' listener set on ' +listener.nodeName);
});
}
})( jQuery );
However, I question your use of the listener. In this case it's the body. So if there is no other receptacle element found, you intend for the imported content to replace the current body's content? I guess that might be OK, and/or, since there are ample opportunities to define a narrower receptacle, a person using this code would be in most situations foolish to let it come to that. While at the same time, if the listener were a narrower element, then that part might make sense in those cases.
However, I question your use of the listener. In this case it's the body. So if there is no other receptacle element found, you intend for the imported content to replace the current body's content? I guess that might be OK, and/or, since there are ample opportunities to define a narrower receptacle, a person using this code would be in most situations foolish to let it come to that. While at the same time, if the listener were a narrower element, then that part might make sense in those cases.There's no significance in using the body as the listener; that's just how it worked out in the demo. It could have just as easily been a wrapper <div>, or on a much more isolated branch of the DOM (in fact, I think I will make that change to the demo). There might be several different "listeners" at different places on the page, possibly with different config options.
One of my primary use cases is form submissions on a sort of "control panel" page: submit a form via ajax, display the result. The result might include other administrative options - or maybe it's a multi-step form, or a generic modal container (automatic ajax-y slideshow using nothing more than a collection of <img> tags? :) ). I can also imagine times where it might actually be useful to load a completely new <body> while leaving the page itself intact, such as where there are a significant number of resources to reload, or an application state that you don't want to interrupt.
Anyway, what I wanted is to provide a method of removing/replacing *everything* that is no longer needed, while keeping all of the application scripts (including the ajax stuff) intact. The autoXHR attributes provide hooks for building new ajax calls, based on whatever the app needs from the server, without the need for dealing with loading any actual script (which is a PITA, imo).
jscheuer1
09-23-2012, 06:57 AM
I did say it could be OK. What do you think about moving the function?
did it. liked it. I pushed the new version (http://github.com/customanything/front-end-lib) about an hour ago.
I really appreciate all your help!
jscheuer1
09-23-2012, 05:43 PM
Neat. I'm better off with a link to a live demo. But I have one here. I discovered that:
tD.success = function( response ){
tD.target[successmethod]( response );
msg('autoXHR: successful (loaded resource <' +tD.resource+ '> to target ' +tD.target.selector+ ')' );
event.preventDefault();
}
return tD;
Throws an error in IE 8 and less unless we pass the event to getTriggerOpts. But since the event is already default prevented here:
return this.each( function(){
var listener = this;
$( this ).on( options.triggerEvent,'['+options.data_autoXHR+']',function( event ){
msg('autoXHR: triggered');
event.preventDefault();
// get settings for this call
var triggerOpts = getTrigg
It probably doesn't need to be in the success function, and if it were only in the success function, it would probably fire too late to do anything. Alternatively, if you wanted to fire the element normally on error, you could use the reference you have to it and fire it's href (if any and if it was an <a> tag) to its target (here I mean like _self, _parent, _blank, etc.) as part of the error function.
I also experimented with making the function that fires on the click a separate function and so far came up with (includes passing the event to getTriggerOpts, which as I say above probably could be completely dropped along with the second preventDefault):
/**
* @package front-end-lib
* @subpackage jQuery
* @author Adrian Testa-Avila <AT@custom-anything.com>
* @copyright 2012 Adrian Testa-Avila
* @license Creative Commons Attribution-ShareAlike 3.0 Unported
* <http://creativecommons.org/licenses/by-sa/3.0/>
* @version 0.2
*/
/**
* jQuery plugin: AT.autoXHR
* uses HTML5 data-* attributes to inobtrusively trigger ajax calls.
* @uses jQuery version 1.8 <http://jquery.com>
* @example demo package at <http://custom-anything.com/front-end-lib/autoXHR>
* @todo extended testing
*/
if(typeof console === 'undefined'){
window.console = {log: function(){}};
}
(function( $ ){
$.fn.autoXHR = function( userOptions ){
console.log('autoXHR: setting event listeners...');
var options = {
data_autoXHR: 'data-autoxhr'
,data_XHRinto: 'data-xhrinto'
,triggerEvent: 'click'
};
function getTriggerData(t, listener, event){
// base settings map
var tD = { args:'',complete:'',resource:'',success:'',target:'' };
// get autoXHR attribute value
var d = t.attr( options.data_autoXHR );
// is it JSON?
try{ d = JSON.parse( d ); }catch(e){ /* no error, just not JSON */ }
// a map means it contains settings;
// a string is assumed to be a URI for a resource to load
if( typeof d === 'object' ){ $.extend( tD,d ); }
else if( typeof d === 'string' ){ tD.resource = d; }
// set resource
// if resource is empty or a fragment, prepend trigger's href value
if( !tD.resource || /^#/.test( tD.resource ) ){
h = t.attr( 'href' ) || '';
tD.resource = h + tD.resource;
// no resource?
if( !tD.resource ){
console.log('autoXHR: trigger specifies no resource');
return false;
}
}
// set container
// if no target specified, look for:
// - first container _down_ DOM from listener element
// - listener element
if( !tD.target || !tD.target.length ){
tD.target = $( listener ).children( '[' +options.data_XHRinto+ ']' ).first();
if( !tD.target || !tD.target.length ){
tD.target = $( listener );
}
}else{ tD.target = $( tD.target ); }
// no target?
if( !tD.target || !tD.target.length ){
console.log('autoXHR: target container invalid or not specified');
return false;
}
// set success handler
switch( tD.success ){
case 'append':
case 'prepend':
case 'html': var successmethod = tD.success; break;
default: var successmethod = 'html'; break;
}
tD.success = function( response ){
tD.target[successmethod]( response );
console.log('autoXHR: successful (loaded resource <' +tD.resource+ '> to target ' +tD.target.selector+ ')' );
event.preventDefault();
}
return tD;
}
function processEvent( event, listener ){
console.log('autoXHR: triggered');
event.preventDefault();
// get settings for this call
var triggerData = getTriggerData( $( this ), listener, event );
$.ajax({
'data' : triggerData.args
,'error' : function(x,e){ console.log('autoXHR: failed (' +x.status+ ': ' +x.statusText+ ')'); }
,'global' : false
,'success' : triggerData.success
,'url' : triggerData.resource
});
}
if( userOptions ){ $.extend( options, userOptions ); }
return this.each( function(){
var listener = this;
$( this ).on (
options.triggerEvent
,'['+options.data_autoXHR+']'
,function(event){processEvent.call(this, event, listener);}
);
console.log('autoXHR: ' +options.triggerEvent+ ' listener set on ' +listener.nodeName);
});
}
})( jQuery );
But because of the need to still have a function run the function, I'm not sure how beneficial that part (moving it out there as I have) is. Probably saves parsing it each time there's a click. A little oop might be good for that, but jQuery is already using this, so I'm not sure exactly how to oop it.
jscheuer1
09-24-2012, 06:39 AM
I just discovered that JSON.parse isn't supported in IE 7 and less. So if you're concerned about that, change:
try{ d = JSON.parse( d ); }catch(e){ /* no error, just not JSON */ }
to:
try{ d = $.parseJSON( d ); }catch(e){ /* no error, just not JSON */ }
The comment there isn't entirely true. It could be malformed JSON - just something to think about.
And the more I thought about it, the more it seems certain that the event.preventDefault() in the success function is pointless. Let me know if I missed something there. Here's what I have at the moment:
/**
* @package front-end-lib
* @subpackage jQuery
* @author Adrian Testa-Avila <AT@custom-anything.com>
* @copyright 2012 Adrian Testa-Avila
* @license Creative Commons Attribution-ShareAlike 3.0 Unported
* <http://creativecommons.org/licenses/by-sa/3.0/>
* @version 0.2
*/
/**
* jQuery plugin: AT.autoXHR
* uses HTML5 data-* attributes to inobtrusively trigger ajax calls.
* @uses jQuery version 1.8 <http://jquery.com>
* @example demo package at <http://custom-anything.com/front-end-lib/autoXHR>
* @todo extended testing
*/
if(typeof console === 'undefined'){
window.console = {log: function(){}};
}
(function( $ ){
$.fn.autoXHR = function( userOptions ){
console.log('autoXHR: setting event listeners...');
var options = {
data_autoXHR: 'data-autoxhr'
,data_XHRinto: 'data-xhrinto'
,triggerEvent: 'click'
};
function getTriggerData(t, listener){
// base settings map
var tD = { args:'',complete:'',resource:'',success:'',target:'' };
// get autoXHR attribute value
var d = t.attr( options.data_autoXHR ), dtemp;
// is it JSON?
try{ d = $.parseJSON( d ); }catch(e){ /* no error, just not JSON */ }
// a map means it contains settings;
// a string is assumed to be a URI for a resource to load
if( typeof d === 'object' ){ $.extend( tD,d ); }
else if( typeof d === 'string' ){ tD.resource = d; }
// set resource
// if resource is empty or a fragment, prepend trigger's href value
if( !tD.resource || /^#/.test( tD.resource ) ){
h = t.attr( 'href' ) || '';
tD.resource = h + tD.resource;
// no resource?
if( !tD.resource ){
console.log('autoXHR: trigger specifies no resource');
return false;
}
}
// set container
// if no target specified, look for:
// - first container _down_ DOM from listener element
// - listener element
if( !tD.target || !tD.target.length ){
tD.target = $( listener ).children( '[' +options.data_XHRinto+ ']' ).first();
if( !tD.target || !tD.target.length ){
tD.target = $( listener );
}
}else{ tD.target = $( tD.target ); }
// no target?
if( !tD.target || !tD.target.length ){
console.log('autoXHR: target container invalid or not specified');
return false;
}
// set success handler
switch( tD.success ){
case 'append':
case 'prepend':
case 'html': var successmethod = tD.success; break;
default: var successmethod = 'html'; break;
}
tD.success = function( response ){
tD.target[successmethod]( response );
console.log('autoXHR: successful (loaded resource <' +tD.resource+ '> to target ' +tD.target.selector+ ')' );
}
return tD;
}
function processEvent( event, listener ){
console.log('autoXHR: triggered');
event.preventDefault();
// get settings for this call
var triggerData = getTriggerData( $( this ), listener );
$.ajax({
'data' : triggerData.args
,'error' : function(x,e){ console.log('autoXHR: failed (' +x.status+ ': ' +x.statusText+ ')'); }
,'global' : false
,'success' : triggerData.success
,'url' : triggerData.resource
});
}
if( userOptions ){ $.extend( options, userOptions ); }
return this.each( function(){
var listener = this;
$( this ).on (
options.triggerEvent
,'['+options.data_autoXHR+']'
,function(event){processEvent.call(this, event, listener);}
);
console.log('autoXHR: ' +options.triggerEvent+ ' listener set on ' +listener.nodeName);
});
}
})( jQuery );
I discovered that:
tD.success = function( response ){
tD.target[successmethod]( response );
msg('autoXHR: successful (loaded resource <' +tD.resource+ '> to target ' +tD.target.selector+ ')' );
event.preventDefault();
}
return tD;
Throws an error in IE 8 and less unless we pass the event to getTriggerOpts. But since the event is already default prevented here:
return this.each( function(){
var listener = this;
$( this ).on( options.triggerEvent,'['+options.data_autoXHR+']',function( event ){
msg('autoXHR: triggered');
event.preventDefault();
// get settings for this call
var triggerOpts = getTrigg
It probably doesn't need to be in the success function, and if it were only in the success function, it would probably fire too late to do anything. Alternatively, if you wanted to fire the element normally on error, you could use the reference you have to it and fire it's href (if any and if it was an <a> tag) to its target (here I mean like _self, _parent, _blank, etc.) as part of the error function.the second one was a mistake. I think I'll try to roll it into the success/error functions, though.
I also experimented with making the function that fires on the click a separate function and so far came up with (includes passing the event to getTriggerOpts, which as I say above probably could be completely dropped along with the second preventDefault):
...
But because of the need to still have a function run the function, I'm not sure how beneficial that part (moving it out there as I have) is. Probably saves parsing it each time there's a click. A little oop might be good for that, but jQuery is already using this, so I'm not sure exactly how to oop it.
I don't know how beneficial it would be either, but I do like it better the way you have it.
I just discovered that JSON.parse isn't supported in IE 7 and less.
...
try{ d = $.parseJSON( d ); }catch(e){ /* no error, just not JSON */ }
The comment there isn't entirely true. It could be malformed JSON - just something to think about.
true. same outcome -doesn't matter to the script- but I should clarify the comment.
Your IE comments helps, though. I have to dig out my old laptop to test IE7, and beg to borrow my wife's to see IE8. :D Haven't been testing in IE at all, yet.
Broadly speaking, I support IE7. I don't mind if some stuff isn't available, but I want everything to at least degrade gracefully. I think moving the .preventDefault() into the success function would accomplish that - it would "never work" in IE7, but at least it wouldn't throw any errors and would still allow hyperlinks to work normally.
jscheuer1
09-25-2012, 02:08 AM
OK, hmm. I'm not sure if you understand what I'm saying about event.preventDefault(). Having it in the success function is ineffective. By the time the success function fires, and this is regardless of the browser, it's too late to prevent the default action of the original click.
Do you follow that, or did you get that before?
In any case, to prevent the default action you need to keep:
function processEvent( event, listener ){
console.log('autoXHR: triggered');
event.preventDefault();
// get settings for this call
var triggerData = getTriggerData( $( this ), listener );
$.ajax({
'data' : triggerData.args
,'error' : function(x,e){ console.log('autoXHR: failed (' +x.status+ ': ' +x.statusText+ ')'); }
,'global' : false
,'success' : triggerData.success
,'url' : triggerData.resource
});
}
If you want the default action to fire on error, you need to determine what it might be and pass that or more likely information about it to the error function so the error function can execute an equivalent action. So if it was a link for instance, you pass the href to the error function and have the error function load that href into the window via:
window.location.href = passedhref;
OK, hmm. I'm not sure if you understand what I'm saying about event.preventDefault(). Having it in the success function is ineffective. By the time the success function fires, and this is regardless of the browser, it's too late to prevent the default action of the original click.
Do you follow that, or did you get that before?
...
window.location.href = passedhref;
No, I had misunderstood what you said. Got it now - thanks for the example!
jscheuer1
09-25-2012, 03:17 AM
OK, good.
However, it can be tricky, even very tricky determining what the exact intended action of the click or whatever event is being tracked was. Consider for example a link that already has a default prevented click or even a hard coded onclick event that returns false on it for some other purpose. That link was never intended to fire. So, unless you really need to execute it on error, you're better off just having an alert, or a div that pops up with an alternate action that can optionally be tried, or nothing. Or have it be an option that can be passed in the JSON object.
Powered by vBulletin® Version 4.2.2 Copyright © 2021 vBulletin Solutions, Inc. All rights reserved.