getElementById in a for-loop only displays the first item - javascript

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);
}

Related

Using array to store elements doesn't let you change the properties

const divArr = [];
for (let i = 0; i < 5; i++) {
document.getElementById("h").innerHTML += `<div id=${i}>hello</div>`;
divArr.push(document.getElementById(`${i}`));
}
console.log(divArr);
let divArrIndex = 0;
setInterval(() => {
document.getElementById(`${divArrIndex}`).style.backgroundColor = 'green';
if (divArrIndex > 4) {
divArrIndex = 0;
}
divArrIndex += 1;
}, 1000 / 1);
<div id="h">alsdjf;lkajsdf</div>
The code above successfully turns all the divs green.
But, when I use
divArr[divArrIndex].style.backgroundColor = "green"
instead of
document.getElementById(`${divArrIndex}`).style.backgroundColor='green';
I only get the last div green.
Why?
codepen: https://codepen.io/sai-nallani/pen/KKopOXZ?editors=1111
By reassignment to innerHTML, you are destroying and recreating the contents of #h in each iteration of the loop. You create #0, then discard it and create #0 and #1, then discard those and create #0, #1, #2... So the elements you push into the array don't exist any more in your document (though references to them in divArr still keep the garbage collector from destroying them outright).
When you change the colour of divArr[0], you are changing the colour of an element that only exists in memory, but is not in DOM any more. However, #4 is still the original #4, it has not been discarded, since you have performed no further assignments to innerHTML.
One solution is to gather all the divs after you have constructed them all. You can use another loop, but the easiest way would be:
const divArr = Array.from(document.querySelectorAll('#h > div'));
(Depending on what you are doing with it next, you may not need Array.from since NodeList should suffice for many purposes.)
Another solution is to construct elements in their own right, not by changing the parent's innerHTML:
const hEl = document.querySelector('#h');
for (let i = 0; i < 5; i++) {
const divEl = document.createElement('div');
divEl.textContent = 'Hello';
divEl.id = i;
hEl.appendChild(divEl);
divArr.push(divEl);
}
This way, every element is created and added to #h without affecting any other elements already there.

Detect a button and then press it in JavaScript

I want to make a function that would detect a button on a web page and then click it. But I want it to click a specific item.
function imready()
{
var btn = document.getElementsByClassName('text-xxxs mb-02');
for (var i = 0; i < btn.length; i++)
{
if (btn[i].innerText.indexOf('AK-47') > -1)
{
console.log('runtime');
chrome.runtime.sendMessage({ type: 'dontrun', update: 1 }, function (response) {
});
btn[i].click();
pressok();
}
}
How do I make it so that the var "btn" should equal to document.getElementsbyClassName('x') and also a different className ('y')?
Quoting from https://stackoverflow.com/a/29366682/10450049
getElementsByClassName() returns an HTMLcollection object which is similar to an array but not really an array so you can't call
array methods using the returned value. One hack is to use Array's
prototype methods along with .call()/.apply() to pass the returned
object as the context.
var elems = document.getElementsByClassName("royal") ;
var collapsedElems = document.getElementsByClassName("collapsed");
var earray = Array.prototype.slice.call(elems, 0);
var concatenated = earray.concat.apply(earray, collapsedElems) ;
console.log(concatenated)
Demo Fiddle
As far as i understand your question, you can use document.querySelector('.classX.classY') to select the needed button with both classes.
That works for the case if you only need one button on the page selected, from your code i assume exactly that.

Apps Script JS adding items to array from range (if not already in array) fails

I am looping through various cells and want to add their string content do an array, if the content is not already in the array. It works perfectly fine when I do it manually like so, trying to add 'eJobs' to the array (see below "var item = 'eJobs') which already containts 'eJobs':
var divisionarray = ['eJobs']
for (var i = 0; i < cells_users.length-1; ++i) {
var row_users = cells_users[i];
if (row_users[0] == user_ldap) {
var podarray = row_users[1].split(', ')
for (j = 0; j < podarray.length; j++) {
for (var k = 0; k < cells_edit.length; ++k) {
var row_edit = cells_edit[k]
if (podarray[j] === row_edit[0]) {
var item = 'eJobs'
if (!(divisionarray.indexOf(item) >= 0)) {
divisionarray.push(item)
}
}
}
}
Logger.log(divisionarray)
As expected, the log file shows [17-10-08 19:11:04:111 BST] [eJobs], illustrating that the code works and 'eJobs' has not been added to the array as it is already in the array.
Now, when I change var item='eJobs' to values of a range
var item = sheet_pods_edit.getRange(startRow+k, startColumn+1).getValue();
the code does not work anylonger, as the log file shows:
[17-10-08 19:14:03:770 BST] [eJobs, eJobs, BestJobs, Vivre Deco, ...
Note I have a range of thousands of cells, so I get alot of duplicates added. What am I missing? Note the cells of the defined range are indeed just strings with a single word (e.g. 'eJobs').
The code is working and the log file is indicating what the problem is..
[eJobs, eJobs, BestJobs, Vivre Deco,
In the second eJobs there is a white space before eJobs, so the first value and the second value don't match.
Without seeing your data and going by the 'just strings with a single word' I would say that using a .replace(" ", "") on the text string should work, this will find the first " " in the string and remove it. I.e. " eJobs" would become "eJobs".
2.
Is this line of code just for testing? You should never use a method like this in a script. It will be extremely inefficient
var item = sheet_pods_edit.getRange(startRow+k, startColumn+1).getValue();
Instead get the full range using .getValues()and iterate over it then.
3.
Is there a reason you are using === in if (podarray[j] === row_edit[0]) unless you need to check for type always use ==

Why does removing an element with javascript prevent iteration of elements?

I am trying to replace all text fields on a page with labels.
function replaceInputTextFieldsWithValues() {
var inputFields = document.getElementsByTagName("input");
for(var i = 0; i < inputFields.length; i++) {
if(inputFields[i].getAttribute("type")== "text") {
var parent = inputFields[i].parentNode;
var value = inputFields[i].value;
parent.removeChild(inputFields[i]);
var label = document.createElement('label');
label.setAttribute('for', value);
label.innerHTML = value;
parent.appendChild(label);
}
}
}
My HTML document is organized in tables. This function only seems to work on the first element in each table.
On the other hand, when I remove the line:
parent.removeChild(inputFields[i]);
The code seems to work fine. Why is this happening and how do I fix it?
What you get back from getElementsByTagName is an HTMLCollection, which is live. (This is true for the other getElementsByXYZ methods, but not querySelectorAll.) That means if you remove the element at index 0, the HTMLCollection's length will go down and you'll have a new element at index 0 instead of the one you just removed.
Just work your way through it backward and you'll be fine:
for(var i = inputFields.length - 1; i >= 0; i--) {
// ...
}
Alternately, convert the HTMLCollection into an array and then loop through the array. (See the live example and code below).
Edit: Or, as Chris Shouts points out in the comments, you can just make use of the changing length, but it's not quite as simple as Chris' suggestion because you're only removing the elements sometimes. It would look like this:
var inputFields = document.getElementsByTagName("input");
var i = 0;
while (i < inputFields.length) {
if(inputFields[i].getAttribute("type")== "text") {
// Remove it and DON'T increment `index`
}
else {
// Skip this one by incrementing `index`
++index;
}
}
Which of these three approaches to use will depend on the situation. Copying to an array gives you a nice static dataset to work with, and if you make sure to release the reference to the HTMLCollection, you're giving the browser the opportunity to realize it doesn't have to keep that list up-to-date when things change, which could reduce overhead. But you're copying the references briefly, which increases overhead a bit. :-)
Additional: Here's an example showing this effect, and also showing a fairly efficient (but obscure) way to create an array from a HTMLCollection:
HTML:
<ul>
<li>LI0</li>
<li>LI1</li>
<li>LI2</li>
</ul>
JavaScript:
var lilist, liarray;
// Get the HTMLCollection, which is live
lilist = document.getElementsByTagName('li');
// Create an array of its elements
liarray = Array.prototype.slice.call(lilist, 0);
// Show initial length of both
display("lilist.length = " + lilist.length); // Shows 3
display("liarray.length = " + liarray.length); // Shows 3
// Show what the 0th element of both is (both show "LI0" in the live example)
display("lilist[0].innerHTML = " + lilist[0].innerHTML); // Shows LI0
display("liarray[0].innerHTML = " + liarray[0].innerHTML); // Shows LI0
// Remove the first list item
display("Removing item 0");
lilist[0].parentNode.removeChild(lilist[0]);
// Show the length of both, note that the list's length
// has gone down, but the array's hasn't
display("lilist.length = " + lilist.length); // Shows 2, not 3
display("liarray.length = " + liarray.length); // Still shows 3
// Show what the 0th element of both *now* is
display("lilist[0].innerHTML = " + lilist[0].innerHTML); // Shows LI1 now
display("liarray[0].innerHTML = " + liarray[0].innerHTML); // Still shows LI0
Live copy

Storing elements in memory to prevent updating the DOM too often?

Currently I have a loop that updates the DOM in each iteration; I have learned this is a bad practice & you should update the DOM as little as possible for better speed.
So I was wondering how I go about editing the below so I can store all the elements in one element or something & then do a single DOM addition once the loop ends.
Here is the loop..
for (var i = spot; i < spot + batchSize && i < cats.options.length; i++) {
// Check if the cat is selected
if (cats.options[i].selected == true) {
// Set this category's values to some variables
var cat_id = cats.options[i].getAttribute('value');
var cat_name = cats.options[i].text;
if (checkCatSICAdd(cat_id) === false) {
// Now we create the new element
var new_option = document.createElement('option');
// Add attribute
new_option.setAttribute('value',cat_id);
// Create text node
var new_text_node = document.createTextNode(cat_name);
// Append new text node to new option element we created
new_option.appendChild(new_text_node);
// Append new option tag to select list
sel_cats.appendChild(new_option);
} else {
failed++;
}
}
}
Working with DOM element in the loop is slow - no matter if you attach them to the document or not. Attaching them at the end is a bit faster since only only redraw is required but it's still slow.
The proper way is generating a plain old string containing HTML and attaching this string to the DOM using the innerHTML property of a DOM element.
Your code should be ok. The DOM won't actually redraw until the Javascript has finished executing. However, if you've encountered a problem browser that does perform badly, you could try creating a new select before your loop that is not yet attached to the DOM, populating it as you are now, then replacing sel_cats with that new select at the end. That way, the DOM is only updated once.
Your way is good enough unless you have great many items added to sel_cats - you add to the DOM only once.
The only way to improve efficiency might be to store the options as raw HTML then assign that after the loop:
var arrHTML = [];
for (var i = spot; i < spot + batchSize && i < cats.options.length; i++) {
// Check if the cat is selected
if (cats.options[i].selected == true) {
// Set this category's values to some variables
var cat_id = cats.options[i].value;
var cat_name = cats.options[i].text;
if (checkCatSICAdd(cat_id) === false) {
arrHTML.push("<option value=\"" + cat_id + "\">" + cat_name + "</option>";
}
else {
failed++;
}
}
}
sel_cats.innerHTML = arrHTML.join("");
Once you have the select list assigned to a variable, remove it from the dom using removeChild on its parent tag. You can then use appendChild in the loop before adding the select list back into the dom.
Your code is way bloated, DOM 0 methods will be much faster.
If speed really matters, store spot + batchSize && i < cats.options.length in variables so they aren't re-calcuated on each loop (modern browsers probably don't, but older ones did):
for (var i=spot, j=spot+batchSize, k=cats.options.length; i < j && i < k; i++) {
// Store reference to element
var opt = cats.options[i];
// The selected property is boolean, no need to compare
if (opt.selected) {
// if checkCatSICAdd() returns boolean, just use it
// but maybe you need the boolean comparison
if (checkCatSICAdd(opt.name) === false) {
// Wrapped for posting
sel_cats.options[sel_cats.options.length] =
new Option(opt.value, opt.name);
} else {
failed++;
}
}
}

Categories