infinite-loop via prepend element in DOM - javascript

Not Looking for a Use Framework XXX Answer
This question is not intended for finding a practical solution via a framework. Answering with use framework XXX, or this is so easy in framework XXX, or why not use this framework XXX??? doesn't answer the question.
I have a function meant to run after a page has been loaded: performShim. This function iterates over all elements in the DOM that are span tags, checks if they have a className of shim and if so, calls shim passing to it a reference of the matched element.
My goal was to prepend another span that contains an iframe to the element that is passed to shim.
With the code I wrote so far, I am able to append to the element's parent just fine. However, if I comment out the append line and instead try the prepend line the browser hangs in presumably an infinite-loop.
It's not readily obvious to me why this is the case.
function shim( element ) {
var iframe = document.createElement('iframe');
iframe.setAttribute( 'frameborder', '0' );
iframe.setAttribute( 'scrolling', 'no' );
iframe.setAttribute( 'align', 'bottom' );
iframe.setAttribute( 'marginheight', '0' );
iframe.setAttribute( 'marginwidth', '0' );
iframe.setAttribute( 'src', "javascript:'';" );
var span = document.createElement('span');
span.appendChild(iframe);
//element.parentNode.insertBefore(span,element); //causes infinite loop?
element.parentNode.appendChild(span); //this line works OK
var els = element.style;
var originalVisibility = els.visibility;
var originalPosition = els.position;
var originalDisplay = els.display;
els.visibility = 'hidden';
els.position = 'absolute';
els.display = 'inline';
var width = element.offsetWidth;
var height = element.offsetHeight;
els.display = originalDisplay;
els.position = originalPosition;
els.visibility = originalVisibility;
iframe.style.width = (width-6) + 'px';
iframe.style.height = (height-6) + 'px';
}
function performShim() {
var children = document.getElementsByTagName("span");
for( var i = 0; i < children.length; i++ ) {
if( children[i].className == "shim" ) {
shim(children[i]);
}
}
}

A NodeList (such as the one returned by document.getElementsByTagName) is typically a live list -- changes you make to the DOM show up in it as well. So each time you add a span before the current one, you're extending the list by one element and moving the current element over by one, and the next iteration puts you right back at the node you just finished.
You have a couple of easy workarounds for that...
Bump the counter when you add a node. (Ugly, and if you ever end up adding something instead of a span, you'll end up skipping nodes and it won't be obvious why.)
Copy the list to an array and iterate over the array. You could do this with something like
children = [].slice.call(children, 0); (more common) or
children = Array.apply(window, children);.
Use document.querySelectorAll, which returns you a NodeList that's not live. (And even if it were live, in this case you could select 'span.shim' and the inserted spans wouldn't show up in it anyway.)
Iterate backwards (from children.length - 1 to 0).

Related

Alter unappened element's html tag type with javascript

So I have an element created by another process, created in a method akin to
var their_element = document.createElement("div");
/* Lots of stuff is done to their_element */
And that object is being passed to me later. It has not been appended anywhere yet. The problem is, I need it to be different html tag type when it finally hits the html, such as:
<form></form>
How do i change it? The solutions I have found involve editing after it's appended, not before.
Edit:
Also, learned nodeName can't be assigned for this.
their_element.nodeName = "FORM"
doesn't work.
Also, this doesn't work either:
their_element.tagName = "FORM"
Also this didn't work either:
var outside = their_element.outerHTML;
outside = outside.replace(/div/g, 'form');
their_element.outerHTML = outside;
All of these still leave it as a DIV when appended.
(And I'm not looking for jQuery)
Check on this for cross-browser compatability, but there are properties and methods on elements that could be of use. Particularly, Element.attributes, Element.hasAttributes(), and Element.setAttribute(). See: https://developer.mozilla.org/en-US/docs/Web/API/Element/attributes
I'm not going to use ES6 here, so we don't have to worry about transpiling. You can update if you want:
var el = document.createElement('div');
el.id="random";
el.style.background="red";
el.style.width="200px";
el.style.padding="10px";
el.style.margin="10px";
el.innerHTML="<input type='submit' value='submit'>";
console.log({"Element 1": el});
var newEl = document.createElement('form');
console.log({"Element 2 Before Transformation": newEl})
if (el.hasAttributes()) {
var attr = el.attributes
for (var i = 0; i < attr.length; i++) {
var name = attr[i].name, val = attr[i].value;
newEl.setAttribute(name, val)
}
}
if (el.innerHTML) { newEl.innerHTML = el.innerHTML }
console.log({"Element 2 After Transformation": newEl})
document.body.append(el);
document.body.append(newEl);
There are certain properties you need to account for like innerHTML, innerText, and textContent that would overwrite one another if multiples are set. You may also have to account for childNodes and what not.

Why Document.querySelector is more efficient than Element.querySelector

I did a test with few iterations to test efficiency of Document.querySelector and Element.querySelector.
Markup:
<form>
<input type="text" />
</form>
Script:
Querying with Document.querySelector
begin = performance.now();
var
i = 0,
iterations = 999999;
for ( i; i < iterations; i++ )
{
element = document.querySelector('[type="text"]');
}
end = performance.now();
firstResult = end - begin;
Querying with Element.querySelector
begin = performance.now();
var
i = 0,
iterations = 999999,
form = document.querySelector('form');
for ( i; i < iterations; i++ )
{
element = form.querySelector('[type="text"]');
}
end = performance.now();
secondResult = end - begin;
Log:
console.log( firstResult ); // 703.7450000001118
console.log( secondResult ); // 1088.3349999999627
The log is amazing for me because i think that Element.querySelector query only on nodes that is a descendant of the element and Document.querySelector query on all nodes of current document, right?
Why get this result?
From my comment above, the selector takes into account the entire document, then filters the items to check if they are descendants of the target. So it's likely that it still needs to scan the entire DOM tree like document.querySelector would need to do.
There is a discussion of the issue (that is still the current behaviour) here. You'll see in the code sample below the span is included as a result, because it can't just query items below foo in isolation.
Fiddle
Code:
document.body.innerHTML = '<div><p id="foo"><span></span></p></div>';
var foo = document.getElementById('foo');
alert( foo.querySelectorAll('div span').length);
It seems obvious to me that the result should be null, as the context is outside the element the search is began from.
Things are simple when you make them simple.... Don't you are fixing the "context", how can you think the "context" is "outside" the "context" you stated... In the example from Evan is really curious to get any result, as there are no "div" in the foo node where the selector is executed... but the answer is "I've found a span descendant of a div... but you'll not find the div there"

Counting parent nodes

Is there a native method of DOM element in ECMAScript that will allow to count all ancestors of a given element (up to window object or DOM element specified by Id,Name etc.)?
Example use is to check all ancestors of a given element and remove a specified attribute.
There is the node iterator ( https://developer.mozilla.org/en-US/docs/Web/API/NodeIterator ) which could be used for this purpose
You can use xpath:
document.evaluate('ancestor::*', x, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null)
See https://developer.mozilla.org/en/docs/Introduction_to_using_XPath_in_JavaScript for more details.
I've wrote a simple function that does an action described in a question. It gets all ancestors of a given element and removes a given attribute from every element that is between "starting" and "ending" element (it does not perform an removeAttribute method on "ending" element).
var modifyAncestors = function(startingelementid,endingelementId,searchedattribute) {
var nodecount = document.getElementById(endingelementId).childNodes.length;
console.log(nodecount);
var currentid = startingelementid;
console.log(currentid);
console.log(searchedattribute);
for(var i = 0; i < nodecount; i++) {
if(currentid == endingelementId) {
break;
}
else {
document.getElementById(currentid).removeAttribute(searchedattribute);
currentid = document.getElementById(currentid).parentNode.id;
}
}
}
Working example: http://www.blacktieseo.com/so/js/test.html (couldn't get it to work with Fiddle JS).
Any comments, bugs etc. will be highly appreciated.

Is there any way to find an element in a documentFragment?

var oFra = document.createDocumentFragment();
// oFra.[add elements];
document.createElement("div").id="myId";
oFra.getElementById("myId"); //not in FF
How can I get "myId" before attaching fragment to document?
All of these answers are rather old, from back when querySelectorAll and querySelector were not widely available. It should be noted that these two functions which accept CSS selectors as parameters do work on DocumentFragments in modern browsers, and should be the preferred way of dealing with the situation in the question. The alternate solutions presented in some of the answers would be a good approach for legacy browsers which did not support querySelectorAll or querySelector.
Here is an example usage:
var df = document.createDocumentFragment();
var div = document.createElement('div');
div.id = 'foo';
df.appendChild(div);
var result = df.querySelector('#foo'); // result contains the div element
A good implementation should first use object detection to see if the browser supports this. For instance:
function getElementByIdInFragment(fragment, id) {
if (fragment.querySelector) {
return fragment.querySelector('#' + id);
} else {
// your custom implementation here
}
}
No. The DocumentFragment API is minimal to say the least: it defines no properties or methods, meaning that it only supports the properties and methods defined in the Node API. As methods such as getElementById are defined in the Document API, they cannot be used with a DocumentFragment.
NickFitz is right, DocumentFragment doesn't have the API you expect from Document or Element, in the standard or in browsers (which is a shame; it would be really handy to be able to set a fragment's innerHTML.
Even frameworks don't help you here, as they tend to require Nodes be in the document, or otherwise use methods on the context node that don't exist on fragments. You'd probably have to write your own, eg.:
function Node_getElementById(node, id) {
for (var i= 0; i<node.childNodes.length; i++) {
var child= node.childNodes[i];
if (child.nodeType!==1) // ELEMENT_NODE
continue;
if (child.id===id)
return child;
child= Node_getElementById(child, id);
if (child!==null)
return child;
}
return null;
}
It would almost certainly be better to keep track of references as you go along than to rely on a naïve, poorly-performing function like the above.
var frag= document.createDocumentFragment();
var mydiv= document.createElement("div");
mydiv.id= 'myId';
frag.appendChild(mydiv);
// keep reference to mydiv
What about:
var oFra = document.createDocumentFragment();
var myDiv = document.createElement("div");
myDiv.id="myId";
oFra.appendChild(myDiv);
oFra.getElementById("myId"); //not in FF
Unless you've added the the created div to your document fragment I'm not sure why getElementById would find it?
--edit
If you're willing to roll your own getElementById function then you ought to be able to get the reference you're after, because this code works:
var oFra = document.createDocumentFragment();
var myDiv = document.createElement("div");
myDiv.id = "myId";
oFra.appendChild(myDiv);
if (oFra.hasChildNodes()) {
var i=0;
var myEl;
var children = oFra.childNodes;
for (var i = 0; i < children.length; i++) {
if (children[i].id == "myId") {
myEl = children[i];
}
}
}
window.alert(myEl.id);
Using jQuery:
// Create DocumentFragment
var fragment = document.createDocumentFragment(),
container = document.createElement('div');
container.textContent = 'A div full of text!';
container.setAttribute('id', 'my-div-1');
container.setAttribute('class', 'a-div-class');
fragment.appendChild(container);
// Query container's class when given ID
var div = $('<div></div>').html(fragment);
console.log(div.find('#my-div-1').attr('class'));
jsFiddle: http://jsfiddle.net/CCkFs/
You have the overhead of creating the div with jQuery, though. A little hacky, but it works.
The best way by far to find out what you can and can't do with a DocumentFragment is to examine its prototype:
const newFrag = document.createDocumentFragment();
const protNewFrag = Object.getPrototypeOf( newFrag );
console.log( '£ protNewFrag:' );
console.log( protNewFrag );
I get
DocumentFragmentPrototype { getElementById: getElementById(),
querySelector: querySelector(), querySelectorAll: querySelectorAll(),
prepend: prepend(), append: append(), children: Getter,
firstElementChild: Getter, lastElementChild: Getter,
childElementCount: Getter, 1 more… }
... which tells me I can do things like:
const firstChild = newFrag.children[ 0 ];
PS this won't work:
const firstChild = Object.getPrototypeOf( newFrag ).children[ 0 ];
... you'll be told that "the object doesn't implement the DocumentFragment interface"
An external source, listed below, showed the following code snippet:
var textblock=document.createElement("p")
textblock.setAttribute("id", "george")
textblock.setAttribute("align", "center")
Which displays a different way of setting the object's ID parameter.
Javascript Kit - Document Object Methods
My DOM has a #document-fragment under the element tag.
This is what I am using (using jQuery) , Also I have a use case where I have the HTML DOM in a string -
var texttemplate = $(filecontents).find('template').html();
$(texttemplate).children()
<p>​Super produced One​</p>​,
<appler-one>​</appler-one>,
<p>​Super produced Two​</p>,
<appler-two>​…​</appler-two>]
$(texttemplate).html()
"<p>Super produced One</p>
<appler-one></appler-one>
<p>Super produced Two</p>
<appler-two>
<p>Super produced Three</p>
<appler-three></appler-three>
</appler-two>"
$(texttemplate).find("appler-one")
[<appler-one>​</appler-one>​]

Why this javascript is not working in IE?

I am using the following javascript to dynamically add rows in a table:-
var trObj = document.createElement('tr');
trObj.setAttribute('name', 'dynamicTR');
var tdObjEmpty = document.createElement('td');
tdObjEmpty.setAttribute('colspan', '2');
tdObjEmpty.innerHTML = ' '
trObj.appendChild ( tdObjEmpty );
var tdObj = document.createElement('td');
tdObj.setAttribute('colspan', '15');
tdObj.innerHTML = postingDivObj.innerHTML; // <-- copy the innerHTML
trObj.appendChild ( tdObj );
parentObj = approvedDisapprovedTableObj.getElementsByTagName('tbody')[0];
targetElementObj = getNthTr ( parentObj, rowIndex1 - extraTr ); // <-- it will just return the trObject,
if ( targetElementObj ){
parentObj.insertBefore(trObj, targetElementObj.nextSibling )
}else{
//alert ( 'targetElementObj is null' );
}
This is working in FF as well as in IE, [ but, i guess, in case of IE name and colspan attribute is not set using setAttribute. but not sure ] .
Now, when i have to remove all rows which are dynamically created i use:-
dynamicTRObjs = document.getElementsByName('dynamicTR');
if ( dynamicTRObjs ){
parentObj = approvedDisapprovedTableObj.getElementsByTagName('tbody')[0];
for ( i = 0 ; i < dynamicTRObjs.length; i++ ){
parentObj.removeChild ( dynamicTRObjs[i] );
extraTr++;
}
}
This code removes all dynamically created TRs. and it works fine in FF, but not in IE.
Also in case of IE dynamicTRObjs.length is always 0,whereas in FF dynamicTRObjs.length it gives correct number of rows. Please tell me what i am missing here.
The HTML4 spec list of attributes lists elements that the name attribute can be set on. Tables and table elements are not on the list. The most obvious option is one of,
Keep references to all TRs you create so you don't have to find them in the DOM
Set a className on your TRs and use selectors to find them
That Firefox uses getElementsByName 'correctly' and IE does not is something others have run into too. I'd just avoid using name here altogether.
http://www.quirksmode.org/dom/w3c_core.html
getElementsByName() is not working well in IE6-8
I would suggest that you some other way of identifying that element if you want cross browser usability.
I know, it's a bit off-topic, but let me give you a small advice on using getElementsByName functionality in a browser. It will not help you to solve the current problem (which is because TR can not have Name attribute ), but it will definitely help you to prevent future problems which you will met.
getElementsByName returns you collection, which always keeps itself up-to-date with the DOM tree. This means, that at the moment when you remove ONE item with removeChild, the SIZE of collection will be decreased. So, if you will removing nodes and keep relying on the length of the collection, not all nodes will be removed.
Check this example of your for loop:
Collection length is 3, Your i var is 0, i < length
You remove child,
collection length is 2, your i var is 1, i < length
you remove child,
collection length is 1 and your i var i 2.
Condition i< length == true that means that for loop will stop, BUT some of the elements will still be presented in the DOM.
Choose any solution you like to fix this, but try to avoid relying on the length of the Collection which is returned by getElementsByTagName.
Good luck
since I'm not the only one to suggest avoidance of low-level DOM manipulation, here's an example: an untested implementation with jquery. not exactly an answer to your question, but a comment would lose the formatting.
var mkTd = function (colspan, html)
{
return $('<td />')
.attr('colspan', colspan)
.html(html)
;
}
var addRow = function (rowNr)
{
var target = $('#approvedDisapprovedTableObj tbody tr:eq('+rowNr+')');
if (!target.length) {
//alert ( 'target is null' );
return;
}
target.after(
$('<tr />')
.addClass('dynamicTR')
.append(mkTd(2, ' ')
.append(mkTd(15, $('#postingDivObj').html()))
);
}
var dropRows = function ()
{
$('.dynamicTR').remove();
}
notice that the expression $('.dynamicTR').remove() achieves the same as your
dynamicTRObjs = document.getElementsByName('dynamicTR');
if ( dynamicTRObjs ){
parentObj = approvedDisapprovedTableObj.getElementsByTagName('tbody')[0];
for ( i = 0 ; i < dynamicTRObjs.length; i++ ){
parentObj.removeChild ( dynamicTRObjs[i] );
extraTr++;
}
}
IMO it's obvious that the benefits are huge.

Categories

Resources