How do you use createDocumentFragment to create seven nested div elements in one hit?
I want to create a container A which contains A1, A2, A3 & A4, and then A2a & A2b within A2.
Note: this is a follow-up question to this which explained createDocumentFragment, but not how to nest divs using it. The answer given was as follows (which was helpful as far as it went):
var fragment = document.createDocumentFragment();
function u1(tag, id, className){
var tag = document.createElement(tag);
tag.id = id;
tag.className = className;
fragment.appendChild(tag);
}
// call u1() seven times
document.getElementById('foo').appendChild(fragment);
Could someone explain how to nest? The above just adds seven children to 'foo'. I've trawled the web, but to no avail.
Thanks.
Rather than calling appendChild on the fragment (which creates a top level object in your fragment), you call appendChild on one of the other objects in your fragment and that nests into that object. Here's a pseudo code example that puts tag2 nested into tag.
var tag = document.createElement(tag);
tag.id = id;
tag.className = className;
fragment.appendChild(tag);
var tag2 = document.createElement(tag);
tag2.id = id2;
tag.className = className2;
tag.appendChild(tag2);
Note: you can also set tag.innerHTML and create a whole host of objects (including as many layers of nesting as you want) just from that HTML.
Once I made a recursive function that parsed an JSON object (received from the server) to a DocumentFragment. I need to dig it out again. Here is a JSON of that kind. Recursion start on 'children':
var input="query":"#toPasteId","child":{"#toPasteId":{"a":"div","style":"color:blue","children":[{"a":"span","textcontent":"blabla"},{"a":"div","style":"border: 5px solid red","textcontent":"blublub"}]}}
It might not help but probably you'll find out before I find my paddle.
PS:Found it.
,oParse=function(obj){
var query=obj.query
,curObj=obj.child
,frag=document.createDocumentFragment()
,d=document
,rParse=function(curObj,frag){
var curElem=d.createElement(curObj.a);
frag.appendChild(curElem);
delete curObj.a;
for(var elem in curObj){
switch(elem){
case 'child':
if(curObj.child.length){
for(var i=0;i<curObj.child.length;i++){
rParse(curObj.child[i],curElem);
}
}
else{
rParse(curObj.child,curElem);
}
break;
case 'style':
curElem.style.cssText=curObj[elem];
break;
default:
curElem[elem]=curObj[elem];
}
}
return frag;
};
d.querySelector(query).appendChild(rParse(curObj,frag));
};
oParse(input);
Related
I'm having trouble with taking an array of integers and creating a singly linked list with JavaScript. It sounds easy enough, but there's something I'm just not seeing with the function I have an I'd appreciate any help you can provide.
This is the constructor function I use to make nodes:
function ListNode(val) {
this.val = val;
this.next = null;
}
And this is the function I'm writing that is supposed to take an array and create a linked list out of it. The basic idea is just a while loop that shifts off the first value until there's nothing left to shift:
var createLinkedList = function(array) {
var head = new ListNode(parseInt(array[0]));
array.shift();
while(array.length) {
var prev = new ListNode(parseInt(array[0]));
head.next = head;
prev = head;
array.shift();
}
return head;
}
I've tried running this with a couple basic arrays and it always just returns the last value in the array instead of a linked list. Is there something simple I'm just not seeing here? Thanks in advance.
The problem is not with array.shift, but how you link the nodes together inside the while loop.
To chain the nodes together, essentially you need to do:
var new_node = new ListNode(parseInt(array[0]));
head.next = new_node;
new_node = new ListNode(parseInt(array[1]));
head.next.next = new_node;
new_node = new ListNode(parseInt(array[2]));
head.next.next.next = new_node;
.
.
.
I think you get the idea. So what you want to do is rework your while loop, store a reference of the tailNode so instead of calling head.next.next.next.next.next = newNode, you can call tailNode.next = newNode, and tailNode = newNode inside the while loop.
I have the following JavaScript that I'd like to translate to CoffeeScript:
function initPage() {
var tr = document.getElementsByTagName('tr')[0];
labs.forEach(function(lab) {
var td = document.createElement('td');
// Create a header for each lab.
var h2 = document.createElement('h2');
h2.innerHTML = lab.name;
td.appendChild(h2);
// Create a div for each machine in a given lab.
for(i = lab.first; i <= lab.last; i++) {
var machine = ((i < 10) ? "0" : "") + i;
var div = document.createElement('div');
div.setAttribute('id', lab.name + "-" + machine);
div.setAttribute('class', 'Grey');
div.innerHTML = machine;
td.appendChild(div);
}
// Append the new table data element to the table row.
tr.appendChild(td);
});
}
Right now my CoffeeScript translation looks something like this:
initPage = () ->
tr = document.getElementsByTagName('tr')[0]
labs.forEach (lab) ->
td = document.createElement 'td'
# Create a header for each lab.
h2 = document.createElement 'h2'
h2.innerHTML = lab.name
tr.appendChild h2
# Create a div for a machine given the machine number
createDiv = (i) ->
machine = if i < 10 then "0#{i}" else "#{i}"
div = document.createElement 'div'
div.setAttribute 'id', "#{lab.name}-#{machine}"
div.setAttribute 'class', 'Grey'
div.innerHTML = machine
td.appendChild div
# Create a div for each machine in a given lab
createDiv machine for machine in [lab.first..lab.last]
# Append the new table data element to the table row.
tr.appendChild td
Is there a better, more idiomatic way to create the divs for each lab? Would it be better to avoid the createDiv function and do something like:
for i in [lab.first..lab.last]
machine = if i < 10 then "0#{i}" else "#{i}"
div = document.createElement 'div'
div.setAttribute 'id', "#{lab.name}-#{machine}"
div.setAttribute 'class', 'Grey'
div.innerHTML = machine
td.appendChild div
The CoffeeScript language reference says
Most of the loops you'll write in CoffeeScript will be comprehensions over arrays, objects, and ranges.
and
Comprehensions should be able to handle most places where you otherwise would use a loop, each/forEach, map, or select/filter
I'm new to the idea of list comprehensions and want to make sure that I'm translating this code in a way that leverages the strengths of CoffeeScript appropriately.
Would it be better to avoid the createDiv function and just inline it?
Yes, that function looks a bit superfluous.
I'm new to the idea of list comprehensions and want to make sure that I'm translating this code in an appropriate way
The aim of list comprehensions is to build new lists, like map and filter would do it. And for which loops or (inappropriately) forEach was used often, manually pushing to an array.
However, your aim is not to create an array, but a DOM element only. Comprehensions don't help here, you will need to use them as loops to execute side effects.
I know that adding innerHTML to document fragments has been recently discussed, and will hopefully see inclusion in the DOM Standard. But, what is the workaround you're supposed to use in the meantime?
That is, take
var html = '<div>x</div><span>y</span>';
var frag = document.createDocumentFragment();
I want both the div and the span inside of frag, with an easy one-liner.
Bonus points for no loops. jQuery is allowed, but I've already tried $(html).appendTo(frag); frag is still empty afterward.
Here is a way in modern browsers without looping:
var temp = document.createElement('template');
temp.innerHTML = '<div>x</div><span>y</span>';
var frag = temp.content;
or, as a re-usable
function fragmentFromString(strHTML) {
var temp = document.createElement('template');
temp.innerHTML = strHTML;
return temp.content;
}
UPDATE:
I found a simpler way to use Pete's main idea, which adds IE11 to the mix:
function fragmentFromString(strHTML) {
return document.createRange().createContextualFragment(strHTML);
}
The coverage is better than the <template> method and tested ok in IE11, Ch, FF.
Live test/demo available http://pagedemos.com/str2fragment/
Currently, the only way to fill a document fragment using only a string is to create a temporary object, and loop through the children to append them to the fragment.
Since it's not appended to the document, nothing is rendered, so there's no performance hit.
You see a loop, but it's only looping through the first childs. Most documents have only a few semi-root elements, so that's not a big deal either.
If you want to create a whole document, use the DOMParser instead. Have a look at this answer.
Code:
var frag = document.createDocumentFragment(),
tmp = document.createElement('body'), child;
tmp.innerHTML = '<div>x</div><span>y</span>';
while (child = tmp.firstElementChild) {
frag.appendChild(child);
}
A one-liner (two lines for readability) (input: String html, output: DocumentFragment frag):
var frag =document.createDocumentFragment(), t=document.createElement('body'), c;
t.innerHTML = html; while(c=t.firstElementChild) frag.appendChild(c);
Use Range.createContextualFragment:
var html = '<div>x</div><span>y</span>';
var range = document.createRange();
// or whatever context the fragment is to be evaluated in.
var parseContext = document.body;
range.selectNodeContents(parseContext);
var fragment = range.createContextualFragment(html);
Note that the primary differences between this approach and the <template> approach are:
Range.createContextualFragment is a bit more widely supported (IE11 just got it, Safari, Chrome and FF have had it for a while).
Custom elements within the HTML will be upgraded immediately with the range, but only when cloned into the real doc with template. The template approach is a bit more 'inert', which may be desirable.
No one ever provided the requested "easy one-liner".
Given the variables…
var html = '<div>x</div><span>y</span>';
var frag = document.createDocumentFragment();
… the following line will do the trick (in Firefox 67.0.4):
frag.append(...new DOMParser().parseFromString(html, "text/html").body.childNodes);
#PAEz pointed out that #RobW's approach does not include text between elements. That's because children only grabs Elements, and not Nodes. A more robust approach might be as follows:
var fragment = document.createDocumentFragment(),
intermediateContainer = document.createElement('div');
intermediateContainer.innerHTML = "Wubba<div>Lubba</div>Dub<span>Dub</span>";
while (intermediateContainer.childNodes.length > 0) {
fragment.appendChild(intermediateContainer.childNodes[0]);
}
Performance may suffer on larger chunks of HTML, however, it is compatible with many older browsers, and concise.
createDocumentFragment creates an empty DOM "container". innerHtml and other methods work only on DOM nodes (not the container) so you have to create your nodes first and then add them to the fragment. You can do it using a painful method of appendChild or you can create one node and modify it's innerHtml and add it to your fragment.
var frag = document.createDocumentFragment();
var html = '<div>x</div><span>y</span>';
var holder = document.createElement("div")
holder.innerHTML = html
frag.appendChild(holder)
with jquery you simply keep and build your html as a string. If you want to convert it to a jquery object to perform jquery like operations on it simply do $(html) which creates a jquery object in memory. Once you are ready to append it you simply append it to an existing element on a page
Like #dandavis said, there is a standard way by using the template-tag.
But if you like to support IE11 and you need to parse table elements like '<td>test', you can use this function:
function createFragment(html){
var tmpl = document.createElement('template');
tmpl.innerHTML = html;
if (tmpl.content == void 0){ // ie11
var fragment = document.createDocumentFragment();
var isTableEl = /^[^\S]*?<(t(?:head|body|foot|r|d|h))/i.test(html);
tmpl.innerHTML = isTableEl ? '<table>'+html : html;
var els = isTableEl ? tmpl.querySelector(RegExp.$1).parentNode.childNodes : tmpl.childNodes;
while(els[0]) fragment.appendChild(els[0]);
return fragment;
}
return tmpl.content;
}
Here is a x-browser solution, tested on IE10, IE11, Edge, Chrome and FF.
function HTML2DocumentFragment(markup: string) {
if (markup.toLowerCase().trim().indexOf('<!doctype') === 0) {
let doc = document.implementation.createHTMLDocument("");
doc.documentElement.innerHTML = markup;
return doc;
} else if ('content' in document.createElement('template')) {
// Template tag exists!
let el = document.createElement('template');
el.innerHTML = markup;
return el.content;
} else {
// Template tag doesn't exist!
var docfrag = document.createDocumentFragment();
let el = document.createElement('body');
el.innerHTML = markup;
for (let i = 0; 0 < el.childNodes.length;) {
docfrag.appendChild(el.childNodes[i]);
}
return docfrag;
}
}
I would go with something like this..
function fragmentFromString(html) {
const range = new Range();
const template = range.createContextualFragment(html);
range.selectNode(template.firstElementChild);
return range;
}
// Append to body
// document.body.append(fragmentFromString(`<div>a</div>`).cloneContents())
This way you keep the content inside a Range object and you get all the needed methods for free.
You can find the list of all Range methods and properties here https://developer.mozilla.org/en-US/docs/Web/API/Range
Note: Remember to use detatch() method once you are done with it to avoid leaks and improve performance.
var html = '<div>x</div><span>y</span>';
var frag = document.createDocumentFragment();
var e = document.createElement('i');
frag.appendChild(e);
e.insertAdjacentHTML('afterend', html);
frag.removeChild(e);
To do this with as little lines as possible, you could wrap your content above in another div so you do not have to loop or call appendchild more than once. Using jQuery (as you mentioned is allowed) you can very quickly create an unattached dom node and place it in the fragment.
var html = '<div id="main"><div>x</div><span>y</span></div>';
var frag = document.createDocumentFragment();
frag.appendChild($(html)[0]);
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.
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);
};
};