I have an array of elements and I want use inserBefore on each of them as I iterate through the array. It's supposed to add the element-to-be-inserted after each of the elements in the array but it only adds it to the last element in the array. I thought it was a closure issue but even after using closures i still get the problem. Without closures I tested it by changing the class name to the key value that the array was on and it changed it no problem.
for(var i in elems){
var refElem = elems[i];
refElem.parentNode.insertBefore(elementToInsert, refElem.nextSibling);
}
Here's the code minus the closures. How do I get the elementToInsert added to each element in the array?
It's supposed to add the element-to-be-inserted after each of the elements
That's where the trouble arises, you cannot insert one element multiple times into the DOM. If you try to, it will just remove the element from the DOM before inserting it somewhere (again). So you will need to create distinct elements for each turn of the loop, for example by cloning your elementToInsert.
Btw, never use for…in-loops with arrays!
Just like Bergi said, the element you are inserting is actually being added and then removed from the node.
for(var i in elems){
var refElem = elems[i];
refElem.parentNode.insertBefore(elementToInsert.CloneNode(), refElem.nextSibling);
}
You can also inform the parameter 'deep', which will clone all the child nodes too.
https://developer.mozilla.org/en-US/docs/DOM/Node.cloneNode
just a little correction of the post before:
for(var i in elems){
var refElem = elems[i];
refElem.parentNode.insertBefore(elementToInsert.cloneNode(), refElem.nextSibling);
}
the function cloneNode() starts with a lowercase.
Related
Traditionally, a suggested way of removing a node's children in Javascript is to do something like this:
while(node.firstChild) {
node.removeChild(node.firstChild);
}
Recently, I attempted to remove all of a node's children using the built in forEach() method:
node.childNodes.forEach(child => {
node.removeChild(child);
}
This didn't function as I expected. Instead of removing all child nodes, the forEach stopped executing, leaving nodes leftover. What I ended up having to do was use Array.from:
Array.from(node.childNodes)
And then I could remove nodes with forEach. The reason I could not use the traditional method mentioned above is because for some reason, one child was always left behind, causing an infinite loop.
Why does the childNodes.forEach method not remove all the nodes as I thought it would? What am I misunderstanding?
node.childNodes is a live collection. As you remove items from it, the collection itself is modified (live while you're iterating). Trying to iterate it as you are, causes elements to be removed from the collection and moved down in the array-like structure while you're iterating it, causing you to miss nodes.
As an example, when you call removeChild() on the 2nd element in the collection, that element itself is then removed from the collection. That causes what was the 3rd element to be moved into the spot in the collection where the 2nd element was. Now, your loop moves on to the 3rd element in the collection. But, that will skip over the element that is now in the 2nd position causing you to never remove it.
That means the only safe way to iterate through the actual collection and remove things is with a backwards traversal because removing things form the end does not cause other elements to change their position in the collection. Removing items from the front (which is what you were doing) does cause items to move in the collection.
Array.from() converts the live collection to a static array where items are not removed from the array while deleting items from the DOM.
I have a personal rule of DOM development to NEVER use a live collection while I'm modifying the DOM in any way because the danger that the live collection gets modified while I'm trying to use it is too high. Array.from() is a very simple way to get a copy of a live collection that's static and is safe to work with, even as the DOM is being modified.
Another safe way to delete them all is with this backwards iteration because items are removed from the end of the live collection which doesn't cause any items to move in the collection that you haven't yet processed:
for (let i = node.childNodes.length - 1; i >= 0; i--) {
node.removeChild(node.childNodes[i]);
}
But, I generally find this more cumbersome than just converting to a static array with Array.from() as you've already discovered.
node.childNodes is a live collection, so when you remove a child from node in the forEach you mess with the iterator.
https://developer.mozilla.org/en-US/docs/Web/API/Node/childNodes
I'm looking for a way to associate a DOM element with a unique number, such that no other element in the DOM will be associated with that number. Obviously, I can't use an Id attribute because not all elements have an Id.
The most obvious way to do this is to (somehow) acquire a number that will give the element's position within the DOM, but I'm not sure if this is feasible. Ultimately, given an arbitrary element from the DOM, I'd like to have a way of mapping that element to a number.
Everyone is asking why I need to do this -- given a DOM element, I need to use that element as a key in a JS Object. JS Objects must be strings. So, technically, I do not need a unique number, per se, but I need a unique value that can be turned into a "short string" and used as the key in a JS object.
Technically, I do not need a unique number, per se, but I need a unique value that can be turned into a "short string" and used as the key in a JS object.
I see that you have jQuery tagged. This might be a possible solution to your problem. jQuery has a way to associate an element to an object:
var ele = /* your element */
$(ele).data({name: "Paul"});
// later you can use the element as a key
$(ele).data(); //returns {name: "Paul"}
This will avoid the whole "assigning unique id" mess and just let jQuery does all the hard work for you (creating a map data structure).
Edit by Roamer-1888
Basically, I need to "tag" an element uniquely so that if I encounter the same element in the future, I will know I've seen it before. The "tag" value will be the key in my JS object. I will associate various values in the tagged element. I don't want to use Map or WeakMap because they might not be supported on all browsers.
To meet this requirement, you would typically use jQuery's .data() as follows :
//...
var elementData = $(ele).data('myApplication');
if(!elementData) { // this element was not previously encountered
elementData = {
prop_A: ...,
prop_B: ...,
prop_C: ...
};
$(ele).data('myApplication', elementData);
}
// Here `elementData` is guaranteed to exist and its properties can be read/written.
//...
This technique is particularly useful for preseving state in jQuery plugins. Here is an example
Multiple applications/modules can do this without interfering with each other.
I'm guessing that there might be a better way to solve your problem if we understood what the real problem was. But, at any point in time, you can find what position an element is at (if it is currently inserted in the document) with something like this:
var items = document.getElementsByTagName("*");
If you needed to then get a unique index for a DOM element (at this particular point in time), you can do so by just searching for it in that HTMLCollection. It's position in the collection is guaranteed to be a unique index.
But, of course as the DOM is modified, this HTMLCollection will change as will the index.
If you wanted to assign a non-changing unique index, you could just assign a property to each DOM element based on a monotomically increasing counter.
var idCntr = 1;
function assignIds() {
var items = document.getElementsByTagName("*");
for (var i = 0; i < items.length; i++) {
if (!items[i]._uniqueId) {
items[i]._uniqueId = idCntr++;
}
}
}
You can call this function as many times as you want and it will assign new unique IDs to any DOM elements that don't yet have an id, but will leave ids that were already assigned the same (so they will never change).
If you just want to be able to generate a unique ID for any given DOM node such that you can always have the same ID for that DOM node, then you can just again use a monotomically increasing counter.
var idCntr = 1;
function assignId(elem) {
if (!elem._uniqueId) {
elem._uniqueId = idCntr++;
}
return elem._uniqueId;
}
You can then call this on any DOM element that you want to assign a unique ID to and it will both assign the id to the element and return the id that it assigned. If you pass it an element that already has an id, it will leave that id in place and just return it.
Based on your latest edit, it appears you're just trying to generate an id string that you can use as a key in a JS object. You can certainly use the above assignId() function for that.
In a modern browser, you can also use a Map or a
WeakMap object which will accept the DOM object itself as the key - you don't need to manufacture your own string key. You can then look it up directly with the DOM element too (since it's the key).
I'm encountering some very strange behaviour with JavaScripts new classList API, say we have the following HTML code:
<p class="testing">Lorem Ipsum</p>
<p class="testing">Lorem Ipsum</p>
And the following JavaScript code:
var elements = document.getElementsByClassName("testing");
alert(elements.length);
elements[0].classList.remove("testing");
alert(elements.length);
The first alert will give you a value of 2, whilst the second alert returns 1.
It appears that removing the class from the element is also removing it from the elements HTMLCollection, which makes absolutely no sense to me.
You can see an example of this code HERE.
I encountered this problem when trying to remove a certain class from some elements using code like below:
var elements = document.getElementsByClassName('testing');
var elementsLength = elements.length - 1;
for(var i = 0; i <= elementsLength ; i++)
{
elements[i].classList.remove('testing');
}
Say we have two elements like in the example above, the loop runs successfully the first time, but the second time it's looking for an element in the HTMLCollection that no longer exists, so I get something like "TypeError: elements[i] is undefined".
You can see an example of the above code HERE
This is frustrating to say the least, I can't understand why/how classList.remove could effect what is effectively an array set only once before the classList.remove function is called. I can't even seem to find anything about this behaviour online.
Am I doing something crazy? Or has I unearthed some strange hidden feature of the classList api that no one knows about?
The collection returned by document.getElementsByClassName is live so if an element doesn't have that class anymore it will be removed from the collection.
You can either create a non-live copy of the collection:
var elements = [].slice.call(document.getElementsByClassName('testing'));
Or take in account that it's live:
while (elements.length) elements[0].classList.remove('element-focus');
Using document.getElementsByClassName returns a live HTMLCollection:
An HTMLCollection in the HTML DOM is live; it is automatically updated when the underlying document is changed.
Thus (as described in plalx's answer) if you remove an element's class, the element is removed from an HTMLCollection based on that class.
Instead you could use document.querySelectorAll which returns a static NodeList collection:
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.
So your code would change from
var elements = document.getElementsByClassName("testing");
with the class name "testing" as an argument, to
var elements = document.querySelectorAll(".testing");
with the class selector ".testing" as an argument.
And then you could iterate over elements which would be a static NodeList.
I'm trying to do three things onclick:
have element with id="notes_content" change display:none to display: block
have element with id="oct" change width = "1190px" to width = "550px"
have elements with class="oct_days" change width = "168px" to width = "73px"
Fiddle of full code: http://jsfiddle.net/ascottz/jX3wh/
The first two happen, but the third does not. I suspect it is a syntax error, but can't catch it myself.
getElementsByClassName returns an array of dom elements, it is not a single instance. You must loop over the array and style each element.
Have a look at the updated fiddle.
for( var i = 0; i < days.length; i++ ){
days[i].style.width = "73px";
}
http://jsfiddle.net/jX3wh/4/
document.getElementsByClassName returns somewhat an array of elements, so you cannot simply refer to it as a DOM element in hope that it will work as you refer to each element of the collection (that is possible in jQuery, btw), so you have to use the foreach loop (doesn't matter how are you gonna achieve this -- via simple for(), or for(x in y), or any other way).
I usually use Array.forEach function, but the specific type of an array returned by the document.getElementsByClassName does not have such function in prototype, so you have to use [].forEach.call(inWhat,function(what){}) syntax, or fallback to for(...) syntax.
Check out this: http://jsfiddle.net/jX3wh/1/
Dunno if it works.
Also, what the f is this???
<div onclick="javascript:showDiv();" class="oct_days">
I am really very surprised this works. You should use onclick="showDiv()" instead, I think.
Somebody, please, tell me how does it work!
No libraries please. Beyond my control.
I'm appending a document fragment to the dom. THIS ALL WORKS. No problemo.
How do I retain/retrieve the node list after the fragment is appended? Relevant code:
var frag = document.createDocumentFragment();
//add some content to the fragment
element.appendChild(frag);
Again, this works! I do NOT need a troubleshoot on how to add things to the dom!
If I set var e = element.appendChild(frag);, everything gets appended normally, but e = an empty document fragment.
I'm looking for some smooth magic voodoo here. Don't send me looping over the entire dom. The content could be anything, one or many nodes with or without children. If there's some trick with querySelectorAll or something that would be acceptable.
Thanks!
EDIT
Upon further poking, it appears that e above is in fact a returned reference to the frag var, that is empty after appending it to the dom. It's much like the elements were neatly slid off of the fragment into the dom, and the fragment just lays around empty.
It's exactly what you've described; when the elements are appended to an element, they're removed from the fragment to prevent memory leaks from lingering references.
One way to get those child nodes is to make a shallow copy of the fragment's childNodes before doing appendChild():
// make shallow copy, use Array.prototype to get .slice() goodness
var children = [].slice.call(frag.childNodes, 0);
element.appendChild(frag);
return children;
If you're looking just after appending it, the lastChild is the way to go. Use it like this:
var e = element.lastChild;
More info in SitePoint
From doc of DocumentFragment:
Various other methods can take a document fragment as an argument (e.g., any Node interface methods such as appendChild and insertBefore), in which case the children of the fragment are appended or inserted, not the fragment itself.
So appendChild method return the documentFragment but it's empty because its child nodes are inserted in your element object and (from appendChild doc):
[...] a node can't be in two points of the document simultaneously. So if the node already has a parent, it is first removed, then appended at the new position.
Now...
You can store in a cache array your children and after return it (or see Jack solution :-))
Like Jack's solution, but if you are using esnext you could use Array.from as
const children = Array.from(frag.children);
element.appendChild(frag);
return children;
If you have single main element in your document fragment, you can use .firstElementChild and that's all. To make multiple copies use .cloneNode(true).
Example - https://codepen.io/SergeiMinaev/pen/abLzQjZ
Thanks to #The Sloth's comment, I found this solution, which finally solved it for me:
var node = element.lastElementChild
In my case, element.lastChild returned a text node instead of the appended node.