We're currently not using any serious client side framework besides jQuery (and jQuery.ui + validation + form wizard plugins).
A problem that surfaces a few times in our code is this:
We have a button that initiates an Ajax call to the server.
While the call is taking place, we display a "loading" icon with some text
If the server returns a result too quickly (e.g. < 200 ms), we "sleep" for 200 millis (using setTimeout()), to prevent flickering of the waiting icon & text.
After max(the call returns, a minimal timeout), we clear the loading icon & text.
We then either display an error text, if there was some problem in the ajax call (the server doesn't return 500, but a custom json that has an "error message" property. In fact, sometimes we have such a property in the response per form field ... and we then match errors to form fields ... but I digress).
In case of success, we do ... something (depends on the situation).
I'm trying to minimize code reuse, and either write or reuse a pattern / piece of code / framework that does this. While I probably won't start using an entire new heavy-duty framework just for this use case, I would still like to know what my options are ... perhaps such a client-side framework would be good for other things as well. If there's a lightweight framework that doesn't require me to turn all my code upside down, and I could use just on specific cases, then we might actually use it instead of reinventing the wheel.
I just recently heard about Ember.js - is it a good fit for solving this problem? How would you solve it?
$(function(){
var buttonSelector = "#button";
$('body').on({'click': function(evt){
var $button = $(this);
$button.toggleClass('loading');
var time = new Date();
$.get('some/ajax').then(function(data,text,jqXhr){
// typical guess at load work
$button.empty();
$(data).wrap($button);
}).fail(function(data,text,jqXhr){
alert("failed");
}).done(function(data,text,jqXhr){
var elapsed = new Date();
if((elapsed - time) < 200){
alert("to short, wait");
}
$button.toggleClass('loading');
});
}},buttonSelector,null);
});
Just wrap the $.ajax in your own function. that way you can implement your own queing etc. I would suggest to do a jquery component for this. It can get pretty powerful, for example you can also pass http headers etc.
Regarding frameworks it depends on your requirements.
For example, you may consider Kendo UI, it has good framework for creating data sources:
http://demos.kendoui.com/web/datasource/index.html.
Working Sample Code (well, almost)
I was going for something along the lines of #DefyGravity's answer anyway - his idea is good, but is still pseudo-code/not fully complete. Here is my working code (almost working demo, up to the Ajax URL itself, and UI tweaks)
The code & usage example:
jQuery.fn.disable = function() {
$(this).attr("disabled", "disabled");
$(this).removeClass("enabled");
// Special handling of jquery-ui buttons: http://stackoverflow.com/questions/3646408/how-can-i-disable-a-button-on-a-jquery-ui-dialog
$(this).filter("button").button({disabled: true});
};
jQuery.fn.enable = function() {
$(this).removeAttr("disabled");
$(this).addClass("enabled");
// Special handling of jquery-ui buttons: http://stackoverflow.com/questions/3646408/how-can-i-disable-a-button-on-a-jquery-ui-dialog
$(this).filter("button").button({disabled: false});
};
function AjaxCallbackWaiter(ajaxUrl, button, notificationArea, loadingMessage, errorMessage, inSuccessHandler, inFailureHandler) {
// Every request that takes less than this, will be intentionally delayed to prevent a flickering effect
// http://ripper234.com/p/sometimes-a-little-sleep-is-ok/
var minimalRequestTime = 800;
var loadingIconUrl = 'http://loadinfo.net/images/preview/11_cyrcle_one_24.gif?1200916238';
var loadingImageContent = $("<img class='loading-image small' src='" + loadingIconUrl + "'/><span class='loading-text'>" + loadingMessage + "</span>");
var errorContentTemplate = $("<span class='error ajax-errors'></span>");
var requestSentTime = null;
button.click(clickHandler);
function displayLoadingMessage() {
clearNotificationArea();
notificationArea.html(loadingImageContent);
}
function clearNotificationArea() {
notificationArea.html("");
}
function displayError(message) {
var errorContent = errorContentTemplate.clone(errorContentTemplate).html(message);
notificationArea.html(errorContent);
}
function ajaxHandler(result) {
var requestReceivedTime = new Date().getTime();
var timeElapsed = requestReceivedTime - requestSentTime;
// Reset requestSentTime, preparing it for the next request
requestSentTime = null;
var sleepTime = Math.max(0, minimalRequestTime - timeElapsed);
function action() {
clearNotificationArea();
button.enable();
if (result) {
inSuccessHandler();
} else {
displayError(errorMessage);
inFailureHandler();
}
}
if (sleepTime <= 0) {
action();
} else {
setTimeout(action, sleepTime);
}
}
function failureHandler() {
}
function clickHandler(){
if (requestSentTime !== null) {
logError("Bad state, expected null");
}
requestSentTime = new Date().getTime();
displayLoadingMessage();
button.disable();
$.get(ajaxUrl, 'json').then(ajaxHandler, failureHandler);
}
}
// Usage:
var ajaxUrl = 'FILL IN YOUR OWN URL HERE';
var button = $("#clickme");
var notificationArea = $(".ajax-notification-area");
var waitingMessage = "Doing Stuff";
var errorMessage = "Not Good<br/> Please try again";
$(document).ready(function(){
new AjaxCallbackWaiter(
ajaxUrl,
button,
notificationArea,
waitingMessage,
errorMessage,
function(){
alert("All is well with the world");
},
function(){
alert("Not good - winter is coming");
});
});
Related
Here's the problem. I'm making a callback to the server that receives an MVC partial page. It's been working great, it calls the success function and all that. However, I'm calling a function after which iterates through specific elements:
$(".tool-fields.in div.collapse, .common-fields div.collapse").each(...)
Inside this, I'm checking for a specific attribute (custom one using data-) which is also working great; however; the iterator never finishes. No error messages are given, the program doesn't hold up. It just quits.
Here's the function with the iterator
function HideShow() {
$(".tool-fields.in div.collapse, .common-fields div.collapse").each(function () {
if (IsDataYesNoHide(this)) {
$(this).collapse("show");
}
else
$(this).collapse("hide");
});
alert("test");
}
Here's the function called in that, "IsDataYesNoHide":
function IsDataYesNoHide(element) {
var $element = $(element);
var datayesnohide = $element.attr("data-yes-no-hide");
if (datayesnohide !== undefined) {
var array = datayesnohide.split(";");
var returnAnswer = true;
for (var i in array) {
var answer = array[i].split("=")[1];
returnAnswer = returnAnswer && (answer.toLowerCase() === "true");
}
return returnAnswer;
}
else {
return false;
}
}
This is the way the attribute appears
data-yes-no-hide="pKanban_Val=true;pTwoBoxSystem_Val=true;"
EDIT: Per request, here is the jquery $.post
$.post(path + conPath + '/GrabDetails', $.param({ data: dataArr }, true), function (data) {
ToggleLoader(false); //Page load finished so the spinner should stop
if (data !== "") { //if we got anything back of if there wasn't a ghost record
$container.find(".container").first().append(data); //add the content
var $changes = $("#Changes"); //grab the changes
var $details = $("#details"); //grab the current
SplitPage($container, $details, $changes); //Just CSS changes
MoveApproveReject($changes); //Moves buttons to the left of the screen
MarkAsDifferent($changes, $details) //Adds the data- attribute and colors differences
}
else {
$(".Details .modal-content").removeClass("extra-wide"); //Normal page
$(".Details input[type=radio]").each(function () {
CheckOptionalFields(this);
});
}
HideShow(); //Hide or show fields by business logic
});
For a while, I thought the jquery collapse was breaking, but putting the simple alert('test') showed me what was happening. It just was never finishing.
Are there specific lengths of time a callback function can be called from a jquery postback? I'm loading everything in modal views which would indicate "oh maybe jquery is included twice", but I've already had that problem for other things and have made sure that it only ever includes once. As in the include is only once in the entire app and the layout is only applied to the main page.
I'm open to any possibilities.
Thanks!
~Brandon
Found the problem. I had a variable that was sometimes being set as undefined cause it to silently crash. I have no idea why there was no error message.
Im trying to use PhantomJS to scrape the trophy data from http://my.playstation.com/logged-in/trophies/public-trophies/
The page requires you enter a valid username and then click 'go' and the page will load the data. Ive gotten this to work somewhat, but it never loads the trophy data into the div. Im hoping im missing something ajax related thats causing this?
var fullpagehtml = page.evaluate(function()
{
document.getElementById("trophiesId").value = "<<valid user id>>";
//checkPTrophies(); btn click calls this function
$('#btn_publictrophy').click().delay( 6000 );
console.log("\nWaiting for trophy list to load...");
var trophylist = document.getElementById("trophyTrophyList").innerHtml; // all the data i want ends up inside this div
var counter = 0; //delay andset timeout wont work here so this is the best i coukld think of
while (trophylist == null)
{
//presumably the ajax query should kick in on the page and populate this div, but it doesnt.
trophylist = document.getElementById("trophyTrophyList").innerHtml;
counter ++;
if(counter == 1000000)
{
console.log($('#trophyTrophyList').html());
counter = 0;
}
}
return document.all[0].outerHTML;
});
The delay( 6000 ) does absolutely nothing as the documentation says:
The .delay() method is best for delaying between queued jQuery effects. Because it is limited—it doesn't, for example, offer a way to cancel the delay—.delay() is not a replacement for JavaScript's native setTimeout function, which may be more appropriate for certain use cases.
To wait you have to do this outside of the page context (busy waiting doesn't work in JavaScript because it is single threaded):
page.evaluate(function() {
document.getElementById("trophiesId").value = "<<valid user id>>";
//checkPTrophies(); btn click calls this function
$('#btn_publictrophy').click();
});
console.log("\nWaiting for trophy list to load...");
setTimeout(function(){
var fullpagehtml = page.evaluate(function() {
var trophylist = document.getElementById("trophyTrophyList").innerHTML;
return trophylist;
});
}, 20000);
You also might want to use waitFor to wait until #trophyTrophyList is populated instead of using setTimeout:
waitFor(function(){
return page.evaluate(function(){
var e = document.getElementById("trophyTrophyList");
return e && e.innerHTML;
});
}, function(){
// TODO: get trophies
});
This won't get you far, because just because #trophyTrophyList is loaded, doesn't mean that the descendent elements are already in the DOM. You have to find some selector which signalizes that the page is sufficiently loaded for example by waiting until a .trophy-image exists in the page. It works for me with a 20 second timeout of the waitFor function.
waitFor(function(){
return page.evaluate(function(){
var e = document.querySelector("#trophyTrophyList .trophy-image");
return e;
});
}, function(){
setTimeout(function(){
var trophiesDiv = page.evaluate(function(){
return document.getElementById("trophyTrophyList").innerHTML;
});
console.log(trophiesDiv);
}, 1000); // wait a little longer
}, 20000);
Don't forget that you need page.evaluate to actually access the DOM. Btw, it is innerHTML not innerHtml.
First Question here, too! Yay! Just moved this from AskUbuntu.
I am just about to finish a little private project for gaining some experience where i try to change the app layout so it works as a normal website (on Jimdo, so it was quite of a challenge first) without much JavaScript required but is fully functional on mobile view.
Since Jimdo serves naturally only the actual site, I had to implement an
if (activeTab.getAttribute('jimdo-target') != null)
location.href = activeTab.getAttribute('jimdo-target');
redirect into the __doSelectTab() function in tabs.js . (In js I took the values from the jimdo menu string to build the TABS menu with this link attribute)
Now everything works fine exept at page load the first tab is selected. I got it to set the .active and .inactive classes right easily, but it is not shifted to the left.
So my next idea is to let it initialize as always and then send a command to change to the current tab.
Do you have any idea how to manage this? I couldn't because of the this.thisandthat element I apparently don't really understand...
Most of you answering have the toolkit and the whole code, but I am listing the select function part of the tabs.js:
__doSelectTab: function(tabElement, forcedSelection) {
if ( ! tabElement)
return;
if (tabElement.getAttribute("data-role") !== 'tabitem')
return;
if (forcedSelection ||
(Array.prototype.slice.call(tabElement.classList)).indexOf('inactive') > -1) {
window.clearTimeout(t2);
activeTab = this._tabs.querySelector('[data-role="tabitem"].active');
offsetX = this.offsetLeft;
this._tabs.style['-webkit-transition-duration'] = '.3s';
this._tabs.style.webkitTransform = 'translate3d(-' + offsetX + 'px,0,0)';
this.__updateActiveTab(tabElement, activeTab);
if (activeTab.getAttribute('jimdo-target') != null)
location.href = activeTab.getAttribute('jimdo-target');
[].forEach.call(this._tabs.querySelectorAll('[data-role="tabitem"]:not(.active)'), function (e) {
e.classList.remove('inactive');
});
var targetPageId = tabElement.getAttribute('data-page');
this.activate(targetPageId);
this.__dispatchTabChangedEvent(targetPageId);
} else {
[].forEach.call(this._tabs.querySelectorAll('[data-role="tabitem"]:not(.active)'), function (el) {
el.classList.toggle('inactive');
});
var self = this;
t2 = window.setTimeout(function () {
var nonActiveTabs = self._tabs.querySelectorAll('[data-role="tabitem"]:not(.active)');
[].forEach.call(nonActiveTabs, function (el) {
el.classList.toggle('inactive');
});
}, 3000);
}
},
...and my app.js hasn't anything special:
var UI = new UbuntuUI();
document.addEventListener('deviceready', function() { console.log('device ready') }, true);
$(document).ready(function () {
recreate_jimdo_nav();
UI.init();
});
So meanwhile found a simple workaround, however I'd still like to know if there is another way. Eventually I noticed the __doSelectTab() function is the one that executes the click, so it does nothing but to show the other tab names when they are hidden first. so I added the global value
var jnavinitialized = false;
at the beginning of the tabs.js and run
var t = this;
setTimeout(function(){t.__doSelectTab(t._tabs.querySelector('[data-role="tabitem"].jnav-current'))}, 0);
setTimeout(function(){t.__doSelectTab(t._tabs.querySelector('[data-role="tabitem"].jnav-current'))}, 1);
setTimeout(function(){jnavinitialized = true;}, 10);
at the top of the __setupInitialTabVisibility() function. Then I changed the location.href command to
if (activeTab.getAttribute('jimdo-target') != null && jnavinitialized)
location.href = activeTab.getAttribute('jimdo-target');
And it works. But originally I searched for a way to change the tab on command, not to run the command for selecting twice. So if you know a better or cleaner way, you are welcome!
I'm using a WordPress theme that only has about 10 lines of jQuery, yet it's using the 90kb jQuery file. I want to change this jQuery code to Javascript, but I'm not very good at Javascript:
jQuery('body #main-tabbed-area a, body .wp-pagenavi a, body .pagination a').live("click", function () {
$href = jQuery(this).attr('href');
$contentArea.fadeTo('fast', 0.2).load($href + ' #main-area', function () {
$contentArea.fadeTo('fast', 1);
});
return false;
});
var $tabbed_area = jQuery('div#tabbed');
if ($tabbed_area.length) {
$tabbed_area.tabs({
fx: {
opacity: 'toggle'
}
});
};
Thanks in advance!
Personally I'd persist with jQuery. Although there is "only about 10 lines jQuery" what it is doing is quite substantial. By the time you have recreated a lot of what jQuery is providing you here you will have a fairly decent slab of javaScript to debug and maintain. That is the beauty of jQuery, to quote their tag line "write less, do more" Remember with jQuery you are eliminating a lot of annoying cross browser quirks.
Edit: See tbranyen's answer for a practical example of why jQuery is worth it
Use a CDN version of jQuery, like Google's and you will be using jQuery that your uses may already have cached and therefore does not have to be downloaded again. jQuery UI can also be served in the same way. See this article: http://encosia.com/2008/12/10/3-reasons-why-you-should-let-google-host-jquery-for-you/
EDIT
Reading your question further "I'm not very good at javaScript" is all the more reason to stick with jQuery. Let it do all the heaving lifting for you. HOWEVER don't use jQuery as an excuse not to learn more about javaScript, the more you learn about javaScript the more you will be able to get out of jQuery.
My solution is fragmented, its not complete, and I do not expect points for this answer. This was me taking a stab at replicating jQuery's code in vanilla JS, purely as scientific justice to your question. I consider myself good at JavaScript, but even I know my limitations and time constraints. I only have one life on this planet and its honestly not worth my time to write out a tabs plugin and animation for your Wordpress site.
Just take a look at the code difference. If you're really scared about people downloading, you should ask yourself what makes your site so much different than the thousands/millions? of other sites that are visited by millions of people?
Writing this stuff is tedious, that's why if I have to do these things, I use jQuery. However, lets say you don't care about older browser support. You didn't mention that, I have a solution at the very bottom that does more, but WILL not work with older browsers or work period for that matter.
The original
Very little code to do immensely complicated stuff.
jQuery('body #main-tabbed-area a, body .wp-pagenavi a, body .pagination a').live("click", function () {
$href = jQuery(this).attr('href');
$contentArea.fadeTo('fast', 0.2).load($href + ' #main-area', function () {
$contentArea.fadeTo('fast', 1);
});
return false;
});
Attempting to write from scratch
// Not even close to finished solution
(function(window, document) {
var tabbed = document.getElementById('tabbed');
// Semi-normalized event handling, not even a fraction as good as jQuery's
function attachEvent(node, type, callback) {
if(node.attachEvent) {
return node.attachEvent('on'+type, function() {
callback.apply(window.event.target, arguments);
});
}
return node.addEventListener(type, function(e) {
callback.apply(e.target, arguments);
}, true);
}
// Semi-delegation again, not even a fraction of what jQuery offers
attachEvent(document, 'click', function(e) {
var href = this.href;
var body = document.body;
var elements = [];
var slice = [].slice;
var concat = elements.concat;
// This is just the start of what it would take to emulate what jQuery is doing to match all those items
// Without a reliable selector engine like querySelectorAll (not even that reliable) you'd need to match.
elements = concat(slice.call(body.getElementById('main-tabbed-area').getElementsByTagName('a')));
elements = concat(slice.call(body.getElementsByTagName('...');
// Not even going to attempt fading
// jQuery again does all this
});
if(tabbed && tabbed.tagName === 'div') {
// No idea what tabs is? A plugin? Good luck!
}
})(this, this.document);
Code is slightly more modern... but still jeesh look at all that code
function xhr(url, callback) {
var request = new window.XMLHttpRequest();
request.open('GET', url, true);
request.onreadystatechange = function(e) {
if(e.readyState === 4) {
callback(e.responseXML);
}
};
request.send(null);
}
// No idea what contentArea is
var contentArea = ...???;
(function(window, document) {
var tabbed = document.getElementsById('tabbed');
document.addEventListener('click', function(e) {
var href;
var elements = document.querySelectorAll('body #main-tabbed-area a, body .wp-pagenavi a, body .pagination a');
var match = false;
elements.forEach(function(element) {
if(this === element) {
match = true;
}
});
if(match) {
href = e.target.href;
// Some CSS3 class that does a fade out
contentArea.classList.add('fadeOut');
xhr(href, function(data) {
var data = data.getElementById('main-area').innerHTML;
contentArea.innerHTML = data;
contentArea.classList.remove('fadeOut');
// Some CSS3 class that does a fade in
contentArea.classList.add('fadeIn');
});
return false;
}
}, true);
if(tabbed && tabbed.tagName === 'div') {
// Still no idea what tabs is? A plugin? Good luck!
}
})(this, this.document);
I am using jQuery to grab some JSON and then plug it into some elements and display it on my page.
It works fine on all pages except one, where the response seems to be the page itself.
I have placed alert()s in the callbacks (success and complete) and they never seem to be fired (though Firebug shows the request returning 200 OK which should trigger the success handler).
I don't know what to do, I've never encountered this before.
Here is the jQuery code I am using:
var specials = (function() {
var specials = false,
specialsAnchor;
var init = function() {
specialsAnchor = $('#menu-specials a');
specialsAnchor.click(function(event) {
event.preventDefault();
if (specials != false && specials.is(':visible')) {
hide();
} else {
show();
}
});
};
var load = function(callback) {
specialsAnchor.addClass('loading');
specials = $('<div />', { 'id': 'specials' }).hide().appendTo('#header');
var specialsUl = $('<ul />').appendTo(specials);
$.ajax(specialsAnchor.attr('href'), {
dataType: 'json',
success: function(data) {
$.each(data, function(i, special) {
specialsUl.append('<li><h4>' + special.heading + '</h4><p>' + special.content + '</p></li>');
});
specialsAnchor.removeClass('loading');
callback.call();
}
});
}
var show = function() {
if (specials == false) {
load(show);
return;
}
specials.slideDown(500);
}
var hide = function() {
specials.slideUp(500);
}
$(init);
})();
What is going on?
I noticed that you're including jquery.validate on this page, but not the others. jQuery validate with jQuery > 1.5 causes some issues with AJAX calls.
I realize the linked question/answer aren't exactly what you're seeing, but I've seen all kinds of weird issues with AJAX calls and this combination of validate and jQuery, so I figured it would be worth mentioning.
Hope that helps.
This is probably not a complete answer, but could be a step in the right direction. Using Charles Proxy it seems on your other pages when I click specials, it makes a request to http://www.toberua.com/~new/specials however on the contact-us page the ajax request is instead going to http://www.toberua.com/~new/contact-us (which of course is not json)
One other interesting note:
The XMLHttpRequest on other pages sets the Accept header properly (i.e. Accept application/json, text/javascript, */*; q=0.01 , whereas on the contact-us page it is set to Accept */*). I'd bet there's a different code branch being invoked...