Optional Promise in Coffeescript - javascript

I am building a tree of nodes but when I run across a certain value I need to make a rest request to get the child nodes and then build those as well. The below works when I have the values hard coded in for the connector.get request, but when I turn it into a promise the function returns before the promise returns. How can I refactor this to delay returning until all the promises resolve?
buildDom: (viewObj) ->
treeElm = document.createElement('div')
rowContainer = document.createElement('ul')
view = viewObj.data.options[0]
i = 0
while i < view.rows.length
rowElm = document.createElement('li')
rowElm.innerHTML = 'Row ' + i
row = view.rows[i]
cellContainer = document.createElement('ul')
j = 0
while j < row.cells.length
cell = row.cells[j]
cellElm = document.createElement('li')
if cell.view
cellElm.innerHTML = 'View: ' + cell.view
if !cell.view?
connector.get(cell.view).then (res)->
cellElm.appendChild buildDom(res)
else if cell.fieldId
cellElm.innerHTML = 'Field: ' + cell.fieldId
if cell.displayType != 'NONE'
cellContainer.appendChild cellElm
j++
rowElm.appendChild cellContainer
rowContainer.appendChild rowElm
i++
treeElm.appendChild rowContainer

cellElm changes with each iteration of the loop, and so may change during the time that connector.get is running, which would lead to appending to the wrong element. Is that what you're seeing? The fix would be to pass cellElm to a function so there is a stored copy of it that the connector.get call can use. I've inlined such a function below.
if !cell.view?
((element) ->
connector.get(cell.view).then (res)->
element.appendChild buildDom(res))(cellElm)

Related

React - For Loop conditional render - not working and infinite trigger

I am trying to dynamically multiple an item in my render based on a variable kind of like this
<div>
<Multiple
multiple={props.multiple}
base1={props.base1}
exp1={props.exp1}
/>
</div>
const Multiple = (props) => {
let result = "";
let wtf = "";
console.log("test"); // gets triggered
for(let i = 0; i < props.multiple; i++){
console.log("i: "+i); // gets triggered
result.concat("{props.base1} +"); // this doesn't work for some reason
wtf = i; // gets triggered
}
console.log("result: "+result); // result is blank
console.log("wtf:" +wtf);
return <span>{result}</span>;
}
PROBLEM 1: Even though I am entering the for-loop, my result is not being changed and i don't understand why.
Also since I cant get it to work yet, I wanted to ask: If i do it this way, where I am concatenating {props.base1} as a string, when i return it in the render, will it show up as "{props.base1}" or will it render as the variable value?
Here is an example as to what it should look like:
base1 = abc
multiple = 2
resulting render should look like:
abc + abc +
Will concatenating my prop into a string before rendering result it in looking like this instead of the above block?
{props.base1} + {props.base1} +
PROBLEM 2: EDIT ALSO, for some reason everything in the <Multiple> component is infinitely triggering, which I also do not understand why it is happening
You are using concat, which doesn't update the original string, it creates a new string instead. What you could do is either
let result = '';
for(let i = 0; i < props.multiple; i++) {
console.log("i: "+i); // gets triggered
result += `${props.base1} `;
wtf = i; // gets triggered
}
console.log(result);
As far as the infinite loop problem goes, what actually is props.muitlple? Is it an array or a string? If so, you should change your loop to
for(let i = 0; i < props.multiple.length; i++)
Edit: if props.multiple is a number, i < props.multiple should work, you should log the value in your component and check once.
The result string is not being properly appended to
const Multiple = (props) => {
let result = "";
let wtf = "";
for(let i = 0; i < props.multiple; i++){
result += props.base1.toString() + "+"
}
console.log("result: "+result);
return <span>{result}</span>;
}
For the infinite loop I would check it's values before entering the loop to make sure your bounds are properly set.
// add this line before for loop
console.log(props.multiple)
a. change to result.concat('${props.base1} + '); (backtics!!)
b. i think that maybe there is a problem with the props you pass to <Multiple ... >. check again their value, maybe log their value.

getElementById in a for-loop only displays the first item

Relevant HTML portion
<nav>
<div class="create_button">+ Create KPI</div>
<div id="items"></div>
</nav>
Relevant JS portion
VSS.getService(VSS.ServiceIds.ExtensionData).then(function(dataService) {
// Get all document under the collection
dataService.getDocuments("MyCollection").then(function(docs) {
items = docs
for(var i = 0; i < docs.length; i++) {
console.log('doclen', docs.length)
console.log(items[i].name)
document.getElementById("items").innerHTML = "KPI Name : " + items[i].name;
}
});
});
My JS code fetches all data that I have in my VSTS storage. The docs contains an object with all items. It returns correctly and items[i].name contains the correct value that I want to display.
But this one just displays the first item in my <div id="items"> and not the rest.
Is this the right usage?
How can I fix it?
Here are 2 versions that show different ways to do this. Pay attention to the changes in the code that use es6 style.
VSS.getService(VSS.ServiceIds.ExtensionData).then((dataService) => {
dataService.getDocuments('MyCollection').then((docs) => {
// keep a reference to the element instead of searching for it in each loop.
const itemsDiv = document.getElementById('items');
const contents = [];
for (let i = 0; i < docs.length; i++) {
// using template strings here to show you another way of working with strings in es6
contents.push(
`<div>KPI Name : ${docs[i].name}</div>`
)
}
// finally update the target element one time with your contents.
// The new line character isn't required, can just use '', but this might be easier to read for you
itemsDiv.innerHTML = contents.join('\n');
});
});
More compact version using the map functional array method. But note that this is actually slightly slower than doing a normal for loop because its executing a function on each iteration.
VSS.getService(VSS.ServiceIds.ExtensionData).then((dataService) => {
dataService.getDocuments('MyCollection').then((docs) => {
// much more compact version using map. Note that while this is more compact,
// its slower than the for loop we did in the previous example
document.getElementById('items').innerHTML = docs.map((item) => `<div>KPI Name : ${docs[i].name}</div>`).join('\n');
});
});
The issues occours because you are setting the innerHTML of the items div on each iteration in the loop; meaning that the values will be overwritten every time and only display the last value being set in the loop.
One easy solution is to append a new element instead when you set the values to the items div
for(var i = 0; i < docs.length; i++) {
console.log('doclen', docs.length)
console.log(items[i].name)
var newElement = document.createElement('div');
newElement.innerHTML = "KPI Name : " + items[i].name;
document.getElementById("items").appendChild(newElement);
}

Javascript: increment counter variable inside loop

I am using Selenium and nodejs to iterate through a html table and match a string from an array to the string in the table cell.
My array might be ["Name", "Address1", "Address2", "Address3",...] and the value in tr[1] will try to match with Arr[0], tr[2] with Arr[1] etc.
The html table will have a row for each item in the array, but if there is no data for, say, Address2 then that will not appear.
In that case, tr[3] will find that it cannot match with Arr[3]. Instead of moving to tr[4], I want to see if tr[3] matches with Arr[4], then Arr[5] etc. The items in the table will always be in the same order as the items in the array, so I have no need for any array items "unmatched".
I've posted the whole function in case it is relevant, but the issue seems very simply to be that I cannot get "i = i - 1" to carry the new value into the next loop iteration.
I have proved that I get into the Else section, and that the value of i - 1 is as I would expect at that point.
var retrieveData = function retrieveData(Arr){
j = 0
driver.findElements(webdriver.By.xpath("//*[#class='table-container']")).then (function(rows){
rowCount = rows.length;
console.log("Row count = " + rowCount);
}).then (function(fn){
for (i = 1;i < rowCount + 1; i++){
(function(i){
var element = driver.findElement(webdriver.By.xpath("//div[#class='table-container']["+i+"]/div/strong/a"));
element.getText().then(function(Type){
var typefromArray = String(Arr[j].split(':')[0]);
if (Type == typefromArray){
// Do something
j = j + 1;
} else {
// Do something
i = i - 1 // My problem looks to be here, but may be in the transfer of this back up to the beginning of the loop
j = j + 1;
}
});
})(i);
};
});
};
module.exports.retrieveData = retrieveData;
You are using an IIFE in your for-loop to which you pass the index.
That looks like it was designed to prevent modification of i!
When you do
i = i - 1
at the end of your function, it has absolutely no effect as it only affects the i inside your function.
Here's a good article about variable scopes in JavaScript.
But if you still want to modify i, then one option would be to simply have it be returned by the anonymous function and assigned to the external i variable:
.then (function(fn){
for (i = 1;i < rowCount + 1; i++){
i = (function(i){
var element = driver.findElement(webdriver.By.xpath("//div[#class='table-container']["+i+"]/div/strong/a"));
element.getText().then(function(Type){
var typefromArray = String(Arr[j].split(':')[0]);
if (Type == typefromArray){
// Do something
j = j + 1;
} else {
// Do something
i = i - 1 // My problem looks to be here, but may be in the transfer of this back up to the beginning of the loop
j = j + 1;
}
});
return i;
})(i);
};
That is the solution to the question you asked.
But I'm pretty sure there will be other problems in your code, as you are using a synchronous loop, yet updating the counter in an asynchronously-called function (the function passed to element.getText().then).
I'd suggest you either study a bit about how to handle asynchronous code in JavaScript (you're using Promises right now) or open a more general question about the bigger problem you are trying to solve as there will be some more design hurdles to overcome.
Here's an example of the kind of patterns you may have to use to handle multiple Promises (not meant to be copy-pasted, used ES2015 for brevity):
.then(function(fn) {
Promise.all(
Array(rowCount).fill(true).map((_, i) => i + 1) // => [1..rowCount]
.map(i => {
const element = driver.findElement(webdriver.By.xpath("//div[#class='table-container']["+i+"]/div/strong/a"));
return element.getText();
})
// Here we have an array of Promises that we pass to Promise.all
// so we can wait until all promises are resolved before executing
// the following .then
).then(types => types.filter(type => type === typefromArray)
.map(type => { /* doSomething */ });
});

Using querySelectorAll to get ALL elements with that class name, not only the first

I've ditched jquery about 9(ish) months ago and needed a selector engine (without all the hassle and don't mind ie<7 support) so i made a simplified version of document.querySelectorAll by creating this function:
// "qsa" stands for: "querySelectorAll"
window.qsa = function (el) {
var result = document.querySelectorAll(el)[0];
return result;
};
This works perfectly fine for 95% of the time but I've had this problem for a while now and i have researched mdn, w3c, SO and not to forget Google :) but have not yet found the answer as to why I only get the first element with the requested class.
And I know that only the first element being returned is caused by the "[0]" at the end, but the function won't work if I remove it so I've tried to make a for loop with an index variable that increases in value depending on the length of elements with that class like this:
window.qsa = function (el) {
var result, el = document.querySelectorAll(el);
for(var i = 0; i < el.length; ++i) {
result = el[i];
}
return result;
};
Again that did not work so I tried a while loop like this:
window.qsa = function (el) {
var result, i = 0, el = document.querySelectorAll(el);
while(i < el.length) {
i++;
}
result = el[i];
return result;
};
By now I'm starting to wonder if anything works? and I'm getting very frustrated with document.querySelectorAll...
But my stubborn inner-self keeps going and I keep on failing (tiering cycle) so I know that now is REALLY the time to ask these questions :
Why is it only returning the first element with that class and not all of them?
Why does my for loop fail?
Why does my while loop fail?
And thank you because any / all help is much appreciated.
Why is it only returning the first element with that class and not all of them?
Because you explicitly get the first element off the results and return that.
Why does my for loop fail?
Because you overwrite result with a new value each time you go around the end of loop. Then you return the last thing you get.
Why does my while loop fail?
The same reason.
If you want all the elements, then you just get the result of running the function:
return document.querySelectorAll(el)
That will give you a NodeList object containing all the elements.
Now that does what you say you want, I'm going to speculate about what your real problem is (i.e. why you think it doesn't work).
You haven't shown us what you do with the result of running that function, but my guess is that you are trying to treat it like an element.
It isn't an element. It is a NodeList, which is like an array.
If you wanted to, for instance, change the background colour of an element you could do this:
element.style.backgroundColor = "red";
If you want to change the background colour of every element in a NodeList, then you have to change the background colour of each one in turn: with a loop.
for (var i = 0; i < node_list.length; i++) {
var element = node_list[i];
element.style.backgroundColor = "red";
}
You are returning a single element. You can return the array. If you want to be able to act on all elements at once, jQuery style, you can pass a callback into your function;
window.qsa = function(query, callback) {
var els = document.querySelectorAll(query);
if (typeof callback == 'function') {
for (var i = 0; i < els.length; ++i) {
callback.call(els[i], els[i], i);
}
}
return els;
};
qsa('button.change-all', function(btn) {
// You can reference the element using the first parameter
btn.addEventListener('click', function(){
qsa('p', function(p, index){
// Or you can reference the element using `this`
this.innerHTML = 'Changed ' + index;
});
});
});
qsa('button.change-second', function(btn) {
btn.addEventListener('click', function(){
var second = qsa('p')[1];
second.innerHTML = 'Changed just the second one';
});
});
<p>One</p>
<p>Two</p>
<p>Three</p>
<button class='change-all'>Change Paragraphs</button>
<button class='change-second'>Change Second Paragraph</button>
Then you can call either use the callback
qsa('P', function(){
this.innerHTML = 'test';
});
Or you can use the array that is returned
var pList = qsa('p');
var p1 = pList[0];
This loop
for(var i = 0; i < el.length; ++i) {
result = el[i];
}
overwrites your result variable every time. That's why you always get only one element.
You can use the result outside though, and iterate through it. Kinda like
var result = window.qsa(el)
for(var i = 0; i < result.length; ++i) {
var workOn = result[i];
// Do something with workOn
}

Variable Becomes Undefined in Last Iteration of Loop

I have a application where I am getting route info from one location to another, possibly between multiple locations. In my function, I'm looping over checked locations and writing out the directions to the page with jQuery. On the last iteration of the loop the 'myRoute' variable becomes undefined. So, if I have three locations, it bombs on the third, but if I use those same three and add a fourth, the first three works and the fourth bombs. I traced the behavior in Firebug, and while the myRoute variable does get filled properly, as soon as it moves to the next line, it is all of a sudden undefined. I removed instances of myRoute and replaced with directionResult.routes[0].legs[i] but still got the undefined error. What's going on here?
function setInstructions(directionResult, idname, start) {
//clean tour list
$('#tours_list .tour').remove();
$('#routeTitle').show();
var checkboxArray = $(".selector.Favs" + idname).find("li");
var idx = 0;
var linkMap = $('#routeTitle').find('.link-map')[0];
linkMap.href = __mapLink+'saddr=' + start;
var firstStop = true;
//iterate selected properties
for (var i = 0; i < checkboxArray.length; i++) {
var curChk = checkboxArray[i];
if ($(curChk).hasClass('active')) {
//get steps
var myRoute = directionResult.routes[0].legs[i]; //this is what becomes undfined
var title = $('<div>').addClass('mileage').append($('<p>').append($('<strong>').html(myRoute.distance.text + "<br /> about " + myRoute.duration.text)));
var ol = $('<ol>').addClass('directions');
for (var j = 0; j < myRoute.steps.length; j++) {
var step = myRoute.steps[j];
var li = $('<li>').append($('<div>').addClass('direction').html(step.instructions));
li.append($('<div>').addClass('distance').html(step.distance.text + " - " + step.duration.text));
ol.append(li);
}
//add tour with directions
$('#tours_list').append(temp);
}
}
}
The issue is that the array in directionResult.routes[0].legs is not as long as checkboxArray.length so your code is trying to access beyond the end of directionResult.routes[0].legs and thus gets undefined.
It isn't helping you that you are testing for only active class items because i goes from 0 to checkboxArray.length - 1 regardless.
I don't follow exactly what you're trying to do, but you might be able to work around this by only iterating the items that have .active in the first place so i never goes beyond the number of active items. You might be able to do that by changing this:
var checkboxArray = $(".selector.Favs" + idname).find("li");
to this:
var checkboxArray = $(".selector.Favs" + idname).find("li.active");
And, then remove the if check for the active class. This will make it so your index i never goes higher than the number of active items.
You could also just keep a counter of the active items you've processed and use that counter to index into directionResult.routes[0].legs[cntr] instead of using i.

Categories