XPathResult and invalid iterator state - javascript

I have a relatively large (500-100 rows) HTML table with a bunch of <a> elements. I would like to add a <select> to the top of the page, and populate it by creating an <option> for each <a> in the table.
My first approach looked something like this:
var initSelect = function () {
var select = document.getElementById('mySelect');
var items = document.evaluate('//a', document, null, XPathResult.ANY_TYPE, null);
var item = items.iterateNext();
while (item) {
var elem = document.createElement("option");
var val = document.createAttribute("value");
val.value = elem.nodeValue;
elem.setAttributeNode(val);
elem.innerHTML = item.innerHTML;
select.appendChild(elem);
item = items.iterateNext();
}
};
window.onload = initSelect;
As soon as I tried to appendChild() to the <select> I got an UncaughtInvalidStateError. I figured that modifying the DOM was invalidating my XPathResult iterator, so I tried to add all of the <option> elements to an array first, and then appending them after iterating through all of the results.
var initSelect = function () {
var select = document.getElementById('src_select');
var items = document.evaluate('//a', document, null, XPathResult.ANY_TYPE, null);
var elems = [];
var item = items.iterateNext();
while (item) {
var elem = document.createElement("option");
var val = document.createAttribute("value");
val.value = elem.nodeValue;
elem.setAttributeNode(val);
elem.innerHTML = item.innerHTML;
elems.push(elem);
item = items.iterateNext();
}
for (var i = 0; i < elems.length; i++) {
select.appendChild(elems[i]);
}
};
window.onload = initSelect;
If I step through the code in the debugger, I see items.invalidIteratorState go to true after executing the elem.innerHTML = item.innerHTML line. Then I get the same error on the next call to items.iterateNext().
The first thing I'd like to get working is to just see the <select> populated. After that, the goal is to be able to select an element in the drop down, and have the page navigate to the same link that the corresponding <a> element would have taken me to.
This is the first JavaScript I've written, so I appreciate any and all feedback. At this point, I'm looking for a pure JavaScript solution. Once I get it working I'm going to try to pull JQuery in and revise it.

document.links gives you all a href elements in the document so there is no need to use the XPath API to access those elements. And if you are looking for elements in a particular parent then use e.g. document.getElementById('foo').getElementsByTagName('a') to find all a elements in that element with id attribute being foo. I don't see why you would need the DOM Level 3 XPath API for that, which is not supported in IE anyways. And neither document.links nor the result of getElementsByTagName can be invalidated like an XPath iterator result.
If you really want to use the XPath API then try a snapshot as the result type, it should not fail the way the iterator fails due to document manipulation.

Related

Dynamically setting options for a select elements skips first element

I've dynamically built an options array divided in two options group. These options groups are stored in a javascript array named ogs. The code for the same is as below:
var ogs = [];
for (optGroup in optionsList) {
var og = document.createElement('optgroup');
og.label = optGroup;
var ops = optionsList[optGroup];
for (op in ops) {
var o = document.createElement('option');
o.value = op;
o.text = ops[op];
og.appendChild(o);
}
ogs.push(og);
}
Now, I'm trying to add these options to 2 select elements, as below:
var from_el = document.getElementById('from_selector'), to_el = document.getElementById('to_selector');
for (i = 0; i < ogs.length; ++i) {
var og = ogs[i];
from_el.add(og);
to_el.add(og);
}
However, at the end of the script, only to_selector has the options populated, whereas from_selector remains empty. The reason I'm populating the options like this is because both these select elements use select2, and any other method (such as innerHTML) is taking significantly longer. I've also tried putting these selectors in array and iterating over them, always the last select gets populated, whereas first select remains empty.
Figured out the issue. The problem was in the following lines:
from_el.add(og);
to_el.add(og);
When og was getting added to to_el, it was getting detached from from_el. The solution was to replace this with.
from_el.add(og);
to_el.add(og.cloneNode(true));

Jquery ID selector doesnt work with a variable

I've read a billion questions like this, but never found an answer yet.
Anyway, when I type
var variableContainingID = "header";
var div = $("#"+variableContainingID);
It returns 'undefined'
But when I type
var variableContainingID = "header";
var div = $('[id^="'+variableContainingID+'"]');
It works fine.
Any ideas why?
UPDATE
var json = '{"divs":['
var children = $(".parent_container > div");
var idArray = [];
var numArray = [];
for (var x=0; x<children.length; x++) {
var eleid = $(children[x]).attr("id");
idArray.push('"'+eleid+'"');
numArray.push(x+1);
}
var idString = idArray.join(",");
var numString = numArray.join(",");
json += idString;
json += '],"number":['+numString+']}';
var obj = JSON.parse(json);
for (x in obj["divs"]) {
var div = $('[id^="'+obj["divs"][x]+'"]');
}
Do you think the double quotes could be throwing it off?
As you wrote in your question:
var div = $("#"+variableContainingID);
var div = $('[id^="'+variableContainingID+'"]');
These two lines are not identical. The first one, will select an element with id of header. The second one,
selects elements that have the specified id with a value beginning exactly with a given string (header).
So if you have an element like this:
<div id="headerHere"></div>
The first one ($("#"+variableContainingID)) can't select it, but the second one ($('[id^="'+variableContainingID+'"]')) can select that element.
This is because you used ^= in your selector. See jQuery API: Attribute Starts With Selector (name^="value").
It's worth to see all attribute selectors in jQuery.
Attribute Selectors in jQuery

Get all parents of an element including the element itself

I am trying to create an array of all the elements to where the user clicked in a content editable div. I have this working with the following code.
var els = [];
var target = event.target;
while (target){ //Create an array of parent elements
els.push(target); //Push target to the back of the array
target = target.parentNode;
}
But I was wondering if I could reduce this to one line with jQuery. jQuery .parents() almost gets me there but it doesn't include the first event.target
var els = $(event.target).parents();
Is there a way to include the element itself with .parents() or is there a better way of doing this?
How about andSelf ?
var els = $(event.target).parents().andSelf();
Mine would get you, great-grandparent, grandparent, parent, self.
If you want: self, parent, grandparent, great-grandparent, try extending Jquery with this:
$.fn.reverse = [].reverse;
and then doing:
var els = `$(event.target).parents().andSelf().reverse();
Example: jsFiddle
You can use add :
var els = $(event.target).parents().add(event.target);
or, if you want them in a different order :
var els = $(event.target).add($(event.target).parents());
If what you need is an array (ie not a jQuery object), you can use reverse :
var els = $(event.target).parents().add(event.target).get().reverse();

Remove multiple elements with same name using removeChild?

I have an element with multiple elements inside. All of the elements inside have the same name. Is there any way to remove them using one function?
(refer to this question for example Remove multiple children from parent?
Here's a solution that removes the first level children with the specified name for the parent with the specified id. If you want to go deeper, you can recursively call it on the child elements you get inside (you'll have to add a parent parameter as well).
function removeChildren (params){
var parentId = params.parentId;
var childName = params.childName;
var childNodes = document.getElementById(parentId).childNodes;
for(var i=childNodes.length-1;i >= 0;i--){
var childNode = childNodes[i];
if(childNode.name == 'foo'){
childNode.parentNode.removeChild(childNode);
}
}
}
And to call it:
removeChildren({parentId:'div1',childName:'foo'});
And a fiddle for testing:
Notes: You can only access the name element dependably in JavaScript when it supported on your element (e.g. NOT on DIVs!). See here for why.
UPDATE:
Here's a solution using className based on our conversation:
function removeChildren (params){
var parentId = params.parentId;
var childName = params.childName;
var childNodesToRemove = document.getElementById(parentId).getElementsByClassName('foo');
for(var i=childNodesToRemove.length-1;i >= 0;i--){
var childNode = childNodesToRemove[i];
childNode.parentNode.removeChild(childNode);
}
}
2021 Answer:
Perhaps there are lots of way to do it, such as Element.replaceChildren().
I would like to show you an effective solution with only one redraw & reflow supporting all ES6+ browsers.
function removeChildren(cssSelector, parentNode){
var elements = parentNode.querySelectorAll(cssSelector);
let fragment = document.createDocumentFragment();
fragment.textContent=' ';
fragment.firstChild.replaceWith(...elements);
}
Usage: removeChildren('.foo',document.body);: remove all elements with className foo in <body>
ok this should be easy. First get the parent element:
var theParent = document.getElementById("notSoHappyFather");
then get an array of the nodes that you want to remove:
var theChildren = theParent.getElementsByName("unluckyChild");
Lastly, remove them with a loop:
for (var i = 0; i < theChildren.length; i++)
{
theParent.removeChild(theChildren[i]);
}
A sample of your HTML would get you a more complete answer, but one can fairly easy call DOM functions to get the list of children and just remove them. In jQuery, remove all children would be something like this:
$("#target > *").remove();
or
$("#target").html("");
And, you can see a demo here: http://jsfiddle.net/jfriend00/ZBYCh/
Or, not using jQuery you could also do:
document.getElementById("target").innerHTML = "";
If you're trying to only remove a subset of the children (and leave others intact), then you need to be more specific how one would determine which children to leave and which to remove. In jQuery, you could use a .find() select or a filter() selector to narrow the list of children to just the children you wanted to target for removal.

trouble creating nested dom nodes in javascript

I've a function that takes an object as a parameter, and uses the structure of the object to create nested DOM nodes, but I receive the following error:
http://new.app/:75NOT_FOUND_ERR: DOM Exception 8: An attempt was made to reference a Node in a context where it does not exist.
What I would like my function to do, is, when supplied with a suitable object as a parameter, example:
var nodes = {
tweet: {
children: {
screen_name: {
tag: "h2"
},
text: {
tag: "p"
}
},
tag: "article"
}
};
It would create the following DOM nodes:
<article>
<h2></h2>
<p></p>
</article>
Here is my attempt so far:
function create(obj) {
for(i in obj){
var tmp = document.createElement(obj[i].tag);
if(obj[i].children) {
tmp.appendChild(create(obj[i].children)); /* error */
};
document.getElementById("tweets").appendChild(tmp);
};
};
I'm already struggling!
Ideally I'd like to eventually add more child key's to each object, not just tag, but also id, innerHTML, class etc.
Any hel would be much appreciated, though please: I'm sure a framework or library could do this for me in just a few lines of code, or something similar, but I'd prefer not to use one for this particular project.
If you could briefly explain your answers too it'd really help me learn how this all works, and where I went wrong!
Thank you!
NB: I've changed and marked the line in my function that the error message is talking about.
I changed it from:
mp.appendChild(obj[i].children);
to:
mp.appendChild(create(obj[i].children));
This is because I want any nested keys in the children object to also be created, so screen_name had a children key, they too would be created. Sorry, I hope you can understand this!
I'm looking at http://jsperf.com/create-nested-dom-structure for some pointers, this may help you too!
Your "create" function is going to have to be written recursively.
To create a node from your data (in general), you need to:
Find the "tag" property and create a new element
Give the element the "id" value of the element (taken from the data)
For each element in "children", make a node and append it
Thus:
function create(elementDescription) {
var nodes = [];
for (var n in elementDescription) {
if (!elementDescription.hasOwnProperty(n)) continue;
var elem = elementDescription[n];
var node = document.createElement(elem.tag);
node.id = n; // optional step
var cnodes = create(elem.children);
for (var c = 0; c < cnodes.length; ++c)
node.appendChild(cnodes[c]);
nodes.push(node);
}
return nodes;
}
That will return an array of document elements created from the original "specification" object. Thus from your example, you'd call:
var createdNodes = create(nodes);
and "createdNodes" would be an array of one element, an <article> tag with id "tweets". That element would have two children, an <h2> tag with id "screen_name" and a <p> tag with id "text". (Now that I think of it, you might want to skip the "id" assignment unless the node description has an explicit "id" entry, or something.)
Thus if you have a <div> in your page called "tweets" (to use your example, though if so you'd definitely want to cut out the "id" setting part of my function), you'd add the results like this:
var createdNodes = create(nodes), tweets = document.getElementById('tweets');
for (var eindex = 0; eindex < createdNodes.length; ++eindex)
tweets.appendChild(createdNodes[eindex]);
I added a function appendList that accepts a list of elements, and the container to append to. I removed the append to "tweets" part out of the create function to more effectively separate your code.
function create(obj) {
var els = [];
for(i in obj){
var tmp = document.createElement(obj[i].tag);
var children;
if(children = obj[i].children) {
var childEls = create(children);
appendList(childEls, tmp);
}
els.push(tmp);
};
return els;
};
function appendList(list, container){
for(var i = 0, el; el = list[i]; i++){
container.appendChild(el);
}
};
// gets an array of root elements populated with children
var els = create(nodes);
// appends the array to "tweets"
appendList(els, document.getElementById("tweets"));
Building on the previous answer:
I think you still need to create the element you're trying to append:
tmp.appendChild(children[prop].tag);
should be
tmp.appendChild(document.createElement(children[prop].tag));
function create(obj) {
for(i in obj){
var tmp = document.createElement(obj[i].tag);
var children;
if(children = obj[i].children) {
for(var prop in children)
tmp.appendChild(document.createElement(children[prop].tag));
}
document.getElementById("tweets").appendChild(tmp);
};
};

Categories