Best practice for function flow in Javascript? - javascript

I have what I thought was a relatively easy Ajax/animation that I'm adding to a client site to select between projects for display as images. The flow goes something like this:
User clicks on a thumbnail in a project overview (pHome).
Using jQuery, ajax load in an XML file with the image, caption and project description data (project1).
Construct the HTML from the XML file and inject it into the DOM (div id="project1").
Animate and (4.5) fade out pHome.
Fade in project1.
Add a 'puff' effect to the thumbnail that brought the user to the project.
All of these things need to happen synchronously, but I can't find the right flow for the functions. Attaching all of these steps to the user click fades out pHome before the animation completes, and the 'puff' effect fires before the project1 div is even visible.
As it is, I have functions for all of these steps, and it seems like a real mess. What I'm asking for is some best-practice way of structuring these functions so they all happen synchronously (with the possible exception of 2 & 3). Just as an aid, here's the pseudocode for my problem:
$('#thumbnail').live('click', function(){
loadXML(thumbnail_id);
makeHMTL(data);
$('pHome').animate({blah}).fadeOut();
$('project1').fadeIn();
$('thumbnail_id').puff();
});
I know this is obviously a bad way to do it - but I can't figure out how to nest/structure the functions to make them all synchronous. And really I'd like an answer that gives me some way to structure my functions in the future to avoid rat-nests. Educate me! :)

Nesting animation functions is one way to do but can be nasty when you do a lot of them and you'll easily lose overview.
An option is to pack them all into an object and pass reference to the callback as such:
$('#thumbnail').live('click', animation.step1);
var animation = {
step1: function() {
$("#Element").show("puff", {}, "slow", animation.step2);
},
step2: function() {
$("#Element").hide("linear", {}, "fast", animation.step3);
},
step3: function() {
$("#Element").show("bounce", {}, 500);
}
}
Or as an alternative you can use the built in animation queues like this:
$("#go1").click(function(){
$("#block1").animate( { width:"90%" }, { queue:true, duration:100 } )
.animate( { fontSize:"24px" }, 1500 )
.animate( { borderRightWidth:"15px" }, 1500);
})
Also check the documentation: link

I would recommend you to make a callback function parameter in your loadXML function, for being able to execute the makeHTML function and your effects when the XML data is loaded from the server.
For the animations, you can execute the following animation on the previous one callback, for example:
$('#thumbnail').live('click', function(){
loadXML(thumbnail_id, function (data) { // data is loaded
makeHMTL(data);
$('pHome').animate({blah}, function () {
$(this).fadeOut();
});
$('project1').fadeIn('slow', function () {
$('thumbnail_id').puff();
});
});
});
Your loadXML function may look like this:
function loadXML (thmbId, callback) {
$.post("/your/page", { thumbnail: thmbId }, function (data) {
callback.call(this, data);
});
}

$("#thumbnail").live("click",function() {
$.ajax({
type: "GET",
url: "myUrl.ashx",
data: { param1: 1, param2: 2 },
success: function(data, textStatus) {
buildAndAppend(data); // Make sure it starts hidden.
$("#Element").show("puff", {}, "slow", function() {
anythingElse();
});
}
});
});

You can use a callback to sequence animations like so:
$('#Div1').slideDown('fast', function(){
$('#Div2').slideUp('fast');
});
Also see Finish one animation then start the other one

The animated, fadeIn and puff events/actions should all have callback options of their own that are called when they're complete. So you'd need to nest those as well and not chain them as you have.
$("#pHome").animate({}, function(){
$("#project1").fadeIn(500, function(){
$("#thumbnail_id").puff();
});
});

Related

How show easyautocomplete dialog only if cursor is in input

I'm trying set easyautocomplete on my input. Dataset i am geting from ajax json and there is some delay. If user is too fast and writes for example "Adam" and pushes tab, cursor skips to next input, but after easyautocomplete shows dialog on previous input and doesn´t hide it. Is there any way how to show easyautocomplete dialog only when i have cursor in input?
var options = {
minCharNumber: 3,
url: function(phrase) {
return "data?q=" + phrase;
},
getValue: function(element) {
return element.surname + " " + element.name;
},
template: {
type: "description",
fields: {
description: "phone"
}
},
ajaxSettings: {
dataType: "json",
method: "POST",
data: {
dataType: "json"
}
},
list: {
onClickEvent: function() {
/*Some action*/
},
hideAnimation: {
type: "slide", //normal|slide|fade
time: 400,
callback: function() {}
}
},
requestDelay: 400
};
$(".autoComplete").easyAutocomplete(options);
Minimum, Complete, Verifiable, Example
In order to easily see this result, you'll have to open up your dev tools and throttle your network traffic so the ajax connection takes a little while
Here's a Demo of the issue in jsFiddle
Handling Library Events (Doesn't Work)
My initial thought was you could handle this during some of the EAC lifecycle events that fired, like the onLoadEvent or onShowListEvent:
var options = {
url: "https://raw.githubusercontent.com/KyleMit/libraries/gh-pages/libraries/people.json",
getValue: "name",
list: {
match: {
enabled: true
},
onLoadEvent: function() {
console.log('LoadEvent', this)
},
onShowListEvent: function() {
console.log('ShowListEvent', this)
}
},
};
However, these methods don't seem to provide an option to alter the control flow and prevent future events
Updating Source Code (Works)
Peeking into the library's source code, Easy AutoComplete does the following ...
Handles keyup events
Which then calls loadData
Which fires an AJAX request with the provided URL
depending on the network and server speed, any amount of time can pass before step 4, and the input could lose focus
Once the ajax promise is returned, will call showContainer()
Which triggers the "show.eac" event on the container
Which then opens the list with the selected animation
During step 6, we could add one last check to confirm the selected input still has focus before actually opening, which would look like this:
$elements_container.on("show.eac", function() {
// if input has lost focus, don't show container
if (!$field.is(":focus")) {return}
// ... rest of method ...
Here's a working demo in Plunker which modifies the library's source code in a new file
Unfortunately, that's not a great practice as it leaves you fragile to future changes and transfers ownership of the lib maintenance to your codebase. But it does work.
I created Pull Request #388 with the proposed changes, so hopefully a long term fix within the library itself will be available at some point in the future.
Wrapper (Recommended for now)
If you don't want to muck with third party code, there are some weird workarounds to mess with the internal execution. We can't modify the showContainer method since it's inside a closure.
We can add another listener on the show.eac event so we can be a part of the event pipeline, however there are some challenges here too. Ideally, we'd like to fire before the library handler is executed and conditionally stop propagation. However, initializing EAC will both a) create the container we have to listen to and also b) attach an event listener.
Note: Event handlers in jQuery are fired in the order they are attached!
So, we have to wait until after the lib loads to attach our handler, but that means we'll only fire after the list is already displayed.
From the question How to order events bound with jQuery, we can poke into the jQuery internals and re-order attached events so we can fire our handler before the library's handler is called. That'll look like this:
$._data(element, 'events')["show"].reverse()
Note: Both e.stopPropagation() and e.preventDefault() won't work since they prevent future events; instead we need to take more immediate action with e.stopImmediatePropagation()
So the wrapper would look like this:
$("#countries").easyAutocomplete(options);
$(".easy-autocomplete-container").on("show.eac", function(e) {
var inputId = this.id.replace('eac-container-','')
var isFocused = $("#"+inputId).is(":focus")
if (!isFocused ) {
e.stopImmediatePropagation()
}
});
$(".easy-autocomplete-container").each(function() {
$._data(this, 'events')["show"].reverse()
})
Here's a working demo in CodePen

jQuery animation function breaks when switching tabs

UPDATE: I reproduced the error in a plunkr: See http://plnkr.co/BpYfCNBESUT6ZkiSZHgx
The problem occurs when you do following:
Open website. Refresh website, while you see the page is loading in the browser tab, when you see the spinner in the tab.
Switch to another tab in your browser. If it doens't happen. Try again. If did as said, you are most likely seeing this:
You might say, this is not such a big deal, you have to be real fast to let this error happen. However, imagine somebody with a slow connection, he goes to another tab in his browser, continue watching his youtube video, while website is loading. When he comes back he just sees the page keeps loading.
Lets say ive this animation code:
$pageloaderbar.animate({
width: "46%"
}, {
duration: 700,
complete: function () {
$scope.continue();
}
});
When the first animation completes it calls an new function, called $scope.continue();. Which looks likes this:
$scope.continue = function () {
$timeout(function () {
$pageloaderbar.animate({
width: "100%"
}, {
duration: 500,
complete: function () {
$scope.PageIsLoading = false;
}
});
});
}
The problem is, when a user switches tab in his browser, between the $pageloaderbar.animate and the $scope.continue, the $plageloaderbar.animate function never reaches the complete function. The browser console shows the following error (Chrome)
My question is, how can i see if the user is active on the website? Or, how can i still execute the function, even if the user is not active on the browser tab?
Because there seems no one with an awnser, i have figured an little workaround myself. However, if someone still can explain why the animation breaks when switching tab, im very pleased.
The workaround was quite simple, i only had to add this code.
complete: function () {
if(document.hidden) {
$(window).on("blur focus", function () {
$scope.continue();
});
} else {
$scope.continue();
}
}
instead of:
complete: function () {
$scope.continue();
}
This is being caused by the jQuery animate function, which passes the animate options object to a function called speed. This function checks to see if the document is hidden - which it will be if the tab is inactive. If it is hidden, (or fx.off is set to true), all animation durations are set to 0.
You can see this on line 7137 in your plunkr's jQuery file.
if ( jQuery.fx.off || document.hidden ) {
opt.duration = 0;
As the animation now has no duration, it becomes synchronous, and so the fact that your complete function comes after the call to animate, is an issue.
To fix this, you would need to move the complete function declaration above your call to animate.
In the newer version of chrome this is related to visibility of the tab and your function is breaking due to that. you can use visibility API to know that tab is visible to the user or not.
Please have a look at this link which i found :
http://dystroy.org/demos/vis-en.html
$scope.continue = function () {
$timeout(function () {
$pageloaderbar.animate({
width: "100%"
}, {
duration: 500,
complete: function () {
$scope.PageIsLoading = false;
$("body").css("overflow", "auto");
$pageloaderwrap.addClass("page-loader-finished");
$scope.pageReady();
}
});
});
}
var $pageloaderwrap = $(".page-loader-wrap");
var $pageloader = $(".page-loader");
var $pageloaderbar = $(".page-loader .page-loader-bar");
$("body").css("overflow", "hidden");
$pageloaderbar.animate({
width: "46%"
}, {
duration: 700,
complete: function () {
if (document.hidden) {
$(window).on("blur focus", function () {
$scope.continue();
});
} else {
$scope.continue();
}
}
});
Try to define your continue function before call $pageloaderbar.animate . That may not be the main problem but, it is always possible to have a issue because of that.
Your solution is just a patch, not a real solution and not seems good. Even if it solves your problem, you must find a certain way to prevent this error.
I strongly recommend you to leave jquery. Such problems usually comes from jquery - angular incompatibilities.

Run function only after another function is complete

I have been trauling the web to find an answer to this, but everything I find seems to fail.
In a function I want to disable a href link. Once this function is complete (including animations) I want to re-enable the link.
I currently have the following:
function prev() {
$prevId = $('.active').prev('.slide-item').attr('id');
if ($prevId) {
$('#prev').bind('click', false);
var id = '#'+$prevId;
$('#thumb-list li a.current').removeClass('current');
var thumb = '#thumb-list li a'+id;
$(thumb).addClass('current');
$('.active').transition({ left: offCanvas }, 300, function() { $(this).hide(); }).removeClass('active');
setTimeout(function() {
$(id).addClass('active').css({ left: -pageWidth }).show().transition({ left: 0 }, 300).css("-webkit-transform", "translate3d(0px,0px,0px)"):
}, 30);
prevFinished();
} else {
$('.active').css("-webkit-transform", "translate3d(0px,0px,0px)");
id = '#'+$('.active').attr('id');
}
$prevId = $(id).prev('.slide-item').attr('id');
$nextId = $(id).next('.slide-item').attr('id');
prevNextCheck();
}
function prevFinished() {
$('#prev').unbind('click', false);
}
But it doesn't work at all.
If anyone could point me in the right direction that would be great.
P.S I tried using callbacks, but unless I am doing something terribly wrong it failed every time.
As stated in the previous answer you could use jQuery's Deferred and Promises. Depending on when exactly you'd want your functions to be executed it'd look something like this:
var deferred = new $.Deferred();
var promise = deferred.promise();
$('.someClass').transition({ left: 0 }, 300, function(e) {
// .. maybe do some other stuff or maybe not
deferred.resolve();
});
promise.then(someFunction);
function someFunction() {
// this runs after your transition has finished
}
You can of course chain multiple then together if you wanted.
Additionally if you have multiple functions that you need to wait for to finish you could also use $.when and wait for all of them to finish.
$.when(promise1,promise2).then(someFunction);
There are a few methods of accomplishing this.
Solution one is to use callbacks. Your animation sequences look overly complex, but it appears you are making a rotation animation for multiple objects, and I don't know the requirements of it. But, one thing I can say is that your callbacks are only encapsulating a small amount of functionality from the looks of it. Widen it out to include EVERY action that is expected to occur after the animation.
Solution two is to use deferred objects and promises. I'm not the best person to ask for a working example of how this works, but the functionality fits your requirement.
Both of these routes are documents jquery features that just need to be put in place correctly.

Dynamic configuration of nested (asynch-forcing) callbacks from json file

I have a single page d3.js based browser application with a (likely) varying number of dynamically loaded elements. Ideally I'd like these configured in their entirety from json files, but converting the currently hard-coded nested callbacks (intended to force asynch behaviour, see the lower code block below) is proving something of a stumbling block.
function load_page_element(element, force_asynch) {
d3.json(("path_to/" + element + ".json"), function(data) {
// Attach the element to the DOM
// Set up some event handling
// Enjoy the fireworks
});
force_asynch();
};
// Hard-coded nested asynch-forcing callbacks
load_page_element("colours", function() {
load_page_element("sounds", function() {
load_page_element ("tools", function() {
load_page_element ("tutorials", function() {
load_page_element ("videos", function() {
load_page_element ("games", function() {
console.log("Asynch element loading completed, in order.");
});
});
});
});
});
});
I anticipate some kind of recursive call structure into which I can plug the element names ("colours", "sounds" etc), but have yet to hit on a mechanism that actually works. Presumably this has all been done before, but darned if I can find any examples.. Any suggestions are welcome..
Use queue.js for this. See also this example. The code looks like this:
queue()
.defer(request, "1.json")
.defer(request, "2.json")
.defer(request, "3.json")
.awaitAll(ready);
function ready(error, results1, results2, results3) {
// do something
}

How do I use .queue() to properly chain custom callback functions?

I'm trying to figure out how to chain custom functions:
I have something like this:
show_loader(0, function() {
open_box($target_open, event.value, '.wide-col', function() {
hide_loader(function() {
scroll_to_content($target_open, function() {
});
$(this).dequeue();
});
$(this).dequeue();
});
$(this).dequeue();
});
Those functions have a callback implemented that looks something like this:
function show_loader(position, callback) {
$ajaxSpinner.fadeIn();
status = "loading"; //some non-jQuery stuff
if (callback) $ajaxSpinner.queue( function() {callback()} );
}
You can see the basic idea of what i'm trying to do: execute my functions after the animations inside the functions are complete.
I don't think my code is quite right. The order should be: show loader, open box, hide loader, then finally scroll to content. Instead, it seems like this is what's actually happening when i test it: show loader, hide loader, scroll to content, then open box.
How do I get that order of function calls properly queued? And am I using the keyowrd "this" in the proper context?
You can see the basic idea of what i'm trying to do: execute my functions after the animations inside the functions are complete.
If you use standard animation functions from jQuery, you should be able to directly pass a callback to them. E.g.:
function show_loader(position, callback) {
$ajaxSpinner.fadeIn(callback);
status = "loading"; //some non-jQuery stuff
}
Have a look at http://api.jquery.com and see how they work.
Update:
Here is an example that produces the desired result using queue. I'm using a newer form, where the next function to execute is passed as argument to the callback. Maybe you are doing something wrong with dequeue. Edit: I tried your code and it works fine. I guess your are not using queue properly in the other functions.

Categories

Resources