Does a documentFragment element reset itself? - javascript

This is a strange behavior I noticed. I did not reset a document but immediately used it after I had appended it, and the previous elements it contained were not there.
Should I be clearing it as such?
frag_inner = '';
Are there side-effects I'm not aware of?
frag_outer = NS.createDocumentFragment();
frag_inner = NS.createDocumentFragment();
NS.eachIndex(obj, function(val) {
// fragment element - inner
// image element
val.view_picture = (val.picture === '0') ? this.A.images + 'generic_large.jpg' :
this.A.pictures + val.h_file + '-2.jpg';
img_element = $A.createElement('img');
img_element.className = 'tweet';
img_element.src = val.view_picture;
frag_inner.appendChild(img_element);
// link element
link_element = $A.createElement('a');
link_element.className = 'tweet';
link_element.innerHTML = val.name + ' posted ' +
this.prettyTime(val.time) + '<br>';
frag_inner.appendChild(link_element);
// paragraph element
par_element = $A.createElement('p');
par_element.className = 'tweet';
par_element.innerHTML = val.tweet;
frag_inner.appendChild(par_element);
// div element
div_element = $A.createElement('div');
div_element.className = 'tweet';
div_element.appendChild(frag_inner);
// append the div which is now populated
frag_outer.appendChild(div_element);
}, this);

I think it's actually the expected behaviour, as...
1) it's a well-known feature of Node.appendChild to move existing Nodes. As said in the docs (MDN)...
[Node.appendChild]... adds a node to the end of the list of children of a specified
parent node. If the node already exists it is removed from current
parent node, then added to new parent node.
2) when you append documentFragment, you actually append all its children (MDN again):
Various other methods can take a document fragment as an argument
(e.g., any Node interface methods such as Node.appendChild and
Node.insertBefore), in which case the children of the fragment are
appended or inserted, not the fragment itself.
The point is, when you do someNode.append(documentFragment), you remove all its children from it, then append them to someNode. That's why documentFragment is empty as result.
Note that when you do this...
frag_inner = '';
... you're not clearing the documentFragment stored in this variable - you store a new value (an empty string, obviously) in it instead. The very first attempt to work with it as with documentFragment should result in something like TypeError: Object has no method 'appendChild'.

Related

Why it's not possible to save the older HTMLElement object inside an object?

I've got the following simplified code to a problem I was facing yesterday when I've asked this question:
Problem of JavaScript previous object inside an object is being overwritten [EDITED]
The simplified code is:
HTML
<div class="header"><button>click me to add new to DOM and Object</button></div>
<div class="list"></div>
Javascript
const list = document.querySelector(".list");
const button = document.querySelector('button');
const obj = {
counter: 1
}
function html(counter) {
return `<div>I am Item number ${obj.counter}</div>`;
}
function addItem() {
//add new element to the DOM
list.innerHTML += html(obj.counter);
//add new object to the object
obj[`ITEM-${obj.counter++}`] = [...list.querySelectorAll("div")].slice(-1)[0];
console.log(obj);
}
button.addEventListener("click", addItem);
The problem is:
I'm console.logging the "obj" to see it's content,
I want you to look at the console after some clicks on the button, you'll see this output
{counter: 6, ITEM-1: div, ITEM-2: div, ITEM-3: div, ITEM-4: div, …}
ITEM-1: div
ITEM-2: div
ITEM-3: div
ITEM-4: div
ITEM-5: div
counter: 6
__proto__: Object
The question is:
Why only the last item from inside the object is indicating the HTML code from inside the dom while the other previous items are no longer indicating the HTML elements?
to try what I'm trying to say:
please, inside the console, hover on the last item from the object, in my case, it's ITEM-5: div,
you'll see how the item on the DOM is being highlighted.
but now try to hover on previous items from inside the object, for example in my case
ITEM-1: div
it's not being highlighted on the DOM.
what is the problem?
Never use += on .innerHTML.
It is precisely equivalent to:
element.innerHTML = element.innerHTML + content
In other words it requires that the existing content of the element be serialised into HTML, and then you add your content to that string, and then the entire thing is re-assigned to the node (which has to be de-serialised back onto DOM format again), also forcing the previously-existing child nodes to be recreated.
When you are using innerHTML to add an element you are basically erasing all the elements and creating new ones. So when you create a reference to the element, the element is removed when the innerHTML is replaced.
Typically your question is asked as "Why do my click event handlers only work on the last element when I add to the list?"
Looking at the code you are doing
var current = list.innerHTML; // string of existing HTML
var newString = html(obj.counter); // String of new HTML
var updated = current + newString; // Combine to make a new string
list.innerHTML = updated; // redraw the DOM with the new string
When you change the innerHTML directly, it does not figure out what changes in the string, it just clears the "whiteboard" and redraws it all. If you do not what to redraw the whole element, you need to get smarter and instruct it to what is being added.
You should be appending a new element, not building it as a string.
const list = document.querySelector(".list");
const button = document.querySelector('button');
const obj = {
counter: 1
}
function elem(counter) {
var div = document.createElement("div");
div.innerHTML = `I am Item number ${obj.counter}`;
return div
}
function addItem() {
var newDiv = elem(obj.counter)
list.appendChild(newDiv);
obj[`ITEM-${obj.counter++}`] = newDiv;
console.log(obj);
}
button.addEventListener("click", addItem);
<div class="header"><button>click me to add new to DOM and Object</button></div>
<div class="list"></div>

I cannot append elements with jQuery during javascript loop

There is one similar question, but I still not understand why I can't do the following:
var numLoaded = document.createElement('span')
numLoaded = document.createElement('span')
numLoaded.style= "color:red;font-weight:bold";
numLoaded.innerHTML = " Number of loaded results: "+0;
$("#resultsCounter").append(numLoaded)
for (var i = ((json.length)-1); i >= 0; i--) {
newDiv = document.createElement("div");
newDiv.id = +i+1;
newDiv.innerHTML = newDiv.innerHTML+"..."
numLoaded.innerHTML = " Number of loaded results: "+newDiv.id;
$("#resultsCounter").empty(); // should remove my span
$("#resultsCounter").append(numLoaded) // should add my span with new content
$("#results").prepend(newDiv)
}
After this all as output I see only last added element after whole cycle ends. During cycle iteration it appends to DOM no child, even if it is already created. I want to append always the same element <span> but in every iteration with updated number.
Why child element is added to DOM only after cycle ends? How to update it in cycle? Do I need to recreate that element after every loop iteration? It is way to speed up this process?
It's because you only create the element once, before the list. Then in the loop you are changing the innerHtml of the element, and adding it - but because it is the same element (the same JS object being referenced each time), it's effectively overwriting itself.
You just need to move these lines which create the element:
var numLoaded = document.createElement('span')
numLoaded = document.createElement('span')
numLoaded.style= "color:red;font-weight:bold";
numLoaded.innerHTML = " Number of loaded results: "+0;
inside the loop.

Use of Call method on document.querySelector in JavaScript

This is the sample code I am working on:
var DomClass = {
Eprcent: ".item__percentage"
};
var EPrsent = function (prcent) {
prcent.forEach(function (cur) {
var ele = document.getElementById("exp-" + cur.id).innerHTML;
var S = document.querySelector.call(ele, 'DomStrings.Eprcent'); //Uncaught TypeError: Illegal invocation
S.innerText = cur.prcnt + "%";
});
}
I am getting this exception: Uncaught TypeError: Illegal invocation at line number 7.
How can I use document method on my HTML from JavaScript code, ID, and class which I want to use that is already present in HTML?
This is a data structure of prcent class:
var Expense = function (id, description, value) {
this.id = id;
this.description = description;
this.value = value;
this.prcnt = 0;
};
I am aware of that we can use directly document query but these HTML are generating dynamically with unique ids. I have to put information according to element id in my HTML. ex document.getElementById("exp-" + cur.id) this line use for identifying the HTML element in which i want to put value
My main concern is that how can I use call method on QuerySelector so that I can change "this" pointer to innerHTML variable? If it's not possible, then why?
querySelector acts on a Document object.
Its signature is querySelector(String selector)
The Document method querySelector() returns the first Element node within the document, in document order, that matches the specified selectors, or group of selectors. If no matches are found, null is returned.
The problem related to document.querySelector.call is because you are providing an invalid this value.
Check you are setting document.getElementById("exp-" + cur.id).innerHTML (String) as the this argument for document.querySelector.call (lines: 5), 6))
The Element innerHTML property is used to get or set a string representing serialized HTML describing the element's descendants.
Therefore, using the call method on querySelector to change this pointer to innerHTML is not possible because that variable is a String and not a Document object.
In the following snippet you can check how we can use querySelector.call (nothing different to using call on any other function):
const serializedHtml = document.getElementById('example').innerHTML;
console.log(typeof serializedHtml, serializedHtml); // -> string "<div id="inner">inner div</div><span>...</span>"
const inner = document.querySelector('#inner');
// equivalent call: same result using `.call` with the document object
const usingCall = document.querySelector.call(document, '#inner');
console.log(inner === usingCall, typeof inner, inner); // -> true object <div id="inner">inner div</div>
// using an empty Document
console.log(document.querySelector.call(new Document(), '#inner')); // -> null
// error code when pointing to innerHTML string
document.querySelector.call(serializedHtml, '#inner'); // -> Error: Uncaught TypeError: Illegal invocation...
#example, #inner {
border: 1px solid;
padding: 8px;
text-align: center;
}
<div id="example">
<div id="inner">inner div</div>
<span>...</span>
</div>
Trying to bind the document.querySelector to another Document instance makes no sense to me because if you had another Document object, you can always invoke directly the querySelector function of the other document.
Check the following example:
const htmlStr = '<div id="container"><div id="inner"></div></div>'
const parser = new DOMParser();
const doc = parser.parseFromString(htmlStr, 'text/html');
// using window.document
console.log(document.querySelector('#inner')); // -> null
// pointing to the correct Document
console.log(document.querySelector.call(doc, '#inner')); // -> <div id="inner"></div>
// but, really not neccessary because you can use the correct Document directly
console.log(doc.querySelector('#inner')); // -> <div id="inner"></div>
Hope all of this helps!
Read more info about document.querySelector, Element.innerHTML, Function.prototype.call and DOMParser
You can use querySelector directly on the element if its a DOM element like
var DomClass ={
Eprcent: ".item__percentage"
};
var EPrsent = function (prcent) {
prcent.forEach(function (cur) {
var ele = document.getElementById("exp-" + cur.id);
var S = ele.querySelector(DomClass.Eprcent);
S.innerText = cur.prcnt + "%";
});
}

Object has no method 'appendChild'

When I send a div to a function which creates content to a document fragment it provides me this error when I append the fragment to the div.
Object column_left has no method 'appendChild' '
This is my code:
function progress_bar(parent_div){
var frag = document.createDocumentFragment();
var d = document.createElement('div');
d.className = 'progress_bg';
d.style.width = '90%';
frag.appendChild(d);
parent_div.appendChild(frag);
}
The function is called like this:
var d = document.getElementById('column_left');
progress_bar(d);
Does any one know why i cannot append like this?
Before you call progress_bar(d), #column_left doesn't exist (yet) so d is undefined. Since you're passing in an undefined value into progress_bar you can't call appendChild on it.
In order to fix this, you either have to create #column_left first, or pick a pre-existing element like document.body.
Here is a JSFiddle with the code cleaned up, I made it so it appends to the document's body instead and also sets the inner content something is actually rendered.
If you wanted to do it on #column_left, it would be something like this:
var el = document.createElement('div');
el.id = 'column_left';
document.body.appendChild(el);
progress_bar(el);

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.

Categories