views:

327

answers:

3

Background

I am working on a project that runs in an embedded web browser in a small device with limited resources. The browser itself is a bit dated and has limits to its capabilities (HTML 4.01†, W3C DOM Level 2†, JavaScript 1.4). I have no documentation on the browser, so what I do know comes from trial and error.

The point is to retrieve dynamic content from a server so that only a minimal amount of inflexible code needs to be embedded into the device running the web browser. The browser does not support the XMLHTTPRequest object, so AJAX is out. Working with I do have, I wrote a bit of test code to dynamically insert JavaScript.

† Minor portions of these standards not supported

EDIT While I cannot actually confirm it, I believe that this site may list the DOM support for the embedded browser because I see "Mozilla/4.0 (compatible; EBSWebC 2.6; Windows NT 5.1)" as the user agent in the server log.

<html> 
<head>  
</head> 
<body onload="init()"> 
<div id="root"></div>
<script type="text/javascript"> 
<!--
function init() {
 // Add a div element to the page.
 var div = document.createElement("div");
 div.id = "testDiv";
 document.getElementById("root").appendChild(div);

 // Set a timeout to insert the JavaScript after 2 seconds.
 setTimeout("dynamicJS()", 2000);
}

function dynamicJS() {
 ...
}
//-->
</script> 
</body> 
</html>

Method 1

I initially implemented the dynamicJS function using Method 1 and found that while the code executes as expected in Chrome, IE8, and FireFox 3.5, the JavaScript is not actually retrieved by the embedded browser when the element is appended.

function dynamicJS() {
 var js = document.createElement("script");
 js.type = "text/javascript";
 js.src = "js/test.js";
 document.getElementById("root").appendChild(js);
}

Method 2

Looking for a work around, I implemented Method 2. This method actually works in the embedded browser as the JavaScript is retrieved and executed, but it does not work in other modern web browsers's I tested against (Chrome, IE8, FireFox 3.5).

function dynamicJS() {
 var js= '<script type="text/javascript" src="js/test.js"> </s' + 'cript>';
 document.getElementById("testDiv").innerHTML = js;
}

Question

I'm new to JavaScript and web programming in general, so I'm hoping one (or more) of the experts here can shed some light on this for me.

Is there anything technically wrong with Method 2 and if not, why doesn't it work in modern web browsers?

+3  A: 

There is nothing technically wrong with method 2 but most modern browsers have very loose HTML parsers that tend to get caught up in the code that you're sending. Specifically they parse the </script> in your JavaScript string literal as an end tag. This manifests itself in two ways:

  1. You'll see an "Unterminated String Literal" error.
  2. All code after the </script> text will be rendered as text on the page.

A common workaround for this problem is to split the </script>. You can do this with the following code. Yes, I know its a hack, but it works around the problem.

function dynamicJS() {
   var js= '<script type="text/javascript" src="js/test.js"></s' + 'cript>';
   document.getElementById("testDiv").innerHTML = js;
}

Realistically though, you should be able to use your first approach strictly using the DOM APIs. I've found that some browsers can be really picky about loading scripts added by script in that they will only load them if they are placed as a child of the <head> element. This is how the YUILoader works, so I'd be surprised if it didn't work in all browsers.

Here's an example, you'll want to check this to make sure that it works in all browsers, and add some error checking around the assumption that there will be a <head> element but it give you the general idea.

if (!document.getElementsByTagName) {
  document.getElementsByTagName = function(name) {
    var nodes = [];
    var queue = [document.documentElement];
    while (queue.length > 0) {
      var node = queue.shift();
      if (node.tagName && node.tagName.toLowerCase() === name) {
        nodes.push(node);
      }
      if (node.childNodes && node.childNodes.length > 0) {
        for (var i=0; i<node.childNodes.length; i++) {
          if (node.childNodes[i].nodeType === 1 /* element */) {
            queue.push(node.childNodes[i]);
          }
        }
      }
    }
    return nodes;
  };
}

function dynamicJS() {
   var js = document.createElement("script");
   js.setAttribute('type', 'text/javascript');
   js.setAttribute('src', 'js/test.js');
   var head = document.getElementsByTagName('head')[0];
   head.appendChild(js);
}
Bryan Kyle
I am unable to get your workaround to function in IE8, Chrome, or FireFox 3.5. IE8 doesn't show that the element was added. Chrome shows that the element is added, but the JavaScript is not retrieved (server logs confirm this). Using Firebug with FireFox 3.5, I see the element is added and the error message says it failed to load the source (server logs confirm this).This method of dynamically inserting JavaScript only seems to work in the embedded browser.
jschmier
I've added an implementation of getElementsByTagName() if the browser doesn't natively have it. I've tested it and it works in IE, FF and Chrome so long as you don't call it until after the document is entirely loaded -- just call it in a timeout or in a script tag towards the end of your document.
Bryan Kyle
I updated Method 2 to show what I have tried. It still does not work in modern browsers and I'm still curious as to why.
jschmier
+1  A: 

The innerHTML property has not yet actually been standardized, though all modern browsers support it, and the draft standard of HTML5 includes a definition of how it should work. According to the HTML5 specification:

When inserted using the document.write() method, script elements execute (typically synchronously), but when inserted using innerHTML and outerHTML attributes, they do not execute at all.

innerHTML was first introduced in Microsoft Internet Explorer 4, and due to its popularity among authors, has been adopted by all of the other browsers, which is what led to its inclusion in HTML5. So, let's check Microsoft's documentation:

When using innerHTML to insert script, you must include the DEFER attribute in the script element.

So apparently, in IE you can get scripts inserted via innerHTML to execute, but only if you add a defer attribute (I do not have IE in front of me to test this). defer is another feature that was first added to IE; it was included in HTML 4.01, but not picked up by any of the other browsers for quite a while. HTML5 includes a much more detailed description of how <script defer> should work, though it appears to be slightly incompatible with how it works in IE, as it does not allow execution of scripts added via innerHTML. The HTML5 definition of <script defer> appears to be implemented in Firefox 3.5 and Safari 4.

In summary, innerHTML hasn't really been standardized yet, but instead simply implemented by all of the browser vendors in slightly different ways. In IE, the original implementation, it didn't support execution of scripts except with a defer attribute, and defer hasn't been supported in other browsers until just recently, and so the other browsers simply don't support execution of scripts added using innerHTML. This behavior is what HTML5 is standardizing on, so unless Microsoft objects, is probably going to be what goes into the standard.

It sounds like the browser you are working with didn't do as good a job of implementing a compatible innerHTML, as it executes scripts added using innerHTML no matter what. This is unsurprising, as the behavior isn't standardized and so needs to be either reverse engineered or gleaned from reading the documentation of other browsers (which may not have included this fact in the past). One of the main goals of HTML5 is to actually write down all of these unwritten assumptions and undocumented behaviors, so that in the future, someone implementing a browser can do so without being misled by a spec that doesn't match reality, or without having to do the effort of reverse engineering the existing browsers.

It looks to me that you may have to use Method 2 on your embedded browser, and Method 1 if you want to run on the common desktop browsers. It would probably be a good idea to try Method 1 first, and fall back to Method 2 if that does not work, and then error out (or silently fail, depending on your needs) if neither one works.

Brian Campbell
A: 

A long shot but does the embedded browser support iframes? And if so would you be able to use that to load in whatever additional JS you needed and access it via the iframe?

Simon