getElementsByClassName vs querySelectorAll - javascript

if I use
var temp = document.querySelectorAll(".class");
for (var i=0, max=temp.length; i<max; i++) {
temp[i].className = "new_class";
}
everything works fine. All nodes change their classes.
But, with gEBCN:
var temp = document.getElementsByClassName("class");
for (var i=0, max=temp.length; i<max; i++) {
temp[i].className = "new_class";
}
I get error. Code jumps out of the loop at some point, not finishing the job with msg "can't set className of null".
I understand that this is static vs live nodelist problem (I think), but since gEBCN is much faster and I need to traverse through huge list of nodes (tree), I would really like to use getElementsByClassName.
Is there anything I can do to stick with gEBCN and not being forced to use querySelectorAll?

That's because HTMLCollection returned by getElementsByClassName is live.
That means that if you add "class" to some element's classList, it will magically appear in temp.
The oposite is also true: if you remove the "class" class of an element inside temp, it will no longer be there.
Therefore, changing the classes reindexes the collection and changes its length. So the problem is that you iterate it catching its length beforehand, and without taking into account the changes of the indices.
To avoid this problem, you can:
Use a non live collection. For example,
var temp = document.querySelectorAll(".class");
Convert the live HTMLCollection to an array. For example, with one of these
temp = [].slice.call(temp);
temp = Array.from(temp); // EcmaScript 6
Iterate backwards. For example, see #Quentin's answer.
Take into account the changes of the indices. For example,
for (var i=0; i<temp.length; ++i) {
temp[i].className = "new_class";
--i; // Subtract 1 each time you remove an element from the collection
}
while(temp.length) {
temp[0].className = "new_class";
}

Loop over the list backwards, then elements will vanish from the end (where you aren't looking any more).
for (var i = temp.length - 1; i >= 0; i--) {
temp[i].className = "new_class";
}
Note, however, that IE 8 supports querySelectorAll but not getElementsByClassName, so you might want to prefer querySelectorAll for better browser support.
Alternatively, don't remove the existing class:
for (var i=0, max=temp.length; i<max; i++) {
temp[i].className += " new_class";
}

Related

getElementsByClassName isn't returning all elements

I'm creating a button that I should highlight certain words within a specified class, but I am having issues with it returning all elements within the class. It will only work if I specify an index, so I'm assuming there may be something wrong with the existing "for loop". Any help is appreciated!
This will work, but only "highlights" the first element in the class, of course:
var bodyText = document.getElementsByClassName('test')[0].innerHTML;
for (var i = 0; i < searchArray.length; i++) {
bodyText = doHighlight(bodyText, searchArray[i], highlightStartTag,
highlightEndTag);}
document.getElementsByClassName('test')[0].innerHTML = bodyText;
return true;
This will not work at all:
var bodyText = document.getElementsByClassName('test').innerHTML;
for (var i = 0; i < searchArray.length; i++) {
bodyText = doHighlight(bodyText, searchArray[i], highlightStartTag,
highlightEndTag);}
document.getElementsByClassName('test').innerHTML = bodyText;
return true;
If you want to replace multiple words in multiple elements, you need two loops:
const testElements = document.getElementsByClassName('test');
for (const element of testElements) {
for (const search of searchArray) {
element.innerHTML = doHighlight(element.innerHTML, search, highlightStartTag, highlightEndTag);
}
}
As you can see getElementsByClassName is pluralized (Elements). Indeed a same class can be assigned to multiple HTML elements. You won't find any way to ommit the [0] and you shouldn't anyway as it might mean you're getting data from the wrong node. If you need data from a specific element that you can ensure is unique then you need to give it an id and use getElementById instead.
You cannot access innerHTML in something which returns an htmlcollection
document.getElementsByClassName('test').innerHTML
Because it's written in plain english: getElementsByClassName. plural.
"Elements".
with an "s" at the end...
meaning it's a (sort of) Array (an htmlcollection)

Select and add class in javascript

Cross Platform if possible, how can I select classes in Javascript (but not Jquery please -MooTools is fine though-) on code that I can't add an ID?
Specifically, I want to add the class "cf" on any li below:
HTML
<div class="itemRelated">
<ul>
<li class="even">
<li class="odd">
<li class="even">
I tried to fiddle it but something is missing:
Javascript
var list, i;
list = document.getElementsByClassName("even, odd");
for (i = 0; i < list.length; ++i) {
list[index].setAttribute('class', 'cf');
}
JSFiddle
ps. This question phenomenally has possible duplicates, (another one) but none of the answers makes it clear.
Using plain javascript:
var list;
list = document.querySelectorAll("li.even, li.odd");
for (var i = 0; i < list.length; ++i) {
list[i].classList.add('cf');
}
Demo
For older browsers you could use this:
var list = [];
var elements = document.getElementsByTagName('li');
for (var i = 0; i < elements.length; ++i) {
if (elements[i].className == "even" || elements[i].className == "odd") {
list.push(elements[i]);
};
}
for (var i = 0; i < list.length; ++i) {
if (list[i].className.split(' ').indexOf('cf') < 0) {
list[i].className = list[i].className + ' cf';
}
}
Demo
Using Mootools:
$$('.itemRelated li').addClass('cf');
Demo
or if you want to target specific by Class:
$$('li.even, li.odd').addClass('cf');
Demo
I know this is old, but is there any reason not to simply do this (besides potential browser support issues)?
document.querySelectorAll("li.even, li.odd").forEach((el) => {
el.classList.add('cf');
});
Support: https://caniuse.com/#feat=es5
Using some newer browser objects and methods.
Pure JS:
Details: old fashioned way, declaring stuff at the beginging than iterating in one big loop over elements with index 'i', no big science here. One thing is using classList object which is a smart way to add/remove/check classes inside arrays.
var elements = document.querySelectorAll('.even','.odd'),
i, length;
for(i = 0, length = elements.length; i < length; i++) {
elements[i].classList.add('cf');
}
Pure JS - 2:
Details: document.querySelectorAll returns an array-like object which can be accessed via indexes but has no Array methods. Calling slice from Array.prototype returns an array of fetched elements instantly (probably the fastest NodeList -> Array conversion). Than you can use a .forEach method on newly created array object.
Array.prototype.slice.call(document.querySelectorAll('.even','.odd'))
.forEach(function(element) {
element.classList.add('cf');
});
Pure JS - 3:
Details: this is quite similar to v2, [].map is roughly that same as Array.prototype.map except here you declare an empty array to call the map method. It's shorter but more (ok little more) memory consuming. .map method runs a function on every element from the array and returns a new array (adding return in inside function would cause filling the returned values, here it's unused).
[].map.call(document.querySelectorAll('.even','.odd'), function(element) {
element.classList.add('cf');
});
Pick one and use ;)
querySelectorAll is supported in IE8, getElementsByClassName is not, which does not get two classes at the same time either. None of them work in iE7, but who cares.
Then it's just a matter of iterating and adding to the className property.
var list = document.querySelectorAll(".even, .odd");
for (var i = list.length; i--;) {
list[i].className = list[i].className + ' cf';
}
FIDDLE
As others mention for selecting the elements you should use .querySelectorAll() method. DOM also provides classList API which supports adding, removing and toggling classes:
var list, i;
list = document.querySelectorAll('.even, .foo');
for (i = 0; i < list.length; i++) {
list[i].classList.add('cf');
}
As always IE9 and bellow don't support the API, if you want to support those browsers you can use a shim, MDN has one.
If you want to select elements with different classes all together then the best choice is querySelectorAll.
querySelectorAll uses CSS selectors to select elements. As we add the same CSS properties to different elements by separating them by a comma in the same way we can select those elements using this.
.even, .odd {
font-weight: bold;
}
Both elements with class 'even' and 'odd' get bold.
let list = document.querySelectorAll('.even, .odd');
Now, both the elements are selected.
+Point: you should use classList.add() method to add class.
Here is the complete code for you.
let list = document.querySelectorAll('.even, .odd');
for (i = 0; i < list.length; ++i) {
list.classList.add('cf');
}

Inserting html elements while DOM is changing

My code should insert HTML content in all divs that have a predefined class name, without using jQuery and at least compatible with IE8 (so no getElementsbyClass).
The html:
<div class="target">1</div>
<div class="target">2</div>
<div class="target">3</div>
<div class="target">4</div>
The javascript:
var elems = document.getElementsByTagName('*'), i;
for (wwi in elems) {
if((' ' + elems[wwi].className + ' ').indexOf(' ' + "target" + ' ') > -1) {
elems[wwi].innerHTML = "YES";
//elems[wwi].innerHTML = "<div>YES!</div>";
}
}
You can try it here.
As you can see inside each div the word YES is printed. Well the if you comment elems[wwi].innerHTML = "YES"; and replace that for elems[wwi].innerHTML = "<div>YES!</div>" the code fails. I suppose is because inserting div elements modify the DOM and in consequence the FOR cycle fails. Am i right?
Well i can solve this pretty ugly by recalling the for cycle each time i make an innerHTML, and when i insert the code i can add a class (like data-codeAlreadyInserted=1) to ignore the next time the FOR pass in that div. But again, this is pretty much a very bad solution since for an average site with many tags I can even freeze the user browser.
What do you think? lets suppose i dont know the amount of tags i insert on each innerHTML call.
"I suppose is because inserting div elements modify the DOM and in consequence the FOR cycle fails. Am i right?"
Pretty much. Your elems list is a live list that is updated when the DOM changes. Because you're adding a new div on every iteration, the list keeps growing and so you never get to the end.
To avoid this, you can either do a reverse iteration,
for (var i = elems.length-1; i > -1; i--) {
// your code
}
or convert the list to an Array.
var arr = [];
for (var i = 0, len = list.length; i < len; i++) {
arr.push(elems[i]);
}
for (i = 0; i < len; i++) {
// your code
}
Another way is to use replaceChild instead of innerHTML. It works better and it's way faster:
var newEl = elem[wwi].cloneNode(false);
newEl.innerHTML = html;
elem[wwi].parentNode.replaceChild(newEl, elem[wwi]);
You can take a copy of the live node list:
var nodes = [];
for (var i = 0, n = elems.length; i < n; ++i) {
nodes.push(elems[i]);
}
and then use a proper for loop, not for ... in to iterate over the array:
for (var i = 0, n = nodes.length; i < n; ++i) {
...
}
for ... in should only be used on objects, not arrays.

jQuery appending an array of elements

For the purpose of this question lets say we need to append() 1000 objects to the body element.
You could go about it like this:
for(x = 0; x < 1000; x++) {
var element = $('<div>'+x+'</div>');
$('body').append(element);
}
This works, however it seems inefficient to me as AFAIK this will cause 1000 document reflows. A better solution would be:
var elements = [];
for(x = 0; x < 1000; x++) {
var element = $('<div>'+x+'</div>');
elements.push(element);
}
$('body').append(elements);
However this is not an ideal world and this throws an error Could not convert JavaScript argument arg 0 [nsIDOMDocumentFragment.appendChild]. I understand that append() can't handle arrays.
How would I using jQuery (I know about the DocumentFragment node, but assume I need to use other jQuery functions on the element such as .css()) add a bunch of objects to the DOM at once to improve performance?
You could use an empty jQuery object instead of an array:
var elements = $();
for(x = 0; x < 1000; x++) {
elements = elements.add('<div>'+x+'</div>');
// or
// var element = $('<div>'+x+'</div>');
// elements = elements.add(element);
}
$('body').append(elements);
This might be useful if you want to do stuff with newly generated element inside the loop. But note that this will create a huge internal stack of elements (inside the jQuery object).
It seems though that your code works perfectly fine with jQuery 1.8.
You could just call
$('body').append(elements.join(''));
Or you can just create a large string in the first place.
var elements = '';
for(x = 0; x < 1000; x++) {
elements = elements + '<div>'+x+'</div>';
}
$(document.body).append(elements);
Like you mentioned, probably the most "correct" way is the usage of a DocFrag. This could look like
var elements = document.createDocumentFragment(),
newDiv;
for(x = 0; x < 1000; x++) {
newDiv = document.createElement('div');
newDiv.textContent = x;
elements.append( newDiv );
}
$(document.body).append(elements);
.textContent is not supported by IE<9 and would need an conditional check to use .innerText or .text instead.
Upgrade to jQuery 1.8, this works as intended:
​$('body')​.append([
'<b>1</b>',
'<i>2</i>'
])​;​
Since $.fn.append takes a variable number of elements we can use apply to pass the array as arguments to it:
el.append.apply(el, myArray);
This works if you have an array of jQuery objects. According to the spec though you can append an array of elements if you have the DOM elements. If you have an array of html strings you can just .join('') them and append them all at once.
A slight change to your second approach:
var elements = [],
newDiv;
for (x = 0; x < 1000; x++) {
newDiv = $('<div/>').text(x);
elements.push(newDiv);
}
$('body').append(elements);
$.append() certainly can append an array: http://api.jquery.com/append/
.append(content) | content: One or more additional DOM elements, arrays of elements, HTML strings, or jQuery objects to insert at the end of each element in the set of matched elements.
Sometimes, jQuery isn't the best solution. If you have a lot of elements to append to the DOM, documentFragment is a viable solution:
var fragment = document.createDocumentFragment();
for(var i = 0; i < 1000; i++) {
fragment.appendChild(document.createElement('div'));
}
document.getElementsByTagName('body')[0].appendChild(fragment);
If you're going for raw performance then I would suggest pure JS, though some would argue that your development performance is more important than your site's/program performance.
Check this link for benchmarks and a showcase of different DOM insertion techniques.
edit:
As a curiosity, documentFragment proves to be one of the slowest methods.
I would use native Javascript, normally much faster:
var el = document.getElementById('the_container_id');
var aux;
for(x = 0; x < 1000; x++) {
aux = document.createElement('div');
aux.innerHTML = x;
el.appendChild(aux);
}
EDIT:
There you go a jsfiddle with different options implemented. The #jackwander's solution is, clearly, the most effective one.
I know, the question is old, but maybe it helps others.
Or simple use ECMA6 spread operator:
$('body').append(...elements);

Element sort routine works in Firefox, but crashes in Chrome

I've written the following routine to sort the option elements within a select:
function SortSelect(select)
{
var tmpAry = new Array();
for (var i in select.options)
tmpAry[i] = select.options[i];
tmpAry.sort(function(opta, optb)
{
if (opta.id > optb.id) return 1;
if (opta.id < optb.id) return -1;
return 0;
});
while (select.options.length > 0)
select.options[0] = null;
for (var i in tmpAry)
select.appendChild(tmpAry[i]);
}
It works just fine with Firefox as part of a Greasemonkey script. However, in Chrome, both with and without TamperMonkey, I get this:
Uncaught Error: NOT_FOUND_ERR: DOM Exception 8
SortSelect:125
As is typical for Javascript debuggers, the error line number is totally wrong, so it's difficult to pin down exactly why this is breaking and where. I'm open to suggestions as to why the code is bugging out or ways to effectively debug it (I'm new to Chrome). Thanks.
You should iterate over the options collection using an index, not a for..in loop. The following:
for (var i in select.options) {
tmpAry[i] = select.options[i];
should be:
var options = select.options;
for (var i=0, iLen=options.length; i<iLen; i++) {
tmpAry[i] = options[i];
}
You are likely getting properties from the options collection that aren't option elements, such as length.
You also should not assign "null" to an option. If you want to remove all the options, just set the length of options to zero:
var options.length = 0;
Finally, you should iterate over tmpAray using an index since for..in will not return the options in the same order in every browser and may return non-numeric enumerable properties if there are any, use an index. Also, you can just assign the option back to the select's options collection, there is no need for appendChild:
select.options.length = 0;
for (var i=0, iLen=tmpAry.length; i<iLen; i++) {
select.options[i] = tmpAry[i];
}
If you are not removing any options, you should be able to just assign them in the new order, but some browsers can't handle that so removing them first is best.
Edit
Note that while the options property of a select element is readonly, the properties of an options collection are not. You can assign values (which should be references to option elements) directly to them.
What are you trying to do with this piece of code?
while (select.options.length > 0)
select.options[0] = null;
If the answer is you're trying to clear all the select options, that seems dangerous. I could see how this could easily be an infinite loop.
It looks to me like this would be a lot safer:
for (var i = select.options.length - 1; i > 0; i--) {
select.remove(i);
}
Then, there's an select.options.add() method for adding them back.
FYI, it is also considered risky practice to use this construct on arrays or pseudo arrays:
for (var i in select.options)
for (var i in tmpAry)
as that can pick up properties that have been added to the object in addition to just array elements.
More typing, but safer to use:
for (var i = 0, len = tmpAry.length; i < len; i++) {
// code here
}
These lines can cause an infinite loop or an access violation:
while (select.options.length > 0)
select.options[0] = null;
Also, you do not need to delete the nodes and reinsert them; appendChild() works fine in all browsers to move nodes around.
So, this code will work and should be more efficient than removing and recreating nodes (which can also trash any event listeners). :
See it in action at jsFiddle.
function SortSelect (select)
{
var tmpAry = [];
for (var J = select.options.length - 1; J >= 0; --J)
tmpAry.push (select.options[J] );
tmpAry.sort ( function (opta, optb) {
if (opta.id > optb.id) return 1;
if (opta.id < optb.id) return -1;
return 0;
} );
while (tmpAry.length) {
select.appendChild ( document.getElementById (tmpAry[0].id) );
tmpAry.shift ();
}
}

Categories

Resources