Javascript scope/hoisting OR promises/deferreds? - javascript

I am trying to make an external AJAX call to an API within a Jquery each loop.
Here is the code I have so far.
getStylesInfo(tmpMake, tmpModel, tmpModelYear, tmpSubmodel).done(function(data){
var holder = [];
$.each(styles, function(index, value) {
var tempValue = value;
var temp = getNavigationInfo(value.id);
$.when(temp).done(function(){
if(arguments[0].equipmentCount == 1){
holder.push(tempValue);
console.log(holder);
}
});
});
});
console.log(holder);
function getStylesInfo(make, model, year, submodel){
return $.ajax({
type: "GET",
url: apiUrlBase + make + '/' + model + '/' + year + '/' + 'styles? fmt=json&' + 'submodel=' + submodel + '&api_key=' + edmundsApiKey + '&view=full',
dataType: "jsonp"
});
function getNavigationInfo(styleId){
return $.ajax({
type: "GET",
url: apiUrlBase + 'styles/' + styleId + '/equipment?availability=standard&name=NAVIGATION_SYSTEM&fmt=json&api_key=' + edmundsApiKey,
dataType: "jsonp"
});
The getStylesInfo() returns something similar to this. An array of objects with info about a car model.
var sampleReturnedData = [{'drivenWheels': 'front wheel drive', 'id': 234321}, {'drivenWheels': 'front wheel drive', 'id': 994301}, {'drivenWheels': 'rear wheel drive', 'id': 032021}, {'drivenWheels': 'all wheel drive', 'id': 184555}];
I am trying to loop through the sampleReturnedData and use each id as a parameter in a different AJAX call with the getNavigationInfo() function.
I want to loop through the results and make a check. If it is true then I want to push the entire object to the holder array.
The problem is the console.log(holder) outside the function returns an empty array. The console.log(holder) within the if statement works properly.
I am not sure if this is a scope/hoisting issue or a problem with the way I am using deferreds?
I have read this question and many like it. They suggest to use either
async:false
Or to rewrite the code better. I have tried and used console debugger numerous times. I don't want to set it to false. I'm just unsure what exactly is going on.
I've also read up on hoisting via this article.
I believe it has to do with deferreds but I don't have enough JS knowledge to figure it out.
Thanks!

I am not sure if this is a scope/hoisting issue or a problem with the way I am using deferreds?
In fact, it is both:
holder is declared only within the callback function (as a local variable), so it's undefined outside the function.
And the console.log is executed before the asynchronous callback function does fill the array with values, so even if holder was in scope it would still have been empty. See also Why is my variable unaltered after I modify it inside of a function? - Asynchronous code reference
So what you should do is indeed to rewrite your code to use promises properly :-)
getStylesInfo(tmpMake, tmpModel, tmpModelYear, tmpSubmodel).then(function(data) {
var holder = [];
var promises = $.map(data.styles, function(value, index) {
return getNavigationInfo(value.id).then(function(v){
if (v.equipmentCount == 1)
holder.push(value);
});
});
return $.when.apply($, promises).then(function() {
return holder;
}); // a promise for the `holder` array when all navigation requests are done
}).then(function(holder) {
console.log(holder); // use the array here, in an async callback
});

Related

How does a defined variable get set to undefined inside an ajax call via jQuery?

I am making an ajax call to an api which returns successful, however there is a slight issue with a variable that gets set to undefined which I don't get why. I select an <i> element and store it inside a variable to display it as a loading bar, and I then try to hide it again inside the success callback.
The element is stored successfully and is used to display the loading bar before the ajax call, however it gives an error saying its undefined inside the success callback. I am perplexed about what could cause the issue. Couldn't find any other solutions online. Please see code below.
The weird thing is I have an <ul> element that is declared after the <i> element and it never gets set to undefined.
$("li.country").on("click", function(){
var i = $(this).find("i"); //this is where the element is stored
var cc = $(this).attr("data-city") + $(this).attr("data-country-code");
var ul_week = $("#week");
i.toggleClass("no_show"); //works fine here, shows the element
$.ajax({
url: forecast_base_url + cc + units + APPID,
success: function(result){
i.toggleClass("no_show"); //gives an error here
$("#cc_city").html("This forecast for " + result.city.name + ".");
ul_week.html("");
for (var i = 0; i < result.list.length; i++) {
ul_week.append(
"<li>Hello World"+ i +"</li>"
);
}
console.log(result);
},
error: function(err){
console.log(err);
}
});
});
#charlietfl pointed out that the issue is caused by variable hoisting within your ajax callback rather than an issue within jQuery overwriting i.
JavaScript doesn't have block scope (except with ES6 let), so the var declaration within the for loop gets hoisted to the top like so:
$.ajax({
url: forecast_base_url + cc + units + APPID,
success: function(result){
var i = undefined; // variable i is hoisted up
i.toggleClass("no_show"); //gives an error here
$("#cc_city").html("This forecast for " + result.city.name + ".");
ul_week.html("");
for ( i = 0; i < result.list.length; i++) {
ul_week.append(
"<li>Hello World"+ i +"</li>"
);
}
console.log(result);
},

Javascript Issue with the order of printing to screen

I have a problem with my output of this. getYammerJSON contains a line that also appends. The issue is that when I run the script it puts all of the appends that are not inside the function first, and then the contents of the function, is it something to do with ready?
<div id="test"></div>
_
$(function(){
while(n<len){
$('#test').append("<br/><br/>Yammer Group ID: <b>" + groupIDs[n] + "<b/><br/><br/>");
getYammerJSON(page,n)
n++;
}
});
function getData(returnData){
$.each(returnData.users, function(key, value){
if(value.email != undefined){
$('#test').append(value.email + "<br/>");
}
});
}
function getYammerJSON(page,n){
$.get("https://www.yammer.com/api/v1/users/in_group/" + groupIDs[n] + ".json?page=" + page, function (returnData) {
getData(returnData);
if(!returnData.more_available === true){
return false;
}
else {
page++;
getYammerJSON(page,n);
}
});
}
an example (it should separate the emails by group ID, not put at the top) -
Yammer Group ID: 12069
Yammer Group ID: 46371
adetan#test.com
alanild#test.com
alexchi#test.com
alisoc#test.com
alwyn#test.com
...
This is a asynchronous problem. You expect the functions to fire after each other. They don't. The jQuery get is asynchronous. So it fires when the request is ready, not when the function is completed. This causes data to appear in the wrong place. You need to rethink the way #test is being populated.
The solution here will solve the asynchronous problem.
function getYammerJSON(page,n){
if (!$('#yg_'+n)[0]) //test if the div already exists, if not create.
{
$('#test').append("<div id='yg_"+n+"'>Yammer Group ID: <b>" + groupIDs[n] + "<b/></div><br/>");
}
$.get("https://www.yammer.com/api/v1/users/in_group/" + groupIDs[n] + ".json?page=" + page, function (returnData) {
getData(returnData, n);
if(!returnData.more_available === true){
return false;
}
else {
page++;
getYammerJSON(page,n);
}
});
}
function getData(returnData, n){
$.each(returnData.users, function(key, value){
if(value.email != undefined){
$('#yg_'+n).append(value.email + "<br/>");
}
});
Now everytime getYammerJSON is called it will create a new div inside #test if it doesn't already exists. The div is given an ID that refers to the n value. When getData is called it will append the addresses into the correct div always corresponding with the correct group id.
asynchronous forces you to rethink your strategy of data retrieval. It's relying more on callbacks. There are new techniques around the corner. They are mentioned in comments, but they are far from common practice. Till that day you need to apply call backs in order to make asynchronous work without faults.
Learn more here: Asynchronous vs synchronous execution, what does it really mean?

How to make a variable accessible outside a function?

I am fetching data from a json file using $.getJSON to get an id of a character and then use $.getJSON to fetch it from another page using the id that I go previously.
However, in the console it says
https://prod.api.pvp.net/api/lol/eune/v1.2/stats/by-summoner/undefined/summary?api_key=API_KEY_HERE
It should show the id where it says unidentified - /by-summoner/undefined/summary
This is my current script:
var input = "netuetamundis";
var sID;
$(document).ready(function () {
// get json from this page to get the ID of the input
$.getJSON("https://prod.api.pvp.net/api/lol/eune/v1.1/summoner/by-name/" + input + "?api_key=API_KEY_HERE", function (name) {
obj = name;
sID = obj.id;
console.log(sID);
});
$.getJSON("https://prod.api.pvp.net/api/lol/eune/v1.2/stats/by-summoner/" + sID + "/summary?api_key=API_KEY_HERE", function (stats) {
console.log(stats);
});
});
When I googled, it said to declare the variable outside the function, which I did as you can see in the code, however it still doesn't work.
Your variable declarations and their scope are correct. The problem you are facing is that the first AJAX request may take a little bit time to finish. Therefore, the second URL will be filled with the value of sID before the its content has been set. You have to remember that AJAX request are normally asynchronous, i.e. the code execution goes on while the data is being fetched in the background.
You have to nest the requests:
$.getJSON("https://prod.api.pvp.net/api/lol/eune/v1.1/summoner/by-name/"+input+"?api_key=API_KEY_HERE" , function(name){
obj = name;
// sID is only now available!
sID = obj.id;
console.log(sID);
});
Clean up your code!
Put the second request into a function
and let it accept sID as a parameter, so you don't have to declare it globally anymore!
(Global variables are almost always evil!)
Remove sID and obj variables - name.id is sufficient unless you really need the other variables outside the function.
$.getJSON("https://prod.api.pvp.net/api/lol/eune/v1.1/summoner/by-name/"+input+"?api_key=API_KEY_HERE" , function(name){
// We don't need sID or obj here - name.id is sufficient
console.log(name.id);
doSecondRequest(name.id);
});
/// TODO Choose a better name
function doSecondRequest(sID) {
$.getJSON("https://prod.api.pvp.net/api/lol/eune/v1.2/stats/by-summoner/" + sID + "/summary?api_key=API_KEY_HERE", function(stats){
console.log(stats);
});
}
Hapy New Year :)
$.getJSON is an asynchronous request, meaning the code will continue to run even though the request is not yet done. You should trigger the second request when the first one is done, one of the choices you seen already in ComFreek's answer.
Alternatively you could use jQuery's $.when/.then(), similar to this:
var input = "netuetamundis";
var sID;
$(document).ready(function () {
$.when($.getJSON("https://prod.api.pvp.net/api/lol/eune/v1.1/summoner/by-name/" + input + "?api_key=API_KEY_HERE", function () {
obj = name;
sID = obj.id;
console.log(sID);
})).then(function () {
$.getJSON("https://prod.api.pvp.net/api/lol/eune/v1.2/stats/by-summoner/" + sID + "/summary?api_key=API_KEY_HERE", function (stats) {
console.log(stats);
});
});
});
This would be more open for future modification and separates out the responsibility for the first call to know about the second call.
The first call can simply complete and do it's own thing not having to be aware of any other logic you may want to add, leaving the coupling of the logic separated.

Cannot access elements in object returned by Javascript Function in AngularJS

Thanks to #asgoth, I am able to use AngularJS $http service to retrieve stock prices from Yahoo as described here: Cannot read response from AngularJS $resource JSONP get from Yahoo Finance
In the "getHistoricalPrice" function, it puts the price inside an array, which is inside an object. From inside that function, I am able to access the price and write it to console.
The function returns the object to where it is called from. From there, I can successfully write the entire object out to console. However, I cannot access the elements of this object. I tried many different ways, but still cannot access the data in the object. You can see the code at http://jsfiddle.net/curt00/LTazR/2/ or below:
angular.module('app', ['ngResource']);
function AppCtrl($scope, $http, $resource) {
var historical_price = getHistoricalPrice("AAPL", 'start date is hard coded', 'end date is hard coded');
console.log("after calling historical price: ", historical_price); // historical_price is an object and all of the correct data is outputted to console here, but I cannot access its elements directly from Javascript.
for(var key in historical_price) {
console.log("key =",key); // this outputs "key = list"
}
console.log("after calling getHistoricalPrice: ", historical_price.list[0][1]); // Cannot access this as browser console gives error: TypeError: Cannot read property '1' of undefined
console.log("after calling getHistoricalPrice: ", historical_price['list'][0][1]); // Cannot access this as browser console gives error: TypeError: Cannot read property '1' of undefined
console.log("after calling getHistoricalPrice: ", historical_price[0][1]); // Cannot access this as browser console gives error: TypeError: Cannot read property '1' of undefined
function getHistoricalPrice(symbol, start, end) {
var query = 'select * from csv where url=\'http://ichart.yahoo.com/table.csv?s=' + symbol + '&a=' + '11' + '&b=' + '19' + '&c=' + '2012' + '&d=' + '11' + '&e=' + '19' + '&f=' + '2012' + '&g=d&ignore=.csv\'';
var url = 'http://query.yahooapis.com/v1/public/yql?q=' + fixedEncodeURIComponent(query) + '&format=json&callback=JSON_CALLBACK';
var histData = {};
$http.jsonp(url, {timeout: 30000}).success(function(json) {
var list = [];
var result = json.query.results.row;
result.shift(); // remove the header (columns) row
angular.forEach(result, function(row) {
list.push([(new Date(row.col0)).getTime()/1000, parseFloat(row.col4)]);
});
list.sort(function(val1, val2) {
return val1[0] - val2[0];
});
histData.list = list;
console.log('Loaded historical data',histData.list[0][1],', for ' + symbol); // This works and gives the price
});
return histData;
}
var fixedEncodeURIComponent = function(str) {
return encodeURIComponent(str).replace(/[!'()]/g, escape).replace(/\*/g, "%2A");
};
}
​
Any help or suggestions to solve this problem is greatly appreciate!
It's a matter of timing.
In lines 12-14 you are trying to access histData.list before it has been populated. This is because this code is run before the success callback to the $http.jsonp function is executed.
Any code that depends on that callback being completed must be in the callback or in a function called in the callback.
See my answer on https://stackoverflow.com/a/13967709/1916258
A great way to debug the Yahoo api is using the YQL Console: http://developer.yahoo.com/yql/console/
Info about the different posibilities (which stock info) can be found on http://www.gummy-stuff.org/Yahoo-data.htm
Edit: there was still a problem with function fixedEncodeURIComponent. It should encode quotes (") too:
var fixedEncodeURIComponent = function(str) {
return encodeURIComponent(str).replace(/[!'()]/g, escape).replace(/\*/g, "%2A").replace(/\"/g, "%22");
};
BobS is right, you aren't timing things correctly. Also you declared fixedEncodeURIComponent after you had called it. This was resulting in an immediate error when I loaded up the jsfiddle.
While you were passing the callback through to your function correctly, you weren't actually calling it. I stripped out all the post processing of the json as you have some other errors involving the query and just implemented the callback so you can see it working.
After the request is finished and you're still in the success function you need to add
if(typeof(callback) === "function"){
callback();
}
This calls that function you passed in and runs it. Here is a working jsFiddle of it:
http://jsfiddle.net/LTazR/22/
I also updated a new variable i created call output so you can see it changing.
Thanks to everybody for providing suggestions.
I solved the problem by using AngularJS' $scope variable, such as $scope.symbol[user].price. I created this variable before calling the getHistoricalPrice function and then in that function, after the result is returned from $http.jsonp, I put the value into the $scope variable, as such:
$scope.symbol[user].price = row.col4;

Javascript Data Layer Architecture Assistance

I'm making a fairly complex HTML 5 + Javascript game. The client is going to have to download images and data at different points of the game depending on the area they are at. I'm having a huge problem resolving some issues with the Data Layer portion of the Javascript architecture.
The problems I need to solve with the Data Layer:
Data used in the application that becomes outdated needs to be automatically updated whenever calls are made to the server that retrieve fresh data.
Data retrieved from the server should be stored locally to reduce any overhead that would come from requesting the same data twice.
Any portion of the code that needs access to data should be able to retrieve it easily and in a uniform way regardless of whether the data is available locally already.
What I've tried to do to accomplish this is build a data layer that has two main components:
1. The portion of the layer that gives access to the data (through get* methods)
2. The portion of the layer that stores and synchronizes local data with data from the server.
The workflow is as follows:
When the game needs access to some data it calls get* method in the data layer for that data, passing a callback function.
bs.data.getInventory({ teamId: this.refTeam.PartyId, callback: this.inventories.initialize.bind(this.inventories) });
The get* method determines whether the data is already available locally. If so it either returns the data directly (if no callback was specified) or calls the callback function passing it the data.
If the data is not available, it stores the callback method locally (setupListener) and makes a call to the communication object passing the originally requested information along.
getInventory: function (obj) {
if ((obj.teamId && !this.teamInventory[obj.teamId]) || obj.refresh) {
this.setupListener(this.inventoryNotifier, obj);
bs.com.getInventory({ teamId: obj.teamId });
}
else if (typeof (obj.callback) === "function") {
if (obj.teamId) {
obj.callback(this.team[obj.teamId].InventoryList);
}
}
else {
if (obj.teamId) {
return this.team[obj.teamId].InventoryList;
}
}
}
The communication object then makes an ajax call to the server and waits for the data to return.
When the data is returned a call is made to the data layer again asking it to publish the retrieved data.
getInventory: function (obj) {
if (obj.teamId) {
this.doAjaxCall({ orig: obj, url: "/Item/GetTeamEquipment/" + obj.teamId, event: "inventoryRefreshed" });
}
},
doAjaxCall: function (obj) {
var that = this;
if (!this.inprocess[obj.url + obj.data]) {
this.inprocess[obj.url + obj.data] = true;
$.ajax({
type: obj.type || "GET",
contentType: "application/json; charset=utf-8",
dataType: "json",
data: obj.data,
url: obj.url,
async: true,
success: function (data) {
try {
ig.fire(bs.com, obj.event, { data: data, orig: obj.orig });
}
catch (ex) {
// this enables ajaxComplete to fire
ig.log(ex.message + '\n' + ex.stack);
}
finally {
that.inprocess[obj.url + obj.data] = false;
}
},
error: function () { that.inprocess[obj.url + obj.data] = false; }
});
}
}
The data layer then stores all of the data in a local object and finally calls the original callback function, passing it the requested data.
publishInventory: function (data) {
if (!this.inventory) this.inventory = {};
for (var i = 0; i < data.data.length; i++) {
if (this.inventory[data.data[i].Id]) {
this.preservingUpdate(this.inventory[data.data[i].Id], data.data[i]);
}
else {
this.inventory[data.data[i].Id] = data.data[i];
}
}
// if we pulled this inventory for a team, update the team
// with the inventory
if (data.orig.teamId && this.team[data.orig.teamId]) {
this.teamInventory[data.orig.teamId] = true;
this.team[data.orig.teamId].InventoryList = [];
for (var i = 0; i < data.data.length; i++) {
this.team[data.orig.teamId].InventoryList.push(data.data[i]);
}
}
// set up the data we'll notify with
var notifyData = [];
for (var i = 0; i < data.data.length; i++) {
notifyData.push(this.inventory[data.data[i].Id]);
}
ig.fire(this.inventoryNotifier, "refresh", notifyData, null, true);
}
There are several problems with this that bother me constantly. I'll list them in order of most annoying :).
Anytime I have to add a call that goes through this process it takes too much time to do so. (at least an hour)
The amount of jumping and callback passing gets confusing and seems very prone to errors.
The hierarchical way in which I am storing the data is incredibly difficult to synchronize and manage. More on that next.
Regarding issue #3 above, if I have objects in the data layer that are being stored that have a structure that looks like this:
this.Account = {Battles[{ Teams: [{ TeamId: 392, Characters: [{}] }] }]}
this.Teams[392] = {Characters: [{}]}
Because I want to store Teams in a way where I can pass the TeamId to retrieve the data (e.g. return Teams[392];) but I also want to store the teams in relation to the Battles in which they exist (this.Account.Battles[0].Teams[0]); I have a nightmare of a time keeping each instance of the same team fresh and maintaining the same object identity (so I am not actually storing it twice and so that my data will automatically update wherever it is being used which is objective #1 of the data layer).
It just seems so messy and jumbled.
I really appreciate any help.
Thanks
You should consider using jquery's deferred objects.
Example:
var deferredObject = $.Deferred();
$.ajax({
...
success: function(data){
deferredObject.resolve(data);
}
});
return deferredObject;
Now with the deferredObject returned, you can attach callbacks to it like this:
var inventoryDfd = getInventory();
$.when(inventoryDfd).done(function(){
// code that needs data to continue
}
and you're probably less prone to errors. You can even nest deferred objects, or combine them so that a callback isn't called until multiple server calls are downloaded.
+1 for Backbone -- it does some great heavy lifting for you.
Also look at the Memoizer in Douglas Crockford's book Javascript the Good Parts. It's dense, but awesome. I hacked it up to make the memo data store optional, and added more things like the ability to set a value without having to query first -- e.g. to handle data freshness.

Categories

Resources