Update: Solved, it was indeed a scope issue. I got around it by moving the user list code inside the database class and returning the prebuilt list.
Using node.js, I make an asynchronous call to function findUser and I build a list of users from the callback into the variable content. This works fine during the loop (where it says content variable is available) but when the loop exits, the variable is empty. How can I rewrite the code so that the value of variable content is available outside the loop?
exports.listUsers=function(req,res) {
var content=''
isLoggedIn=user.findUser({},function(myuser) {
content = content +'<li>' + myuser.firstname + ' ' + myuser.lastname + "</li>\n";
//here value of content var is available
console.log(content)
})
//here value of content var is empty
console.log(content)
showPage(req,res,{'content':content})
}
If findUser() is asynchronous (which you indicate), then the issue is that findUser() has not yet completed when showPage() is called.
For asynchronous functions, you can only use their results from the success handler function. You can't call an asynchronous function and expect to use it synchronously like your current code.
I don't know exactly what you're trying to accomplish, but this is the general design pattern you would need to use:
exports.listUsers=function(req,res) {
isLoggedIn=user.findUser({},function(myuser) {
var content = '<li>' + myuser.firstname + ' ' + myuser.lastname + "</li>\n";
//here value of content var is available
console.log(content)
showPage(req,res,{'content':content})
});
}
Or, if the callback is being called many times once for each user, you can accumulate the content and call showPage() on the last callback:
exports.listUsers=function(req,res) {
var content = "";
isLoggedIn=user.findUser({},function(myuser) {
content += '<li>' + myuser.firstname + ' ' + myuser.lastname + "</li>\n";
//here value of content var is available
console.log(content)
// devise some logic to know when the last callback is being called
// perhaps based on a user count
if (this is the last user callback) {
showPage(req,res,{'content':content})
}
});
}
Related
I've worked hard to solve a probelm where I can now parse a lot of xml into my js and create an object within the functions that contains all the data from the xml that I want.
What I can't do is get a function to return the object as a result so that I can use it in my wider code.
I've logged to console each exit point of each nested function and worked out where it gets stuck but can't work out how to get the data any further...
Could you have a look and see why it is stuck at the point where I've commented. Many thanks if you choose to help a little!
//now accessing an xml in a file structure
const path = nativeRequire('path')
const fs = nativeRequire('fs');
const xml2js = nativeRequire('xml2js');
const glob = nativeRequire('glob');
//const files = glob.sync(path.join(__dirname, '../../Steinberg/Cubase/Expression Maps/Native Instruments/Symphony Series/*.expressionmap'));
const mapFiles = glob.sync(path.join(__dirname, '../ExpressionMaps/*.expressionmap'));
var artArr = []
var artColor = []
var trackID = '117-4' //test value - this will come from the TRACK ID sysex stuff
buildMap() //I need Build Map to populate Objects artArr and artColor and return them so I can use them in other places in the code
function buildMap() {
mapFiles.forEach(function (mapFile) { //sends mapFile into the parser
//debug
//console.log('Selected Tracks ' + mapFile); // List out all the expression maps
//console.log('mapFiles length' + mapFiles.length) // gets the length of list of the files - number of expression maps in directory
//debug
var parser = new xml2js.Parser({ explicitArray: false, mergeAttrs: true }); // 'mergeAttrs: true' was essential
if (mapFile.includes(trackID)) {
console.log('Selected Map: ' + mapFile);
fs.readFile(mapFile, function (err, data) {
parser.parseString(data, function (err, result) {
let art = result.InstrumentMap.member[1].list.obj;
for (let i = 0, len = art.length; i < len; i++) {
console.log('Articulation at poition: ' + i + ' ' + art[i].member[1].string.value + '; ' + 'Color Value: ' + art[i].int.value) // articulatins and color value
artArr[i] = art[i].member[1].string.value
artColor[i] = art[i].int.value
}
});
//THE BELOW LOGS TO CONSOLE OK
console.log('CHECK LOG artArr[0] ' + artArr[0])
console.log('CHECK LOG artArr[1] ' + artArr[1])
console.log('CHECK LOG artArr[2] ' + artArr[2])
console.log('CHECK LOG artArr[3] ' + artArr[3])
console.log('CHECK LOG artArr[4] ' + artArr[4])
console.log('CHECK LOG artArr[5] ' + artArr[5])
console.log('CHECK LOG artArr[6] ' + artArr[6])
console.log('CHECK LOG artArr[7] ' + artArr[7])
});
//THE BELOW LOGS TO CONSOLE as UNDEFINED artArr and artColor get stuck here
console.log('CHECK LOG 2 artArr[0] ' + artArr[0])
console.log('CHECK LOG 2 artArr[1] ' + artArr[1])
console.log('CHECK LOG 2 artArr[2] ' + artArr[2])
console.log('CHECK LOG 2 artArr[3] ' + artArr[3])
console.log('CHECK LOG 2 artArr[4] ' + artArr[4])
console.log('CHECK LOG 2 artArr[5] ' + artArr[5])
console.log('CHECK LOG 2 artArr[6] ' + artArr[6])
console.log('CHECK LOG 2 artArr[7] ' + artArr[7])
}
});
}
console.log('Test log a value from artArr ' + artArr[3]) //Needs to return a value but currently returns 'undefined'
console.log('Test log a value from artColor ' + artColor[3]) //Needs to return a value but currently returns 'undefined'
This type of code is a lesson in asynchronous programming. Functions like fs.readFile() and parser.parseString() are asynchronous. That means that when you call them, they initiate their operation and then IMMEDIATELY return and the rest of your code after them runs. This seriously messes up sequential programming unless you write the code correctly for asynchronous programming. It is ONE of the main things to learn when you start coding in nodejs as lots and lots of things are asynchronous in nodejs.
Fortunately, promises can make it easier to write proper asynchronous code, but you still must be aware of it. And, even when using promises, you will still have to be aware of how/when you can use asynchronously retrieved results.
Here's a reworked version of your code:
//now accessing an xml in a file structure
const path = nativeRequire('path');
const fs = nativeRequire('fs');
const xml2js = nativeRequire('xml2js');
const glob = nativeRequire('glob');
const mapFiles = glob.sync(path.join(__dirname, '../ExpressionMaps/*.expressionmap'));
const trackID = '117-4' //test value - this will come from the TRACK ID sysex stuff
async function buildMap() {
const artArr = [];
const artColor = [];
for (const mapFile of mapFiles) {
if (mapFile.includes(trackID)) {
const parser = new xml2js.Parser({
explicitArray: false,
mergeAttrs: true
}); // 'mergeAttrs: true' was essential
console.log('Selected Map: ' + mapFile);
const data = await fs.promises.readFile(mapFile);
const result = await parser.parseStringPromise(data);
const art = result.InstrumentMap.member[1].list.obj;
for (let i = 0, len = art.length; i < len; i++) {
console.log('Articulation at poition: ' + i + ' ' + art[i].member[1].string.value + '; ' +
'Color Value: ' + art[i].int.value) // articulatins and color value
artArr[i] = art[i].member[1].string.value
artColor[i] = art[i].int.value
}
// once we've run this inner for loop once, we can't
// run it again because it will overwrite values in
// artArr and artColor, so we break the outer for loop
break;
}
}
return { artArr, artColor };
}
buildMap().then(result => {
// use your results here
console.log(result);
}).catch(err => {
console.log(err);
});
Structural changes to this code:
This only uses asynchronous operations that return a promise so we can use await with it to achieve proper sequencing of asynchronous operations in a loop.
.forEach() is not promise-aware or asynchronous-friendly so it is pretty much obsolete in modern Javascript program. Use a regular for loop instead, most often a for/of loop which is async/await friendly.
Switch to the promise interfaces for both .readFile() and .parseString() so we can use await and make the loop sequence properly.
Asynchronously retrieved results should not be stuffed into higher scoped variables and then used elsewhere because the code elsewhere will NEVER know when those variables actually have their proper values thus why you see undefined a lot as your other code is trying to use the values BEFORE they have been filled into the result. Instead, make the results be the resolved value of your returned promise.
The caller then must use await or .then() to get access to your asynchronously retrieved results.
The second for loop in this code sets the values of artArr[i] and artColor[i]. This assumes that this loop never gets executed more than once (you never get more than one mapFile that .includes(trackID) because if it did, it would be overwriting the prior loop's values in artArr and artColor. To avoid that ever happening, I've forced the loop to only run once by adding a break; to stop the outer loop once, the inner loop has run once. If you intend to run this inner loop more than once, then you need to redesign how you assign to artArr and artColor so they they append to the arrays rather than assign/overwrite previous values.
Other changes:
Don't use var any more. Use const if you can or let if you need to modify the contents of the variable. var should be considered obsolete.
I'm sorry, this definitely has been answered before. I especially know about this thread here:
How do I return the response from an asynchronous call?
But to be honest, I'm still lost. I simply do not understand how to call a function asynchronously and include the return of this function in the response to the client.
I have a rather simple route (in routes.js) which shall provide content to a web page. A specific element of the site is a list of rfid tags. This list is populated from a function, which, of course I'd like and must call asynchron.
I'm using pug (formerly known as jade) as the template render engine and my tags.pug looks like this:
extends index.pug
block content
h2!= subheadline
p!= messagetext
p!= bodyContent
See the last p!=bodyContent? This element shall be the list of tags.
Below is a code snippet from my routes.js with the app.get('/tags') in which this site shall be returned:
// get the listing of all stored rfid tags
app.get("/tags", function(req, res) {
res.render('tags', {
title: 'RFID Tag Startseite',
headline: 'RFID Tag Startseite',
subheadline: 'Verfügbare Tags',
messagetext: 'Bitte ein RFID Tag auswählen, um mehr Daten angezeigt zu bekommen',
bodyContent: tagController.getTagList(app, function(tagController))
});
})
Now, as you can see, I'm trying to call the function (getTagList) at the position bodyContent from a different module (tagController) and this shall provide the list of tags.
Following you can see the code of my getTagList() function:
var getTagList = function(app, result){
// get global app variables
var DEBUG = app.get('DEBUG');
var svrAddr = app.get('svrAddr');
var rfidTagDir = app.get('rfidTagDir');
var responseContent = '';
fs.readdir(rfidTagDir, function(err, items) {
responseContent = "{\'response\': \'info\', \'message\': \'list of all stored rfid tags\', \'tags\': ["
for (i in items) {
var tag = items[i].toString().substring(0,items[i].indexOf('.'));
responseContent += "{\'tag\': \'" + tag + "\', \'endpoint\': \'"+svrAddr+"/tags/tag/" + tag + "\', \'file\': \'"+items[i]+"\'}"
if (i<items.length-1) responseContent += ",";
}
responseContent += "]}"
if (DEBUG) console.log(responseContent);
result = responseContent;
return result;
)};
My problem is, that it doesn't work. The code above gives the error:
SyntaxError: Unexpected token )
For the last of the two ). If I try to populate a variable (say tagContent or whatever) prior to the res.send and include that variable to the response, I simply do not see the result of the function call. In the console.log, however, I can see the function being executed and the list of tags generated.
Can someone please, please, please tell me, how to call my function getTagList from module tagController from within my routes.js so that the content is displayed???
Kind regards,
Christian
So the problem is that getTagList will execute an async operation no matter what you are trying to return its not guaranteed that its correct ... so to be able to handle this you pass a callback function to getTagList which will be called inside the callback function of the async fs.readdir this is where you are 100% sure you have the correct result expected.
So below is the updated getTagList function
var getTagList = function(app, callback) {
// get global app variables
var DEBUG = app.get('DEBUG');
var svrAddr = app.get('svrAddr');
var rfidTagDir = app.get('rfidTagDir');
var responseContent = '';
fs.readdir(rfidTagDir, function(err, items) {
if (err) {
callback(err);
} else {
responseContent = "{\'response\': \'info\', \'message\': \'list of all stored rfid tags\', \'tags\': ["
for (i in items) {
var tag = items[i].toString().substring(0, items[i].indexOf('.'));
responseContent += "{\'tag\': \'" + tag + "\', \'endpoint\': \'" + svrAddr + "/tags/tag/" + tag + "\', \'file\': \'" + items[i] + "\'}"
if (i < items.length - 1) responseContent += ",";
}
responseContent += "]}"
if (DEBUG) {
console.log(responseContent);
}
//Here we are 100% sure we have the correct expected result so we call the callback function with correct data `responseContent`
callback(null, responseContent);
}
)
};
}
Also you update your route to send the request only when data is available which is inside the callback function of getTagList, check code below
app.get("/tags", function(req, res) {
tagController.getTagList(app, function(err, result) {
if (err) {
console.log(err);
//Handle error response
} else {
res.render('tags', {
title: 'RFID Tag Startseite',
headline: 'RFID Tag Startseite',
subheadline: 'Verfügbare Tags',
messagetext: 'Bitte ein RFID Tag auswählen, um mehr Daten angezeigt zu bekommen',
bodyContent: result
});
}
});
})
You can also notice how the callback functions are implemented in node ... we pass any error to the first argument, and the result to the second argument .. to have a better maintainable code.
Finally you can see the same concept is actually used in the fs.readdir that already exist in your code you will see that you are using a callback function to be able to access items correctly.
I'm trying to add a listview element based on my QR code. So when I scan one QR code, i take the first value of the array, and add a listview element with the text of the first value, and use a counter and so on. However, it doesn't increment my counter. All i get is that the function appendToList() gets terminated once listview refreshes. Count doesnt increment and ArrayIndex remains empty. How can I get the counter to work? And the array as well? The function doesn't execute all the way.
function scan() {
cordova.plugins.barcodeScanner.scan(
function(result) {
$("#nullExhibition").remove();
resetData(); // Removes the empty exhibition text
if (!result.cancelled) {
if (result.format == "QR_CODE") {
var value = result.text;
if(!localStorage.getItem("LocalData"))localStorage.setItem("LocalData","[]")
data = localStorage.getItem("LocalData");
//alert(data);
data = JSON.parse(data);
data.push(value);
localStorage.setItem("LocalData", JSON.stringify(data));
//location.reload();
alert(count);
appendToList();
// location.hash = '#Vote';
}
}
},
function(error) {
alert("Scanning failed: " + error);
}
);
}
//JJJ Function: creates table which will be refreshed in the homePage //CW Create the listview if not created
function appendToList() {
$("#list").append("<li id='" + count + "' style='font-family: 'Quicksand', sans-serif !important;' class='ui-content'><a href='javascript:pageGenerator()'>" + data[0] + "</a></li>");
$("#list").listview("refresh");
ArrayIndex.append(data[0]);
count ++;
}
function resetData(){
localStorage.removeItem("LocalData");
}
Where did you declare "count" in the global scope? You reference it in two separate functions, but are neither passing the value between them, nor made it a global var so that both functions reference the same variable. They way you have it, you reinitialize that var each time that you call the appendToList() function.
Add this to the top of your document (outside of the function closures):
var count = 0;
So,I am trying to use the twitch API:
https://codepen.io/sterg/pen/yJmzrN
If you check my codepen page you'll see that each time I refresh the page the status order changes and I can't figure out why is this happening.
Here is my javascript:
$(document).ready(function(){
var ur="";
var tw=["freecodecamp","nightblue3","imaqtpie","bunnyfufuu","mushisgosu","tsm_dyrus","esl_sc2"];
var j=0;
for(var i=0;i<tw.length;i++){
ur="https://api.twitch.tv/kraken/streams/"+tw[i];
$.getJSON(ur,function(json) {
$(".tst").append(JSON.stringify(json));
$(".name").append("<li> "+tw[j]+"<p>"+""+"</p></li>");
if(json.stream==null){
$(".stat").append("<li>"+"Offline"+"</li>");
}
else{
$(".stat").append("<li>"+json.stream.game+"</li>");
}
j++;
})
}
});
$.getJSON() works asynchronously. The JSON won't be returned until the results come back. The API can return in different orders than the requests were made, so you have to handle this.
One way to do this is use the promise API, along with $.when() to bundle up all requests as one big promise, which will succeed or fail as one whole block. This also ensures that the response data is returned to your code in the expected order.
Try this:
var channelIds = ['freecodecamp', 'nightblue3', 'imaqtpie', 'bunnyfufuu', 'mushisgosu', 'tsm_dyrus', 'esl_sc2'];
$(function () {
$.when.apply(
$,
$.map(channelIds, function (channelId) {
return $.getJSON(
'https://api.twitch.tv/kraken/streams/' + encodeURIComponent(channelId)
).then(function (res) {
return {
channelId: channelId,
stream: res.stream
}
});
})
).then(function () {
console.log(arguments);
var $playersBody = $('table.players tbody');
$.each(arguments, function (index, data) {
$playersBody.append(
$('<tr>').append([
$('<td>'),
$('<td>').append(
$('<a>')
.text(data.channelId)
.attr('href', 'https://www.twitch.tv/' + encodeURIComponent(data.channelId))
),
$('<td>').text(data.stream ? data.stream.game : 'Offline')
])
)
})
})
});
https://codepen.io/anon/pen/KrOxwo
Here, I'm using $.when.apply() to use $.when with an array, rather than list of parameters. Next, I'm using $.map() to convert the array of channel IDs into an array of promises for each ID. After that, I have a simple helper function with handles the normal response (res), pulls out the relevant stream data, while attaching the channelId for use later on. (Without this, we would have to go back to the original array to get the ID. You can do this, but in my opinion, that isn't the best practice. I'd much prefer to keep the data with the response so that later refactoring is less likely to break something. This is a matter of preference.)
Next, I have a .then() handler which takes all of the data and loops through them. This data is returned as arguments to the function, so I simply use $.each() to iterate over each argument rather than having to name them out.
I made some changes in how I'm handling the HTML as well. You'll note that I'm using $.text() and $.attr() to set the dynamic values. This ensures that your HTML is valid (as you're not really using HTML for the dynamic bit at all). Otherwise, someone might have the username of <script src="somethingEvil.js"></script> and it'd run on your page. This avoids that problem entirely.
It looks like you're appending the "Display Name" in the same order every time you refresh, by using the j counter variable.
However, you're appending the "Status" as each request returns. Since these HTTP requests are asynchronous, the order in which they are appended to the document will vary each time you reload the page.
If you want the statuses to remain in the same order (matching the order of the Display Names), you'll need to store the response data from each API call as they return, and order it yourself before appending it to the body.
At first, I changed the last else condition (the one that prints out the streamed game) as $(".stat").append("<li>"+jtw[j]+": "+json.stream.game+"</li>"); - it was identical in meaning to what you tried to achieve, yet produced the same error.
There's a discrepancy in the list you've created and the data you receive. They are not directly associated.
It is a preferred way to use $(".stat").append("<li>"+json.stream._links.self+": "+json.stream.game+"</li>");, you may even get the name of the user with regex or substr in the worst case.
As long as you don't run separate loops for uploading the columns "DisplayName" and "Status", you might even be able to separate them, in case you do not desire to write them into the same line, as my example does.
Whatever way you're choosing, in the end, the problem is that the "Status" column's order of uploading is not identical to the one you're doing in "Status Name".
This code will not preserve the order, but will preserve which array entry is being processed
$(document).ready(function() {
var ur = "";
var tw = ["freecodecamp", "nightblue3", "imaqtpie", "bunnyfufuu", "mushisgosu", "tsm_dyrus", "esl_sc2"];
for (var i = 0; i < tw.length; i++) {
ur = "https://api.twitch.tv/kraken/streams/" + tw[i];
(function(j) {
$.getJSON(ur, function(json) {
$(".tst").append(JSON.stringify(json));
$(".name").append("<li> " + tw[j] + "<p>" + "" + "</p></li>");
if (json.stream == null) {
$(".stat").append("<li>" + "Offline" + "</li>");
} else {
$(".stat").append("<li>" + json.stream.game + "</li>");
}
})
}(i));
}
});
This code will preserve the order fully - the layout needs tweaking though
$(document).ready(function() {
var ur = "";
var tw = ["freecodecamp", "nightblue3", "imaqtpie", "bunnyfufuu", "mushisgosu", "tsm_dyrus", "esl_sc2"];
for (var i = 0; i < tw.length; i++) {
ur = "https://api.twitch.tv/kraken/streams/" + tw[i];
(function(j) {
var name = $(".name").append("<li> " + tw[j] + "<p>" + "" + "</p></li>");
var stat = $(".stat").append("<li></li>")[0].lastElementChild;
console.log(stat);
$.getJSON(ur, function(json) {
$(".tst").append(JSON.stringify(json));
if (json.stream == null) {
$(stat).text("Offline");
} else {
$(stat).text(json.stream.game);
}
}).then(function(e) {
console.log(e);
}, function(e) {
console.error(e);
});
}(i));
}
});
So i wrote this piece of code in javascript :
// Additional initialization code such as adding Event Listeners goes here
FB.api('593959083958735/albums', function(response) {
if(!response || response.error) {
// render error
alert("Noo!!");
} else {
// render photos
for(i=0; i<response.data.length; i++){
var albumName = response.data[i].name;
var albumCover = response.data[i].cover_photo;
var albumId = response.data[i].id;
console.log(albumName);
FB.api( albumCover, function(response) {
if(!response || response.error) {
// render error
alert("Noo!!");
} else {
// render photos
$("ul").append('<li>'+
'<a href="testFile.HTML" data-transition="slidedown">'+
'<img src= "' + response.picture + '" />'+
'<h2>' + albumName + '</h2>'+
'<p> Test Paragraph</p>'+
'</a>'+
'</li>')
.listview('refresh');
}
});
}
}
});
I am using facebooks javascript API , to import the photo albums of a facebook page to my jquery mobile page and put them in a listView. As you see i create the listView dynamically. The listView has as thumbnail the albums coverPhoto and headline , albums name.
However something is horribly wrong here. The result of the code above can be seen here
The thumbnails are all correct but the album names are wrong. In every cell i get the last album name. When i console.log(albumName) in my code as you see , i get all the names correct. But inside the second call to FB.api the "albumName" variable holds only the last album name.
Any idea what is going on here? Its literally driving me nuts...
Looks like you have the famous i for loop problem where you only have a reference to i.
for (var i = 0; i < response.data.length; i++) { //added the var here
(function (i) { //created a function
var albumName = response.data[i].name;
var albumCover = response.data[i].cover_photo;
var albumId = response.data[i].id;
console.log(albumName);
FB.api(albumCover, function (response) {
if (!response || response.error) {
// render error
alert("Noo!!");
} else {
// render photos
$("ul").append('<li>' +
'<a href="testFile.HTML" data-transition="slidedown">' +
'<img src= "' + response.picture + '" />' +
'<h2>' + albumName + '</h2>' +
'<p> Test Paragraph</p>' +
'</a>' +
'</li>')
.listview('refresh');
}
});
})(i); //call the function with i
}
It's actually not the i that's the problem, but rather albumName.
Most JavaScript engines are very forgiving and allow you to declare the same variable with the var keyword multiple times. That's what you're doing here. It's important to note that the for keyword does not introduce a new scope; so when you (re-)declare each variable within the loop (albumName, albumCover, and albumId) you're overwriting the value previously stored in the same variable.
This comes back to bite you because the callback you pass to FB.api closes over these local variables and (clearly) executes asynchronously. By the time the callback is actually run, the for loop has completed and these variables are all set to their last value.
The exception, of course, is albumCover, as you already observed (you said the thumbnails were all correct). That's because you pass this value to FB.api synchronously; i.e., you pass it within the loop, so it isn't closed over.
For a better understanding of closures in JavaScript, see How do JavaScript closures work?
To be clear: epascarello's answer should solve the problem for you. Wrapping up your logic in an anonymous function does introduce a new scope (a useful trick that can be used in a variety of scenarios), and so the value of albumName, etc. getting closed over is different for each callback. I just wanted to point out that it wasn't the i per se causing the problem (since your callback doesn't close over i at all).