How to sum elements dynamically added with Javascript? - javascript

I am adding table rows dynamically to a table using Javascript.
Is there a reason why items.length doesn't increase when I add a row?
I am also trying to sum the numbers contained in each row. But this doesn't work either. The dynamically added rows are completely ignored for some reason.
I am coming from jQuery where things like these used to work.
I am probably missing something really fundamental here. Thanks for any pointers.
document.addEventListener("DOMContentLoaded", function() {
var form = document.querySelector("#form");
var items = form.querySelectorAll(".item");
form.addEventListener("click", function(event) {
if (event.target.className == ".add_item") {
addFields(event);
}
});
function addFields(event) {
var item = document.createElement("template");
item.innerHTML = fields.trim();
items[items.length - 1].insertAdjacentElement("afterend", item.content.firstChild);
console.log(items.length);
event.preventDefault();
}
})

querySelector and querySelectorAll returns NodeList where as getElementsByClassName returns HTMLCollection.
The difference is, NodeList is a static copy but HTMLCollection is a live copy. So if element is modified, like in your case, a new row is added, HTMLCollection will work but NodeList will not.
So changing
var items = form.querySelectorAll(".item")
to
var items = form.getElementsByClassName("item")
might solve the problem.
Pointers
You cannot have a selector in getElementsByClassName. It expects a className, you cannot use composite selector like #form .item.
Reference:
Difference between HTMLCollection, NodeLists, and arrays of objects

You only query the items once here:
var items = form.querySelectorAll(".item");
form.addEventListener(
//you call addItem here
)
function addItem(){
//you access items.length here
}
You need to keep re-query-ing the selectors every time you add an item.
var items = form.querySelectorAll(".item"); //i got queried for the 1st time
function addFields(event) {
items = form.querySelectorAll(".item"); //get items again
//...
}
Or just don't query outside addFields() all in all. Put var items = ... in the function. There's no need to put it outside.
Read up #Rajesh's answer why this is so.

Related

Add class using getElementsByClassName with in HTML [duplicate]

I am Javascript beginner.
I am initing web page via the window.onload, I have to find bunch of elements by their class name (slide) and redistribute them into different nodes based on some logic. I have function Distribute(element) which takes an element as input and does the distribution. I want to do something like this (as outlined for example here or here):
var slides = getElementsByClassName("slide");
for(var i = 0; i < slides.length; i++)
{
Distribute(slides[i]);
}
however this does not do the magic for me, because getElementsByClassName does not actually return array, but a NodeList, which is...
...this is my speculation...
...being changed inside function Distribute (the DOM tree is being changed inside this function, and cloning of certain nodes happen). For-each loop structure does not help either.
The variable slides act's really un-deterministicaly, through every iteration it changes it's length and order of elements wildly.
What is the correct way to iterate through NodeList in my case? I was thinking about filling some temporary array, but am not sure how to do that...
EDIT:
important fact I forgot to mention is that there might be one slide inside another, this is actually what changes the slides variable as I have just found out thanks to user Alohci.
The solution for me was to clone each element into an array first and pass the array ono-by-one into Distribute() afterwards.
According to MDN, the way to retrieve an item from a NodeList is:
nodeItem = nodeList.item(index)
Thus:
var slides = document.getElementsByClassName("slide");
for (var i = 0; i < slides.length; i++) {
Distribute(slides.item(i));
}
I haven't tried this myself (the normal for loop has always worked for me), but give it a shot.
If you use the new querySelectorAll you can call forEach directly.
document.querySelectorAll('.edit').forEach(function(button) {
// Now do something with my button
});
Per the comment below. nodeLists do not have a forEach function.
If using this with babel you can add Array.from and it will convert non node lists to a forEach array. Array.from does not work natively in browsers below and including IE 11.
Array.from(document.querySelectorAll('.edit')).forEach(function(button) {
// Now do something with my button
});
At our meetup last night I discovered another way to handle node lists not having forEach
[...document.querySelectorAll('.edit')].forEach(function(button) {
// Now do something with my button
});
Browser Support for [...]
Showing as Node List
Showing as Array
An up-to-date answer in 2021
.getElementsBy* methods return a live HTMLCollection, not a NodeList, getElementsByName being an exception.
There are remarkable differencences between these two lists. Whereas HTMLCollection has two methods, NodeList has five methods, including NodeList.forEach, which can be used to iterate through a NodeList.
Live collections are problematic because there's no way to keep the collection updated under the hood. To achieve a reliable collection, the DOM is traversed every time a collection is accessed, in every current implementation of HTMLCollection. In practice this means, that every time you access a member of a live collection (including the length), the browser traverses the entire document to find the specific element.
The Standard says:
If a collection is live, then the attributes and methods on that object must operate on the actual underlying data, not a snapshot of the data.
Never iterate live HTMLCollection!
Instead, convert the collection to array, and iterate that array. Or rather get the elements using .querySelectorAll, which gives you a static NodeList and a more flexible way to select elements.
If you really need a live list of elements, use the closest possible common ancestor element as the context instead of document.
It's notable, that also live NodeLists exist. Examples of live NodeLists are Node.childNodes and the return value of getElementsByName.
You could always use array methods:
var slides = getElementsByClassName("slide");
Array.prototype.forEach.call(slides, function(slide, index) {
Distribute(slides.item(index));
});
Update 2022
Fastest and shortest solution
[...document.getElementsByClassName('className')].forEach(el => {
// Do something with each element
})
Why does it work?
Iterating live HTML collection is extremely inefficient
As mentioned in styks' answer above, [...htmlCollection] converts the the class collection into an array.
It is necessary to convert it to an array, since iterating a live HTMLCollection directly would be extremely inefficient. As teemu wrote above "Never iterate live HTMLCollection!":
Live collections are problematic because there's no way to keep the collection updated under the hood. To achieve a reliable collection, the DOM is traversed every time a collection is accessed, in every current implementation of HTMLCollection. In practice this means, that every time you access a member of a live collection (including the length), the browser traverses the entire document to find the specific element.
Why is it the fastest and most efficient?
Note that using [...arr]
is the fastest and most efficient way to convert htmlCollection to an array.
A performance comparison of all methods made by harpo can be found here:
http://jsben.ch/h2IFA
(See all details about htmlCollection conversion to an array here)
I followed Alohci's recommendation of looping in reverse because it's a live nodeList. Here's what I did for those who are curious...
var activeObjects = documents.getElementsByClassName('active'); // a live nodeList
//Use a reverse-loop because the array is an active NodeList
while(activeObjects.length > 0) {
var lastElem = activePaths[activePaths.length-1]; //select the last element
//Remove the 'active' class from the element.
//This will automatically update the nodeList's length too.
var className = lastElem.getAttribute('class').replace('active','');
lastElem.setAttribute('class', className);
}
You could use Object.values + for...of loop:
const listA = document.getElementById('A');
const listB = document.getElementById('B');
const listC = document.getElementById('C');
const btn = document.getElementById('btn');
btn.addEventListener('click', e => {
// Loop & manipulate live nodeLList
for (const li of Object.values(listA.getElementsByClassName('li'))) {
if (li.classList.contains('active')) {
listB.append(li);
} else {
listC.append(li);
}
}
});
ul {
display: inline-flex;
flex-direction: column;
border: 1px solid;
}
ul::before {
content: attr(id);
}
.active {
color: red;
}
.active::after {
content: " (active)";
}
<ul id="A">
<li class="li active">1. Item</li>
<li class="li">2. Item</li>
<li class="li">3. Item</li>
<li class="li active">4. Item</li>
<li class="li active">5. Item</li>
<li class="li">6. Item</li>
<li class="li active">7. Item</li>
<li class="li">8. Item</li>
</ul>
<button id="btn">Distribute A</button>
<ul id="B"></ul>
<ul id="C"></ul>
One-liner:
Object.values(listA.getElementsByClassName('li')).forEach(li => (li.classList.contains('active') ? listB : listC).append(li))
Here is the example I've done like this to assign random number to each class:
var a = $(".className").length;
for(var i = 0; i < a; i++){
var val = $(".className")[i];
var rand_ = (Math.random()*100).toFixed(0);
$(val).val(rand_);
}
It's just for reference
<!--something like this-->
<html>
<body>
<!-- i've used for loop...this pointer takes current element to apply a
particular change on it ...other elements take change by else condition
-->
<div class="classname" onclick="myFunction(this);">first</div>
<div class="classname" onclick="myFunction(this);">second</div>
<script>
function myFunction(p) {
var x = document.getElementsByClassName("classname");
var i;
for (i = 0; i < x.length; i++) {
if(x[i] == p)
{
x[i].style.background="blue";
}
else{
x[i].style.background="red";
}
}
}
</script>
<!--this script will only work for a class with onclick event but if u want
to use all class of same name then u can use querySelectorAll() ...-->
var variable_name=document.querySelectorAll('.classname');
for(var i=0;i<variable_name.length;i++){
variable_name[i].(--your option--);
}
<!--if u like to divide it on some logic apply it inside this for loop
using your nodelist-->
</body>
</html>
I had a similar issue with the iteration and I landed here. Maybe someone else is also doing the same mistake I did.
In my case, the selector was not the problem at all. The problem was that I had messed up the javascript code:
I had a loop and a subloop. The subloop was also using i as a counter, instead of j, so because the subloop was overriding the value of i of the main loop, this one never got to the second iteration.
var dayContainers = document.getElementsByClassName('day-container');
for(var i = 0; i < dayContainers.length; i++) { //loop of length = 2
var thisDayDiv = dayContainers[i];
// do whatever
var inputs = thisDayDiv.getElementsByTagName('input');
for(var j = 0; j < inputs.length; j++) { //loop of length = 4
var thisInput = inputs[j];
// do whatever
};
};

Get href list from element list in Javascript

I know I can do this with a simple for loop but I'm trying to understand better how to use forEach.
What I'm trying to do is, having a list of "a" elements coming from a querySelectorAll(), to obtain an array with the href attributes. What is wrong?
var links = document.querySelectorAll("a");
function get_hrefs(links){
var links_array = links.forEach(function(elem){ return elem.getAttribute("href"); });
return links_array;
}
get_hrefs(links);
Use slice to get an Array, like this:
var links = Array.prototype.slice.call(document.querySelectorAll("a"));
var links_array = links.map(function(elem){ return elem.getAttribute("href"); });
As for the updated question:
var links = document.querySelectorAll("a");
function get_hrefs(links){
links = Array.prototype.slice.call(links);
return links.map(function(elem){ return elem.getAttribute("href"); });;
}
get_hrefs(links);
You're looking for map rather than forEach, and you need to call it differently because querySelectorAll returns a NodeList, not an arary:
var links = document.querySelectorAll("a");
var links_array = Array.prototype.map.call(links, function(e){
return e.getAttribute("href");
});
That's if you really want the attribute value. If you want the resolved href, then:
var links = document.querySelectorAll("a");
var links_array = Array.prototype.map.call(links, function(e){
return e.href;
});
Re your updated question, where the function will receive links and not necessarily know what it is: The code above will work both for true arrays and for array-like things like the NodeList returned by querySelectorAll. So no changes needed to handle it.
If you want to use foreach, because querySelectorAll doesn't return array so you can do something like this:
var links = document.querySelectorAll("a");
var links_array = [];
[].forEach.call(
links,
function(elem){
links_array.push(elem.getAttribute("href"));
}
);
var hrefArray = [];
var links = document.querySelectorAll("a");
var Array.prototype.forEach.call(links, function(elem){ hrefArray.push(elem.getAttribute("href")); });
Let forEach push the elements into an empty array. forEach just iterates over all the elements. What you do with it is your decision.
Other users are pointing to the use of map. That functions suits the problem well. However the question was to get a better understanding of forEach.
Cerbrus pointed us to the fact that querySelectorAll returns a nodelist. Which I overlook at first. instead of an array. Now a nodelist is arrayish. So you can still treat it as one by using Array.prototype.forEach.call();
Since nodelist doesn't have the function forEach we need to invoke it from the Array object. Then we pass a reference to links using the first argument of call.

add options to select - multiple selects

I have 4 selects and I want to append the options to all 4 selects using their class.
// groupings is just an array of strings
createElement: function(el, value, html) {
var htmlText = html || value;
var elm = document.createElement(el);
elm.value = value;
elm.innerHTML = htmlText;
return elm;
}
var htmlOptions = [];
$.each(groupings, function(i, e) {
htmlOptions.push(_util.createElement("option", e));
});
$('.groupBy_impact').append(htmlOptions);
This seems to work in Chrome, but in IE and Firefox, the values are overflown outside the select box. Not sure why.
If I do the following then it works cross-browser, but I want to avoid it
$('#select_groupBy_impact_1').append(htmlOptions);
$('#select_groupBy_impact_2').append($(htmlOptions).clone());
$('#select_groupBy_impact_3').append($(htmlOptions).clone());
$('#select_groupBy_impact_4').append($(htmlOptions).clone());
Something like this should work, if you want to avoid cloning.
$('.groupBy_impact').each(function() {
$(thid).append(htmlOptions);
});
This avoids cloning and iterates through all the elements in the collection.
It is important that all the elements you need to use this on, have the groupBy_impact class.
Furthermore .append() can take in different parameters.
You are using jQuery, why not make your .createElement(); return jQuery objects that are put into an array. this will be easier for jQuery to handle.
Or even better. Create a new jQuery collection (instead of an array) and append the created jQuery objects to that empty jQuery object, essentially creating a new collection of DOM elements to add.
try something like this
var htmlOptions_len = htmlOptions.length;
for(var i= 0;i<htmlOptions_len;i++ ){
$('.groupBy_impact').append(htmlOptions[i]);
}
or simply try something like this
$.each(groupings, function(i, e) {
$('.groupBy_impact').append(_util.createElement("option", e));
});

jQuery append() for multiple elements after for loop without flattening to HTML

I have a loop:
for (index = 0; index < total_groups; index += 1) {
groups[index].list_item = $(list_item_snippet);
// Closure to bind the index for event handling
(function (new_index) {
groups[index].list_item.find('.listing-group-title')
.html(groups[index].Group.Name)
.click(function(e){
fns.manageActiveGroup(new_index, groups);
return false;
});
})(index);
// Append to DOM
mkp.$group_listing.append(groups[index].list_item);
};
I would rather not call append() each time the loop fires.
I know that I could use a String and concatenate the markup with each loop iteration and append the string to mkp.$group_listing at the end, however this flattens the object and the bindings are lost (I am not relying on IDs).
Is there a way to perhaps add my objects to an array and append them all in one go at the bottom without flatening to HTML?
Assumptions:
$(list_item_snippet) contains some HTML defining a list item (and includes an element with class .listing-group-title).
groups is a block of JSON defining a 'group' in my script
The closure works perfectly
Edit:
Found that I can use the following syntax to append multiple elements:
mkp.$group_listing.append(groups[0].list_item, groups[1].list_item );
But i obviously need to automate it - it's not an array it's just optional additional function parameters so I'm not sure how to do this.
To append an array of elements to a selector you can use this:
$.fn.append.apply($sel, myArray);
In your case, since it's actually the .list_item property of each array element that you need you can use $.map to extract those first:
$.fn.append.apply(mkp.$group_listing, $.map(groups, function(value) {
return value.list_item;
}));
Instead of bind it the way you've done, if you bind it using on() like below,
$(document).on('click', '.listing-group-title', function() {
// click handler code here
});
You can flatten the HTML and append it in one statement and it'll still work.
Note: For better efficiency, replace document in the above statement to a selector matching the closest parent of .listing-group-title
Yes. Use the jQuery add method to add all your items to a jQuery object. Then append that one object.
http://api.jquery.com/add/
EDIT: Example:
var arr = $();
for (index = 0; index < total_groups; index += 1) {
groups[index].list_item = $(list_item_snippet);
// Closure to bind the index for event handling
(function (new_index) {
...
})(index);
// Add to jQuery object.
arr.add(groups[index].list_item));
};
mkp.$group_listing.append(arr);

Javascript array not working as expected

I'm pretty new to js/jquery. For each checkbox with the ID of check$ (where $ is a sequential number), I want to toggle the class "agree" of the surrounding span that uses the same check$ (but as a class). I don't want to have to hard-code the list of matching checkboxes, as this may vary.
Here's my code. This function works as expected:
agree = function (checkbox, span) {
$(checkbox).change(function(){
$(span).toggleClass('agree');
});
};
This is what I'm trying to pass to the above function, which does not work:
$(function() {
var elemid = 'check',
checks = Array($('[id^='+elemid+']').length);
console.log(checks);
for (i=0; i < checks; i++) {
agree('#'+elemid+checks[i], "."+elemid+checks[i]);
}
});
console.log(checks) returns [undefined × 4]. The number of elements is correct, but I don't know why it's undefined, or whether that is even significant.
The following code works as expected, but as I say, I'd rather not have to specify every matched element:
$(function() {
var checks = ["check1", "check2", "check3", "check4"];
for (i=0; i < checks.length; i++) {
agree('#'+checks[i], "."+checks[i]);
}
});
Thanks.
Edit: Thanks to Jack, I was overlooking the most simple method. I added the same class to all checkboxes and spans, and solved the problem with this:
$('input.check').change(function(){
$(this).closest('span.check').toggleClass('agree');
});
I might be totally missing something, but I'm pretty sure you are just trying to attach a change handler to each checkbox. In this case you can give them all the same class. I'm also guessing at your html structure for the span.
For reference:
http://api.jquery.com/closest/
http://docs.jquery.com/Tutorials:How_jQuery_Works
$('.yourcheckboxclass').change(function(){ //grab all elements with this class and attach this change handler
$(this).closest('span').toggleClass('agree');
});
The reason that the array is full of undefined values, is that you are just getting the number of items in the jQuery object, and create an array with that size. The jQuery object is discarded.
Put the jQuery object in the variable instead:
var elemid = 'check', checks = $('[id^='+elemid+']');
checks.each(function(){
agree(this, "."+elemid+checks[i]);
});

Categories

Resources