2011

Wesley Hales' Blog

August 2011 Previous month Next month

The most common approach for receiving markup from an Ajax request is to use innerHTML for placement of the responseText. This method has been widely used (and argued) since the inception of XHR, but it surprises me that it's still being recommended and used not only on desktop browsers but mobile ones as well.

3 or 4 years have passed since many folks raised their concerns with innerHTML:

 

 

From Javascript The Good Parts:

 

“If the HTML text contains a <script> tag or its equivalent, then an evil script will run. .. This danger is a direct consequence of JavaScript’s global object which is far and away the worst part of JavaScript’s many bad parts.”

 

 

Not only is innerHTML bad, it is the root cause of many problems... from browser memory leaks (it destroys/replaces existing elements that may have event handlers attached) to failing completely on iOS’s Mobile Safari. Yes, that's right, it just flakes out.

 

So even if you use Crockford’s purge method to fix the memory leaks and sanitize your response string returned from the server, you still have a showstopping flaw when running any mobile web solution that uses innerHTML on iOS devices

 

Just to name a few mobile frameworks that use this flawed innerHTML approach:

JQuery Mobile (uses jQuery’s .html() wich is a wrapper for innerHTML)

Phone Gap

Sencha

 

A possible solution:

Disclaimer: I have not done any benchmarking so performance is unknown.

 

We all know that innerHTML is a favorite for it’s speed and ease of use but speed doesn’t really matter when it doesn’t work at all. So one solution is through use of some new features in HTML5 and the DOM api:

 

Let's start with the scenario that you've made your XHR and received the responseText.

First thing we'll do is create a temporary iFrame element. This isn't any ordinary iframe, it received a major security enhancement with HTML5 and we have some new sanitizing features with the "sandbox" attribute.

 

From the spec:

The sandbox attribute, when specified, enables a set of extra restrictions on any content hosted by the iframe. Its value must be an unordered set of unique space-separated tokens that are ASCII case-insensitive. The allowed values are allow-forms, allow-same-origin, allow-scripts, and allow-top-navigation. When the attribute is set, the content is treated as being from a unique origin, forms and scripts are disabled, links are prevented from targeting other browsing contexts, and plugins are disabled.

To limit the damage that can be caused by hostile HTML content, it should be served using the text/html-sandboxed MIME type.

 

 

function getFrame() {
    var frame = document.getElementById("temp-frame");
    if (!frame) {
        // create frame
        frame = document.createElement("iframe");
        frame.setAttribute("id", "temp-frame");
        frame.setAttribute("name", "temp-frame");
        frame.setAttribute("seamless", "");
        frame.setAttribute("sandbox", "");
        frame.style.display = 'none';
        document.documentElement.appendChild(frame);
    }
    return frame.contentDocument;
}

 

Now, we get our ajax response and write it to the iframe:

 

var frame = getFrame();
frame.write(responseText);

 

The beauty of this solution is the fact that we don't have to deal with a javascript text to DOM parser. We're allowing the browser to do what it does best... parse the HTML and build a DOM. And we don't have to worry about parsing the response and removing a blacklist of prohibited security risk elements and other XSS hacking pitas.

 

After writing the response to the iframe, you now have a ready to use sanitized DOM. Next you can use the DOM API to grab any part of the new document.

 

 var incomingElements = frame.getElementsByClassName('elementClassName');

 

Safari correctly refuses to implicitly move a node from one document to another. An error is raised if the new child node was created in a different document. So here we use adoptNode to add the incomingElements to our existing page.

 

 document.getElementById(elementId).appendChild(document.adoptNode(incomingElements));

 

The only thing left to do now is benchmarking. As I said earlier, working with the DOM has been notably slower than using innerHTML in the past. So there may be a derivative of this proposed solution that is faster? or there may not be a huge difference in execution time? Let me know....

 

Update: @_boye has created a perf test which shows the performance of this solution. Remarkably, This iFrame solution outperforms innerHTML on Firefox 7 and maintains the same speed on Chrome 16.