Its not like I couldn't do it otherwise, but I'm just curious: Why does this code crashes the browser tab?
var links = document.getElementsByTagName("a");
for (var i = 0; i < links.length; i++) {
var a = document.createElement("A");
a.innerHTML = "[?]";
a.href = links[i].href; //this is the evil line
a.onclick = function () {
return false;
};
links[i].parentNode.appendChild(a);
}
Because the NodeList (I think they call it an HTMLCollection now) you get back from getElementsByTagName is live. So when you add a new a to the document, the browser adds it to the list you're looping through. Since you add another one each time you loop, you'll never reach the end of the loop.
If you want a disconnected array or collection instead, you can do:
var collection = document.querySelectorAll("a");
or
var array = Array.prototype.slice.call(document.getElementsByTagName("a"));
querySelectorAll supports the full range of CSS selectors. It's supported by all modern browsers, and also IE8. But it may be slower than cloning the getElementsByTagName NodeList (not that that usually matters).
Element.getElementsByTagName() returns a live HTMLCollection, meaning each time you add a new link element to the page, the length of links increases, leading to an infinite loop.
Related
I want to get all elements (svgs) that have a specific class and clone them inside a div.
const images = document.getElementsByClassName('image_svg'); // collection of all elements (around 5 or so of them)
const myDiv = document.getElementsByClassName('myDiv')[0];
for (let i = 0; i < images.length; i++) {
var clone = images[i].cloneNode(true);
myDiv.appendChild(clone);
}
When I execute my code, it runs forever and the browser stops responding. What am I doing wrong here?
Note, this is a pure JS solution, so no jQuery answers please.
The reason is getElementsByClassName() returns a live collection.
Try with Document.querySelectorAll() and Document.querySelector()
The Document method querySelectorAll() returns a static (not live) NodeList representing a list of the document's elements that match the specified group of selectors.
The Document method querySelector() returns the first Element within the document that matches the specified selector, or group of selectors. If no matches are found, null is returned.
const images = document.querySelectorAll('.image_svg'); // collection of all elements (around 5 or so of them)
const myDiv = document.querySelector('.myDiv');
I'm developing a chrome extension and having a problem with a nodelist type.
var compoentChange = document.getElementById("component_change");
var items = compoentChange.getElementsByTagName("option");
When I console.log(items), it shows [item: function]. When I expand it, it has all the option elements and length property.
The problem is that I can't access those elements. When I console.log(items.length), I get undefined.
How do I iterate through items variable?
for(i in items){} and for loop do not work.
NodeLists are array-like objects. You can iterate with regular for loop (not for..in):
for (var i = 0; i < items.length; i++) { ... }
Or you can convert this array-like object to a real array and use native array methods on it:
[].forEach.call(items, function(item) { ... });
You can still do items.length, so just make a for loop like this. I suggest pushing it into an array.
var myArray = [];
for(var i=0; i<items.length; i++){
myArray.push(items[i]);
}
Alright if this isn't an option maybe try something like this:
var myArray = [];
for(var i=0, e=1; i<e; i++ ){
if(items[i] != undefined){
e++;
myArray.push(items[i]);
}else{
break;
}
}
If you're logging the two during the pageload, the reason that you can console.log() the NodeList, but not the length attribute is because the NodeList is a "live" collection. Both are undefined until the DOM finishes loading, but because the NodeList is live, it will update in the Chrome console. The lengthattribute was undefined when it was logged, and because it's not live, it'll stay undefined in the console.
You can set a variable to reference the nodeList at any time, but wait until the DOM is ready before trying to use the data (using the document.ready function or perhaps document.addEventListener()).
I met similar problem with you. It turns out that it is because I should access data after DOM finishes loading.
document.addEventListener("DOMContentLoaded", function () {
divs = document.getElementsByTagName("div");
console.log(divs);
}, false);
for...of can be used for this situation. However due to a bug, this opportunity cannot be used on chromium.
My goal it to loop through a set of given elements, and replace there inner HTML with a linkifying Regex so I can convert HTML text in the form of http://*.*/* into http://*.*/*
So I'm running a bit of vanilla javascript:
for (var i = 0; i < document.getElementsByClassName('title').length; i++) {
var title = document.getElementsByClassName('title')[i]
title.innerHTML = title.innerHTML.replace(/(\b(https?|ftp|file):\/\/[-A-Z0-9+&##\/%?=~_|!:,.;]*[-A-Z0-9+&##\/%=~_|])/ig,"<a target='_blank' href='$1'>$1</a>")
}
Here's just the RegExp I'm using:
/(\b(https?|ftp|file):\/\/[-A-Z0-9+&##\/%?=~_|!:,.;]*[-A-Z0-9+&##\/%=~_|])/ig
So, why on earth would this loop cause the browser to hang? The loop is over text no longer than 256 characters and there are usually between 5 and 30 .title elements, definitely not the levels of data that would crash/hang a browser. I've only experienced it in Chrome/Safari, unsure if it happens in Firefox/Opera or not.
Try storing results.
var titles = document.querySelectorAll(".title");
// querySelectorAll is supported in slightly more browsers than getElementsByClassName
var l = titles.length, i, title;
for( i=0; i<l; i++) {
title = titles[i];
title.innerHTML = title.innerHTML.replace(/..../,'....');
}
If the hanging continues, it's probably because the regex is matching stuff you've already replaced. Try adding a negative lookahead to ensure there is no single quote immediately after the URL you are matching. (?=!')
I swear this was just working fine a few days ago...
elm = document.querySelectorAll(selector);
var frag = document.createDocumentFragment();
while (elm[0]){
frag.appendChild(elm[0]);
}
Right, so, this should append each node from our elm node list. When the first one is appended, the second "moves" to the first position in node list, hence the next one is always elm[0]. It should stop when the elm nodeList is completely appended. However, this is giving me an infinite loop. Thoughts?
EDIT - because I've gotten the same answer several times...
A nodeList is not an array, it is a live reference. When a node is "moved" (here, appended) it should be removed automatically from the node list. The answers all saying "you're appending the same element over and over" - this is what's happening, it shouldn't be. A for loop shouldn't work, because when the first node is appended, the next node takes its index.
2nd EDIT
So the question is now "why is the nodeList behaving as an array?". The node list SHOULD be updating every time a node is being appended somewhere. Most peculiar.
Solution (in case someone needs something to handle live + non-live node lists)
elm = (/*however you're getting a node list*/);
var frag = document.createDocumentFragment();
var elength = elm.length;
for (var b = 0; b<elength; b++){
if (elm.length === elength){
frag.appendChild(elm[b]);
} else {
frag.appendChild(elm[0].cloneNode());
}
}
Basically, just checking to see if the node list has changed length.
From the MDN Docs
Element.querySelectorAll
Summary
Returns a non-live NodeList of all elements descended from the element on which it is invoked that match the specified group of CSS selectors.
Syntax
elementList = baseElement.querySelectorAll(selectors);
where
elementList is a non-live list of element objects.
baseElement is an element object.
selectors is a group of selectors to match on.
From the docs above you can see it does not automatically remove it when you append it to another element since it is non live. Run a demo to show that feature.
var selector = "div";
elm = document.querySelectorAll(selector);
var frag = document.createDocumentFragment();
console.log("before",elm.length);
frag.appendChild(elm[0]);
console.log("after",elm.length);
When the code above runs, in the console you get.
before 3
after 3
If you want to do the while loop, convert to an array and shift() the items off
var selector = "div";
var elmNodeLIst = document.querySelectorAll(selector);
var frag = document.createDocumentFragment();
var elems = Array.prototype.slice.call(elmNodeLIst );
while (elems.length) {
frag.appendChild(elems.shift());
}
console.log(frag);
You are appending the first item in the node list, over and over and over. You never removing any items from the array, but always adding the first one to the fragment. And the first one is always the same.
elm = document.querySelectorAll(selector);
var frag = document.createDocumentFragment();
while (elm.length){
frag.appendChild(elm.shift());
}
This may be closer to what you meant to do. We can use while (elm.length) because as items get removed form the array, eventually length will be zero which is a flasy value and the loop will stop.
And we use elm.shift() to fetch the item from the array because that method will return the item at index zero and remove it from the array, which gives us the mutation of the original array we need.
I think you thought this might work because a node can only have one parent. Meaning adding somewhere removes it from the previous parent. However, elm is not a DOM fragment. It's just a aray (or perhaps a NodeList) that holds references to element. The array is not the parent node of these elements, it just holds references.
Your loop might work if you had it like this, since you are query the parent node each time for its children, a list of node that will actually change as you move around:
elm = document.getElementById(id);
var frag = document.createDocumentFragment();
while (elm.children[0]){
frag.appendChild(elm.children[0]);
}
I wouldn't have expected it to work in the first place.
Your elm array is initialized, and never updated. Even if the result from running document.querySelectorAll(selector); would return something different, this doesn't change your current references in the array.
You would either need to rerun the selector, or manually remove the first element in the array after appending it.
elm[0] is static and unchanging in above code
fix is as below
elm = document.querySelectorAll(".container");
var frag = document.createDocumentFragment();
console.log(elm);
var i=0;
while (elm[i]){
frag.appendChild(elm[i++]);
}
I didn't actually focus much on the code (and if it made sense -judging from the comments- or not); but if this worked a few days ago then the problem is in the input you are giving to your code selector.
That's when Unit Testing comes in handy. If you can remember the input with which the code worked, then you can make it work again and start debugging from there.
Otherwise, you are just lying to yourself.
It's an infinite loop as it's written right now because elm[0] always refers to the same element, and that element is never null (any non-null/non-zero result would be true). You also don't do anything with the elements themselves to make it iterate across the list. You should be using a for loop instead of a while or at least having some kind of indexer to try to traverse the collection.
elm = document.querySelectorAll(selector);
var frag = document.createDocumentFragment();
for (i= 0; i < elm.length; i++)
{
frag.appendChild(elm[i]);
}
Edit:
From the documentation:
A "live" collection
In most cases, the NodeList is a live collection. This means that changes on the DOM tree >are going to be reflected on the collection.
var links = document.getElementsByTagName('a'); // links.length === 2 for instance
document.body.appendChild( links[0].cloneNode(true) ); // another link is added to the document
// the 'links' NodeList is automatically updated
// links.length === 3 now. If the NodeList is the return value of document.querySelectorAll, it is NOT live.
Going on this documentation, your current usage of the method indicates you do not have a live NodeList. Thus appending will never modify the original list. You will either need to modify your usage within the loop to mirror this usage of .cloneNode(true) or iterate manually.
I need to get all the input objects and manipulate the onclick param.
The following does the job for <a> links. Looking for something like this for input tags.
for (var ls = document.links, numLinks = ls.length, i=0; i<numLinks; i++){
var link = unescape(ls[i].href);
link = link.replace(/\\'/ig,"#");
if(ls[i].href.indexOf("javascript:") == -1)
{
ls[i].href = "javascript:LoadExtern(\\""+link+"\\",\\"ControlPanelContent\\",true,true);";
}
}
(See update at end of answer.)
You can get a NodeList of all of the input elements via getElementsByTagName (DOM specification, MDC, MSDN), then simply loop through it:
var inputs, index;
inputs = document.getElementsByTagName('input');
for (index = 0; index < inputs.length; ++index) {
// deal with inputs[index] element.
}
There I've used it on the document, which will search the entire document. It also exists on individual elements (DOM specification), allowing you to search only their descendants rather than the whole document, e.g.:
var container, inputs, index;
// Get the container element
container = document.getElementById('container');
// Find its child `input` elements
inputs = container.getElementsByTagName('input');
for (index = 0; index < inputs.length; ++index) {
// deal with inputs[index] element.
}
...but you've said you don't want to use the parent form, so the first example is more applicable to your question (the second is just there for completeness, in case someone else finding this answer needs to know).
Update: getElementsByTagName is an absolutely fine way to do the above, but what if you want to do something slightly more complicated, like just finding all of the checkboxes instead of all of the input elements?
That's where the useful querySelectorAll comes in: It lets us get a list of elements that match any CSS selector we want. So for our checkboxes example:
var checkboxes = document.querySelectorAll("input[type=checkbox]");
You can also use it at the element level. For instance, if we have a div element in our element variable, we can find all of the spans with the class foo that are inside that div like this:
var fooSpans = element.querySelectorAll("span.foo");
querySelectorAll and its cousin querySelector (which just finds the first matching element instead of giving you a list) are supported by all modern browsers, and also IE8.
querySelectorAll returns a NodeList which has its own forEach method:
document.querySelectorAll('input').forEach( input => {
// ...
});
getElementsByTagName now returns an HTMLCollection instead of a NodeList. So you would first need to convert it to an array to have access to methods like map and forEach:
Array.from(document.getElementsByTagName('input')).forEach( input => {
// ...
});
var inputs = document.getElementsByTagName('input');
for (var i = 0; i < inputs.length; ++i) {
// ...
}