How to loop through htmlcollection object? - javascript

I have looked at almost every question that has been asked here about htmlcollection.
So I have a div and I am fetching data and creating divs inside this div with ajax so they are not hardcoded.
this is how div look like before I fetch the data
<div id="tivvits"></div>
this is how div#tivvits looks like after I call function show_all_tivvits();
show_all_tivvits() is a function where I create a ajax request and create new divs
such as div#tivvit-21, div#tivvit-22, etc.
<div id="tivvits">
<div id="tivvit-19" class="grid-container">...</div>
<div id="tivvit-20" class="grid-container">...</div>
</div>
this is part of the js file
document.addEventListener("DOMContentLoaded", function(){
show_all_tivvits();
var t = document.getElementById('tivvits');
const j = t.getElementsByClassName("grid-container");
const k = Array.prototype.slice.call(j)
console.log(k);
for (var i = 0; i < k.length; i++) {
console.log(k[i]);
}
});
what I wanted to do in show_all_tivvits() function is I want to get the divs that are already in the div#tivvits and that way I am not gonna create them again but the problem is when I use console.log() to print out document.getElementById('tivvits').getElementsByClassName('grid-container') there are items in the htmlcollection but when I print out length it returns 0.
one more thing when I open inspect>source in chrome my index.php doesn't have updated div#tivvits.
I have tried almost every way to loop this htmlcollection but it is not working.
list of things I have tried;
Array.from(links)
Array.prototype.slice.call(links)
[].forEach.call(links, function (el) {...});
HTMLCollection.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];
HTMLCollection.prototype.forEach = Array.prototype.forEach;

It's not really clear, but are you looking for something like this?
targets = document.querySelectorAll('#tivvits > .grid-container')
for (let target of targets)
{console.log(target.id)}
This should select all <div> nodes which are direct children of the <div id="tivvits"> node and have a class attribute with the value "grid-container", and extract from them the attribute value of the id attribute.

Have a go with this
I use the spread operator to allow the use of map on the HTMLCollection
window.addEventListener("load", function() {
const gridContainers = document.querySelectorAll("#tivvits .grid-container");
const ids = [...gridContainers].map(div => div.id);
console.log(ids)
});
<div id="tivvits">
<div id="tivvit-19" class="grid-container">...</div>
<div id="tivvit-20" class="grid-container">...</div>
</div>
To just display change
const ids = [...gridContainers].map(div => div.id);
to
[...gridContainers].forEach(div => console.log(div.id));

Related

Is it possible to update/refresh NodeList?

I have a "Maindiv" (div) with, say, 4 elements in it. Lets consider the elements belong to a class called "Subdiv". When I query the number of "Subdivs" with "Maindiv.getElementsByClassName('Subdiv').length;" , it returns 4, as expected. But if I create a new "Subdiv" and append it to my main "Maindiv" and instantly query for the length, it will return 4 (which is wrong), and until the NodeList is updated (usually 20-50 milliseconds after appending the new element) it returns 4. Finally after this interval it returns the right number (5). My question is, if there's a way to update/refresh the NodeList faster just after I append the new element?
<div>
<div id='Maindiv'>
<div class='Subdiv' id='Subdiv1'></div>
<div class='Subdiv' id='Subdiv2'></div>
<div class='Subdiv' id='Subdiv3'></div>
<div class='Subdiv' id='Subdiv4'></div>
</div>
<button type='button' onclick='CreateNewSubdivs()'>Create Subdiv</button>
</div>
<script>
function CreateNewSubdivs(){
var MainDiv = document.getElementById('Maindiv');
var SubdivsLength= MainDiv.getElementsByClassName('Subdiv').length;
var NewSubDiv = document.createElement('div');
var NewCopyNumber = SubdivsLength+1;
var NewSubDivID = 'Subdiv'+NewCopyNumber;
NewSubDiv.setAttribute('class', 'Subdiv');
NewSubDiv.setAttribute('id', NewSubDivID );
MainDiv.appendChild(NewSubDiv );
var SubdivsLength= MainDiv .getElementsByClassName('Subdiv').length;
console.log(SubdivsLength); /// This number is wrong until 20-50 millisec later
}
</script>
A NodeList can either be a static or "live" collection of nodes.
In some cases, the NodeList is live, which means that changes in the DOM automatically update the collection.
e.g. Node.childNodes
In other cases, the NodeList is static, where any changes in the DOM does not affect the content of the collection.
e.g. a list returned by querySelectorAll()
Source: https://developer.mozilla.org/en-US/docs/Web/API/NodeList
As you can see in this example, I set a reference to childNodes once. It is kept up to date as soon as the DOM changes.
const list = document.querySelector('#main');
const items = list.childNodes;
setInterval(() => {
const item = document.createElement('li');
item.innerHTML = '🌯';
list.appendChild(item);
console.log(items.length);
}, 500);
<ul id="main"></ul>
getElementsByClassName also returns a live collection of nodes:
const list = document.querySelector('#main');
const spans = list.getElementsByClassName('burrito');
setInterval(() => {
const item = document.createElement('li');
item.innerHTML = '<li><span class="burrito">🌯</span></li>';
list.appendChild(item);
console.log(spans.length);
}, 1);
<ul id="main">
</ul>

get second element by class name if it exists

I'm trying to get the ID of an element by class name like this
var prod_id2 = document.getElementsByClassName('select-selected')[1].id
document.getElementById('hidden-input-2').value = prod_id2;
This works fine, but my issue is that if there's only one element with that class it breaks the functionality, so I need some sort of if statement to only define this var if there is a second div with that class.
Any ideas?
Try this:
const elements = document.querySelectorAll('.test');
if (elements[1]) {
elements[1].innerText = 'Hithere';
}
<div class="test">hi</div>
<div class="test">hi</div>
<div class="test">hi</div>
document.querySelectorAll('.test'); selects all elements with the class test and returns a nodelist.
Then we can access the second element via of the nodelist with elements[1].
Here is how to check for the second element.
You can also set another fallback , different to null:
document.addEventListener("DOMContentLoaded", function(event) {
var selectedElements = document.querySelectorAll('.selected-selected'),
prod_id2 = selectedElements[1] || null;
alert(prod_id2)
});
<div id="test" class="selected-selected"></div>
You can also check that value then:
if (prod_id2) { // do some other stuff with the set value }
It breaks the functionality I think because you are grabbing the 2nd element specifically. You can do:
const prod_id2 = document.querySelectorAll('.select-selected');
and loop over the elements and grab the ID
prod_id2.forEach((prod, index) => {
if(index === 2) {
document.getElementById('hidden-input-2').value = prod.id;
}
})

Passing in right information when rendering items from Javascript on the HTML side

I am trying to create an extension for VSTS using their extension kit (https://learn.microsoft.com/en-us/vsts/extend/overview?view=vsts).
<script type="text/javascript">
VSS.init();
var items = {}
// Get data service and display
VSS.getService(VSS.ServiceIds.ExtensionData).then((dataService) => {
dataService.getDocuments('MyCollection2').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
var name = docs[i].name
contents.push(
`<div
class="listItem"
onClick="console.log(docs[i])"
onmouseover="this.style.background='#D5DBDB';"
onmouseout="this.style.background='white';">
${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('');
});
});
</script>
So what my javascript part does is I try to fetch objects from VSTS`s internal data storage (I named it MyCollection2) and display the objects as a list
HTML part
<section>
<nav>
<div class="create_button">+ Create KPI</div>
<div id="items"></div>
</nav>
<article>
<h2>Create KPI</h2>
<br>
<form action="" id="form" onsubmit="sConsole(event)">
KPI Name<br>
<input type="data" id="name">
<br><br>
Actual Value<br>
<input type="data" id="actual">
<br><br>
Potential Value<br>
<input type="data" id="potential">
<br><br>
Goal %<br>
<input type="data" id="goal">
<br><br>
<button type="submit">Create</button><span>Cancel</span>
</form>
</article>
</section>
So all the objects are rendered in the div with the id items.
Everything is fine up to this point.
The problem is the onClick="console.log(docs[i]) part in my javascript part.
My intention was to console.log the document object whenever each item in the list was clicked.
However, this doesn't print the object as I intended.
It just prints externalContentHost10 and I don't know what that is.
What can I do to make this work?
docs is defined in your function; the scope of the onclick attribute (note: should be all lowercase) is not the same. In general, you should avoid inline event handlers as they’re not very flexible or maintainable. You should instead use addEventListener, which means ditching innerHTML and working with proper element nodes. A few other changes I would make are:
Flattening the promises (removing the nesting) by returning them
Using for...of for iteration
Using const (and let, but in this case const is enough) instead of var so that your variables have the right scope
This gives us:
VSS.init();
const items = {};
// Get data service and display
VSS.getService(VSS.ServiceIds.ExtensionData)
// the callback on the next line returns a promise, which the JavaScript engine will follow, so you don't need to nest the next `then`
.then((dataService) => dataService.getDocuments('MyCollection2'))
.then((docs) => {
// keep a reference to the element instead of searching for it in each loop.
const container = document.getElementById('items');
// this loop will remove any existing children
while (container.firstChild !== null) {
container.removeChild(container.firstChild);
}
// `for...of` is a simpler way to iterate over a collection
for (const doc of docs) {
// create a `div` element
const div = document.createElement("div");
// add a text node to it
div.appendChild(document.createTextNode(doc.name));
// add event listeners to change its background
div.addEventListener("mouseover", e => { div.style.background = "#D5DBDB"; });
div.addEventListener("mouseout", e => { div.style.background = "white"; });
// add a `click` listener
div.addEventListener("click", e => { console.log(doc); });
// add the new div to the container
container.appendChild(div);
}
});
If you wanted to use classes instead to manage the styling—which is the recommended method—then you could implement the event listeners using classList:
div.addEventListener("mouseover", e => div.classList.add("hover-class"));
div.addEventListener("mouseout", e => div.classList.remove("hover-class"));
(classList has toggle and replace methods, but they aren’t supported by IE at all, and Edge only seems to support toggle, so whether to use them depends on your minimum supported version.)
But you would probably be better off defining a CSS :hover class rather than doing all this, if styling is all you want to change.

Sorting divs based on array order

I have a drag and drop design that, when you rearrange the draggable items, pushes their ids to an array. So i’ll have an array like:
["#fake-block_1","#fake-block_3","#fake-block_2"]
Behind the scenes, I want to rearrange some corresponding divs that share the same numeric value as these blocks, e.g., #fake-block_1 maps on to #real-block_1. I can’t quite seem to grasp how I would get this rearrangement to happen. Heres what I currently have:
$('.js-fake-block’).each(function(i){
$this = $(this);
$array = new Array();
$delimiter = '_';
$array.push($this.attr("id").split($delimiter)[1]);
$array.forEach(function(item,index){
$realBlockId = "#real-block_”+[item];
});
});
So I loop through every “fake block”, split their ID by an underscore (I match fake and real with the same numeric value), add them into an array, and then have the real Ids made up again… but after that I’m lost. No idea how I’d sort the “real blocks” based on this "fake blocks" arrays order.
Here is a simple example of what you're trying to do JSFiddle
function sortDom(selectorArray) {
while (selectorArray.length) {
let $el = $(selectorArray.pop());
$el.parent().prepend($el);
}
}
//Usage//
$('.ordinal').on('click', function() {
sortDom(['#_01', '#_02', '#_03', '#_04', '#_05', '#_06', '#_07', '#_08', '#_09', '#_10']);
});
$('.reversed').on('click', function() {
sortDom(['#_10', '#_09', '#_08', '#_07', '#_06', '#_05', '#_04', '#_03', '#_02', '#_01']);
});
$('.jumbled').on('click', function() {
sortDom(['#_07', '#_01', '#_10', '#_04', '#_02', '#_03', '#_06', '#_05', '#_09', '#_08']);
});
Note this method does not enforce the array elements reference dom elements attached to the same parent and it does not enforce that all child elements of the parent must be referenced in the array. Unreferenced child elements will be pushed to the bottom of the list.
One solution is to use the sort method to describe how to the sort elements and then re-set the html of the parent:
var sortedDivs = $divs.sort(function (a, b) {
// If referring to your array of IDs, you can use indexOf($(a).attr("id"))
return $(a).attr("id") > $(b).attr("id");
});
$("#container").html(sortedDivs);
JsFiddle
I don't know why you need to save numbers in array and loop again to create a new array .. you can just use the next code
$array = []; // use array outside the loop
$('.js-fake-block').each(function(i){
var $this = $(this);
var $delimiter = '_';
var SplitNum = $this.attr("id").split($delimiter);
$array.push("#real-block_" + SplitNum[1]);
});
console.log($array);
Working example
$array = []; // use array outside the loop
$('.js-fake-block').each(function(i){
var $this = $(this);
var $delimiter = '_';
var SplitNum = $this.attr("id").split($delimiter);
$array.push("#real-block_" + SplitNum[1]);
});
console.log($array);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="js-fake-block" id="fake-block_1"></div>
<div class="js-fake-block" id="fake-block_3"></div>
<div class="js-fake-block" id="fake-block_5"></div>
<div class="js-fake-block" id="fake-block_4"></div>
<div class="js-fake-block" id="fake-block_2"></div>

Get an element's value without an id

I've got following HTML:
<span class="testClass1" >
wanted Text
<a class="ctx" href="#"></a>
</span>
Now I want to get the text "wanted Text".
How can I achieve this?
I tried with:
document.getElementsByClassName("testClass1");
I also tried with document.getElementsByTagName() but I don't know how to use them properly.
You can use querySelectorAll
hence:
document.querySelectorAll('.testclass1 a')
will return all the <a> items children of a .testclass1
Snippet example:
var elements = document.querySelectorAll('.testClass1 a')
console.log(elements) // open the console to see this
console.log(elements[0].text) // this gets the first <a> text `wanted Text`
<span class="testClass1" >
wanted Text
<a class="ctx" href="#"></a>
</span>
The getElementsByClassName() function returns an array of matching elements, so if you need to access them, you could do so using a loop :
// Get each of the elements that have the class "testClass1"
var elements = document.getElementsByClassName("testClass1");
// Iterate through each element that was found
for(var e = 0; e < elements.length; e++){
// Get the inner content via the innerHTML property
var content = elements[e].innerHTML;
}
If you need to actually access the <a> tags directly below some of the elements as your edit indicates, then you could potentially search for those wihtin each of your existing elements using the getElementsbyTagName() function :
// Get each of the elements that have the class "testClass1"
var elements = document.getElementsByClassName("testClass1");
// Iterate through each element that was found
for(var e = 0; e < elements.length; e++){
// Find the <a> elements below this element
var aElements = elements[e].getElementsByTagName('a');
// Iterate through them
for(var a = 0; a < aElements.length; a++){
// Access your element content through aElements[a].innerHTML here
}
}
You can also use an approach like squint's comment or Fred's which take advantage of the querySelectorAll() function as the getElementsByClassName() and getElementsByTagName() are better served when accessing multiple elements instead of one specifically.
Try this:
document.getElementsByClassName("testClass1")[0].getElementsByTagName('a')[0].innerText
var testClass1 = document.getElementsByClassName("testClass1");
console.log(testClass1[0].innerHTML);

Categories