setting innerHTML property breaks references to child elements - javascript

The issue is similar to the one described here, as far as I can tell. However, I am not using mootools and my question and results are different.
This is a test page to demonstrate the issue: http://jsfiddle.net/S7rtU/2/.
I add elements to a container using createElement and appendChild. As I add each, I also store a reference to it in a private array (elems).
Then, I decide to clear the container, and do so by setting container.innerHTML = ''.
(In my example, I set it first to a pending message, and then 3s later, using setTimeout, I clear it. This is to give time to observe the changes to the DOM.)
Then, I try to repopulate the container from the elems array, calling container.appendChild for each of the stored elements.
I have tested on the browsers I have at hand: Firefox 17.0.1, Chrome 23.0.1271.97, Safari 3.1.2, IE 6 and IE 7. All except IE 6 & 7 will actually restore those elements. So they are successfully stored in memory and references not destroyed. Furthermore, the event handlers registered to those elements still work.
In IE, the elements do not reappear.
What I have read about this issue, including the other SO question, seem to suggest that references are supposed to be broken when you modify innerHTML on the container. Event handlers are also supposed to be broken. However the 3 modern browsers I tested do not break the references, nor the event handlers.
Of course, to make this work in IE I can use something like this, but this is significant extra processing if there are lots of elements:
function explicitClearContainer() {
var e;
// Explicitly remove all elements
for (var i = 0; i < elems.length; ++i) {
e = elems[i];
// Update the reference to the removed Node
elems[i] = e.parentNode.removeChild(e);
}
}
My question is what is known about this behaviour, what can be expected in different environments, what are the pitfalls of using this sort of technique?
I would appreciate any comments.

The innerHTML property was only "standardised" in HTML5, which really just documents common browser behaviour for many features. Things such as innerHTML have been implemented differently in different browsers and will continue to be different for some time, so best to avoid using it if it's causing problems.
There are other approaches to clearing the child nodes of an element, e.g.:
function removeContent(element) {
while (element.firstChild) {
element.removeChild(element.firstChild);
}
}
Which should avoid your issues with innerHTML and is a bit less code than your version. You can also replace element with a shallow clone of itself, but that may cause other issues.

Related

Why use cloneNode when I can clone a node using querySelector?

Below two function does same thing - first they copy an element then clear the body and then add the element back. My question is which one is better and in what conditions cloneNode using function or the other one will not work ?
Using querySelector
function noahArc(animal) {
// Preserve the poor animal and its children
var arc = document.querySelector(animal);
// Flood the entire body with rage, water and thunder
document.body.innerHTML = "";
// Restore the preserved animal into the post-apocalyptic world
document.body.appendChild(arc);
}
noahArc('.asd');
Using cloneNode
function noahArc(animal) {
// Preserve the poor animal and its children
var arc = document.getElementsByClassName(animal)[0].cloneNode(true);
// Flood the entire body with rage, water and thunder
document.body.innerHTML = "";
// Restore the preserved animal into the post-apocalyptic world
document.body.appendChild(arc);
}
noahArc('asd');
First, for the avoidance of doubt: querySelector does not clone elements. It just gives you a reference to the element that already exists. In a non-buggy browser, that reference remains valid even if you wipe the conents of body after you get the reference. It just means the element isn't in the document anymore.
Second, your edit and various comments suggesting there's a difference in your code depending on whether you use querySelector or getElementsByClassName to select the element are incorrect. In the code shown, it makes no difference whatsoever. The only difference of consequence in your examples is whether you clone the node.
So, looking at the two examples:
Your first example will fail in some versions of IE because of a bug it has where it wipes the contents of the descendant element when you assign to the innerHTML of its ancestor even when you have a reference to the descendant element. It shouldn't fail, but it will. (This bug cost me hours of debugging time sometime last year...) I don't think Edge has this bug. I've just verified it's still an issue with IE11 using this test:
function noahArc(animal) {
var arc = document.querySelector(animal);
document.body.innerHTML = "";
document.body.appendChild(arc);
}
noahArc('.asd');
console.log("Done, you should still see 'this is the div' above, but you won't in IE");
<div class="asd">this is the div</div>
Other than that, neither is "better." It depends on what you want to do.
Your first example attempts to keep the same element in the document (which may have event handlers attached to it). It doesn't make a copy, and only works (on browsers where it works) because the original is removed from the document by assigning to its ancestor's innerHTML.
Your second example creates a copy of the element (which won't have event handlers on it).
Which you use depends on what you want to achieve, and what browsers you want to support.

purpose of data in jquery? (vs adding my own fields to Jquery objects)

This question is related to mine and explains what .data method is in Jquery.
Apart from the relation to HTML5 data-* element attributes (like <p class='foo_cl' data-bar='gee'>, for example) why would I code:
$('body').data("my_lab", {some:"object"}); // §1
instead of
$('body').my_lab = {some:"object"}; // §2
(I am mostly interested in the case where the Jquery selector gives one object, like for $('body') above)
The later (§2) seems more readable and shorter, and I guess would be more efficient, than the former (§1). Of course data is a Jquery selector (but I could use each to set the .my_lab field, etc...)
And I might even consider changing the DOM element with the ugly looking
$('body')[0].my_lab = {some:"object"}; // §3 is ugly
(which is perhaps undefined behavior, see this)
Of course, there is the potential issue of colliding the field name my_lab with some existing field in JQuery implementation; but I assume that using some common suffix (like my_) in field names should be enough.
FWIW, I am only interested in recent Jquery (e.g. Jquery 2.1.4) on recent Firefox (e.g. 38 or 42) on Linux.
In other words, why would adding my own fields in JQuery objects be frowned upon?
By doing
$('body').my_lab = {some:"object"};
You are setting value to a specified jQuery wrapper. You would not be able to reaccess the data with another selector :
$('body').my_lab = {some:"object"};
console.log($('body').my_lab); // will log undefined
This is why using data is basically more reliable
$('body').data('my_lab', {some:"object"});
console.log($('body').data("my_lab")); // will log {some: "object"}
For the 3rd option : $("body")[0].attr = { my : "object" } part :
The jQuery data method prevents potential memory leaks when manipulating the dom / removing elements from the page (by removing the binded data and stuff) and avoid attribute conflicts (like setting domElement existing attributes and other subtle things)
So basically, if you have jQuery in you app, you don't really have any good reason to reinvent the wheel by doing manual binding between dom element and javascript data.

Is there a possibility to raise an event when a certain tag is created?

I want to create the equivalent of an onCreate event for all tags which will be created that match a JQuery selector.
For instance, let's consider a document where the result of $("#foo > .bar > ul > li") is an empty set. I have a function called fooBar and I want this function to be called whenever a tag matching $("#foo > .bar > ul > li") was created.
I would like to define this event in my
$(function() {});
Does somebody know a possibility for this?
As far as I'm aware there aren't any events that are fired when elements are added to the DOM, so there's nothing you can bind a handler to in order to check for this.
What you can do is set up a polling routine that will periodically check the DOM for elements that match your selector, compare the current number of matches against the previous value, and perform whatever actions you wish if they differ.
var matchedElements = 0;
function poll() {
var $elements = $("#foo > .bar > ul > li");
if($elements.length > matchedElements) {
fooBar();
}
matchedElements = $elements.length;
}
setInterval(poll, 500); // runs poll() every half a second
This all assumes that you're not controlling the creation of these elements, or at least aren't controlling them in a way that allows you to reliably know they've been created.
If the only source of these elements is a single function you've written then you could simply extend that to trigger a handler for a custom event bound in jQuery.
Most practical solution
You can hook into the DOMNodeInserted event on document to detect the changes, and use .is to check if they match the selector of your choice.
$(function() {
var selector = "whatever";
$(document).on('DOMNodeInserted', function(e) {
if ($(e.srcElement || e.target).is(selector)) {
alert("Matching element inserted!");
}
});
});​
See it in action.
Compatibility and alternatives
This approach is convenient, but it does have two drawbacks:
The DOMNodeInserted event is deprecated.
It does not work on IE < 9, and cannot be made to work.
As regards the first, I wouldn't consider this an issue. It may be deprecated, but as long as there is no other alternative I really don't think any browser vendor would pull this functionality. Maybe in five years or so this will become a practical issue, but since the code is 10 lines or so total it will surely be easy to update it to work with the latest standard.
For IE compatibility, the sad truth is that you cannot do anything directly. However, you can resort to verbose, horrible hacks that do provide results by modifying the prototypes of DOM elements. See an example tailored to work on IE8.
Sadly, there are multiple issues with this approach:
You need to fish out all the methods that can result in the DOM being modified (or at least, all of those you will be using) and weave them into the solution. In the future you will be obliged to check if new DOM mutation methods are added and keep up with supporting them.
You need to be extra careful to provide correct replacements for the methods, for all browsers that you choose to target with this (if more than one).
Extending the DOM (in general) can be problematic. If you thought this specific method of extension is bad, consider that IE7 does not support it and on that browser you 'd have to replace methods on all elements in the DOM to make sure you hook into every possible modification.
Specifically, you cannot target all current browsers with just this code (e.g. Chrome defines these methods on Node.prototype, not on Element.prototype). Targeting future browsers should not be mentioned even if joking.
Finally, you can always decide to use polling to detect changes, as Anthony explains in his answer.
Related resources
DOMNodeInserted equivalent in IE?
Modify prototypes of every possible DOM element

JavaScript DOM remove element

I'm trying to test if a DOM element exists, and if it does exist delete it, and if it doesn't exist create it.
var duskdawnkey = localStorage["duskdawnkey"];
var iframe = document.createElement("iframe");
var whereto = document.getElementById("debug");
var frameid = document.getElementById("injected_frame");
iframe.setAttribute("id", "injected_frame");
iframe.setAttribute("src", 'http://google.com');
iframe.setAttribute("width", "100%");
iframe.setAttribute("height", "400");
if (frameid) // check and see if iframe is already on page
{ //yes? Remove iframe
iframe.removeChild(frameid.childNodes[0]);
} else // no? Inject iframe
{
whereto.appendChild(iframe);
// add the newly created element and it's content into the DOM
my_div = document.getElementById("debug");
document.body.insertBefore(iframe, my_div);
}
Checking if it exists works, creating the element works, but deleting the element doesn't. Basically all this code does is inject an iframe into a webpage by clicking a button. What I would like to happen is if the iframe is already there to delete it. But for some reason I am failing.
removeChild should be invoked on the parent, i.e.:
parent.removeChild(child);
In your example, you should be doing something like:
if (frameid) {
frameid.parentNode.removeChild(frameid);
}
In most browsers, there's a slightly more succinct way of removing an element from the DOM than calling .removeChild(element) on its parent, which is to just call element.remove(). In due course, this will probably become the standard and idiomatic way of removing an element from the DOM.
The .remove() method was added to the DOM Living Standard in 2011 (commit), and has since been implemented by Chrome, Firefox, Safari, Opera, and Edge. It was not supported in any version of Internet Explorer.
If you want to support older browsers, you'll need to shim it. This turns out to be a little irritating, both because nobody seems to have made a all-purpose DOM shim that contains these methods, and because we're not just adding the method to a single prototype; it's a method of ChildNode, which is just an interface defined by the spec and isn't accessible to JavaScript, so we can't add anything to its prototype. So we need to find all the prototypes that inherit from ChildNode and are actually defined in the browser, and add .remove to them.
Here's the shim I came up with, which I've confirmed works in IE 8.
(function () {
var typesToPatch = ['DocumentType', 'Element', 'CharacterData'],
remove = function () {
// The check here seems pointless, since we're not adding this
// method to the prototypes of any any elements that CAN be the
// root of the DOM. However, it's required by spec (see point 1 of
// https://dom.spec.whatwg.org/#dom-childnode-remove) and would
// theoretically make a difference if somebody .apply()ed this
// method to the DOM's root node, so let's roll with it.
if (this.parentNode != null) {
this.parentNode.removeChild(this);
}
};
for (var i=0; i<typesToPatch.length; i++) {
var type = typesToPatch[i];
if (window[type] && !window[type].prototype.remove) {
window[type].prototype.remove = remove;
}
}
})();
This won't work in IE 7 or lower, since extending DOM prototypes isn't possible before IE 8. I figure, though, that on the verge of 2015 most people needn't care about such things.
Once you've included them shim, you'll be able to remove a DOM element element from the DOM by simply calling
element.remove();
Seems I don't have enough rep to post a comment, so another answer will have to do.
When you unlink a node using removeChild() or by setting the innerHTML property on the parent, you also need to make sure that there is nothing else referencing it otherwise it won't actually be destroyed and will lead to a memory leak. There are lots of ways in which you could have taken a reference to the node before calling removeChild() and you have to make sure those references that have not gone out of scope are explicitly removed.
Doug Crockford writes here that event handlers are known a cause of circular references in IE and suggests removing them explicitly as follows before calling removeChild()
function purge(d) {
var a = d.attributes, i, l, n;
if (a) {
for (i = a.length - 1; i >= 0; i -= 1) {
n = a[i].name;
if (typeof d[n] === 'function') {
d[n] = null;
}
}
}
a = d.childNodes;
if (a) {
l = a.length;
for (i = 0; i < l; i += 1) {
purge(d.childNodes[i]);
}
}
}
And even if you take a lot of precautions you can still get memory leaks in IE as described by Jens-Ingo Farley here.
And finally, don't fall into the trap of thinking that Javascript delete is the answer. It seems to be suggested by many, but won't do the job. Here is a great reference on understanding delete by Kangax.
Using Node.removeChild() does the job for you, simply use something like this:
var leftSection = document.getElementById('left-section');
leftSection.parentNode.removeChild(leftSection);
In DOM 4, the remove method applied, but there is a poor browser support according to W3C:
The method node.remove() is implemented in the DOM 4 specification.
But because of poor browser support, you should not use it.
But you can use remove method if you using jQuery...
$('#left-section').remove(); //using remove method in jQuery
Also in new frameworks like you can use conditions to remove an element, for example *ngIf in Angular and in React, rendering different views, depends on the conditions...

`clone()` not working in Internet Explorer 6

I was trying to clone an element and append it to another child with following statement of jQuery:
$(userListJId).clone().appendTo(tempOwnJString);
Where userListJId and tempOwnJString are the id's of elements.
The above line of code works fine in Internet Explorer 7 and higher versions of it but does not seem to be working in Internet Explorer 6.
What could be the possible reason?
I used clone() on IE6 and so that should not be the problem.
Maybe you are creatong invalid HTML and IE6 which is less permissive than IE7 complains about this.
Can you show us your code and also the version of jQuery?
It's funny you should ask this because I had a quite similar problem (though it affected IE7 and probably IE6).
Also, not sure if you have done something special (i.e. defining variables) but perhaps you should refer to the objects as $('#userListJId') instead of just the element name. Again, I can't see the rest of the code, so you may have already defined those variables outside the document.* scope.
Basically, in IE, certain attributes cannot be modified after the object is created, an example being the ID attribute.
The work around is to not clone the object, at least through .clone(), but to take the outer HTML of the object you wish to clone as a string and do a regex .replace() on the id attribute, and then append the modified HTML into tempOwnJString.
Another gotchya in IE, is that sometimes (usually?) when it parses the HTML, it doesn't wrap quotes around attribute values if they contain only alphanumeric characters, so be mindful of this in your regex pattern.
Here is an example of some code I used.
if ($.browser.msie === true)
{
//unfortunately jQuery doesn't have an outerHTML function, so this is a hacky work around
templateHTML = $("#activityTemplate").clone().wrap('<div>').parent().html();
newHTML = templateHTML.replace(/id\=\w+/ig, 'id='+jsonObj.ContactLogID);
$(newHTML).prependTo($("#activityContainer"));
// in case i need to refer to newly created object
clone = $("#"+jsonObj.ContactLogID);
}
Again I can't say for certain if this is the issue you're having but with the information you gave and without any debug info (which IE6 doesn't really provide anyway) this is the best guess.
Echoing what #Foxtrot said, you need to ensure you set the id on the cloned element or you'll freak IE6 out. Afterall, all browsers follow the standard that ids must be unique. Their behavior when you violate this varies. You are experiencing variation.
As a trivial example:
var clone = $(userListIJD).clone();
clone[0].id = 'somethingElse'; // use a formula here, as presumably this is run over and over
// proceed with appending the clone

Categories