Howto Dynamically Insert Javascript And CSS

Filed: Thu, Jan 25 2007 under Programming|| Tags: json dhtml javascript css1 css

This is a short and sweet little tutorial to show you how to dynamically insert a new Javascript (or style sheet) into your web pages, and explores how you can exploit cross-domain includes in your applications.

As JSON and DHTML start to get pushed more and more to the forefront of web-based applications, the web designer is faced with a new problem: how to dynamically insert a script element into an existing web page. It won't take long to figure out that ajax loads and innerHTML injections won't work.

Dynamic Cascading Style Sheets

The usefulness of being able to dynamically load a style sheet is fairly limited, but there is one very good reason to keep this tool handy: it lets you load a specific stylesheet for a specific browser. Instead of having one massive style sheet for every browser which visits your page, you can break out the stylesheets into browser specific Firefox, IE, Safari, Opera, etc styles which accommodate the eccentricities of each browser and let you serve smaller css files to your visitors to boot.

The code for this is just as simple as the javascript.

var headID = document.getElementsByTagName("head")[0];         
var cssNode = document.createElement('link');
cssNode.type = 'text/css';
cssNode.rel = 'stylesheet';
cssNode.href = 'FireFox.css';
cssNode.media = 'screen';
headID.appendChild(cssNode);

We get the <head> tag, then create the link and apply the attributes. When it's all set up we insert the new cssNode into the head section of our webpage where the various styles are instantly applied.

A complete reference for adding, creating, altering and deleting stylesheets and their elements can be found in a newer article titled Totally Pwn CSS with Javascript. If you are looking to do more than dynamically add a stylesheet you should definitely check out this article.

Dynamic Javascript Insertion

Fortunately, dynamic insertion of CSS or Javascript is relatively painless.

var headID = document.getElementsByTagName("head")[0];         
var newScript = document.createElement('script');
newScript.type = 'text/javascript';
newScript.src = 'http://www.somedomain.com/somescript.js';
headID.appendChild(newScript);

It's really that simple. headID gets the <head> element of the page. Next we create a new 'script' element, assign it a text/javascript type, and then the url of the script (which can be anywhere on the net since it's basically a javascript include). Finally we append the new element to our head section where it is automatically loaded.

If you're loading an external javascript and you need to know when the script has loaded you can simply use .onload=yourfunction; to set up the onload handler. Here's an example.

var headID = document.getElementsByTagName("head")[0];         
var newScript = document.createElement('script');
newScript.type = 'text/javascript';
newScript.onload=scriptLoaded;
newScript.src = 'http://api.flickr.com/services/feeds/photos_public.gne?tags=sunset&format=json';
headID.appendChild(newScript);

Now when this script has been loaded the .onload event will call a function named scriptLoaded().

Caveat Emptor

Remember that when you're importing cross-domain javascript in this manner, you are giving the site you are connecting to, total, perfect control over your web page. To get an idea of what is basically possible with this sort of cross-domain scripting, boot up Firefox and head over to ma.gnolia.com. Drag the red "roots" banner up to your bookmark bar (right click next to the menu to make sure it's visible first), visit any other site on the net and click your new bookmarklete button.

When you click, the bookmark inserts a javascript element just like the examples above and fades in a traveling forum where people can talk about the page you're visiting. When you click close ma.gnolia.com fades out and you're back on the page exactly where you left off.

Another benign example, but what if a disgruntled employee at flickr alters their script so that in addition to sending you the search results it also sets up a listener to intercept form submissions in an attempt to locate sensitive data?

So, while being VERY powerful, you should use extreme caution when deciding to cross-connect domains in this fashion.

Adding Javascript Through Ajax

On your own site and domain you can use dynamic script loading to keep the size of your javascripts small and serve only what the user actually needs, as the scripts are needed. You should of course use AJAX ( responseText ) to retrieve JSON formated data and Javascripts off your own site instead of dynamic <script> attachment. Here's an example of an ajax object which will load a javascript file then pass it through the eval command (which is basically just the javascript compiler). After this is done the functions and variables will be available to the other functions in your web page.

function ajaxObject(url, callbackFunction) {
  var that=this;      
  this.updating = false;

  this.update = function(passData,postMethod) { 
    if (that.updating==true) { return false; }
    that.updating=true;                       
    var AJAX = null;                          
    if (window.XMLHttpRequest) {              
      AJAX=new XMLHttpRequest();              
    } else {                                  
      AJAX=new ActiveXObject("Microsoft.XMLHTTP");
    }                                             
    if (AJAX==null) {                             
      return false;                               
    } else {
      AJAX.onreadystatechange = function() {  
        if (AJAX.readyState==4) {             
          that.updating=false;                
          that.callback(AJAX.responseText,AJAX.status,AJAX.responseXML);        
          delete AJAX;                                         
        }                                                      
      }                                                        
      var timestamp = new Date();                              
      if (postMethod=='POST') {
        var uri=urlCall+'?'+timestamp.getTime();
        AJAX.open("POST", uri, true);
        AJAX.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
        AJAX.send(passData);
      } else {
        var uri=urlCall+'?'+passData+'×tamp='+(timestamp*1); 
        AJAX.open("GET", uri, true);                             
        AJAX.send(null);                                         
      }              
      return true;                                             
    }                                                                           
  }
  var urlCall = url;        
  this.callback = callbackFunction || function () { };
}  

function attachScript(responseText, responseStatus) {
   // This function is called by the ajaxObject when the server has finished
	 // sending us the requested script.
   if (responseStatus==200) {
	    eval(responseText);
	 }
}

// ajaxObject is an object constructor, pass it the server url you want it to call
// and the function name you want it to call when it gets the data back from the server.
// Use the .update() method to actually start the communication with the server.
// The first optional argument for update is the data you want to send to the server.
// ajaxvar.update('id=1234&greed=good&finish=true');
// The second optional argument for update is 'POST' if you want to send the data
// as a POST instead of the default GET (post can handle larger amounts of data and
// the data doesn't show up in your server logs).
// ajaxvar.update('id=1234&greed=good&finish=true','POST');

var getScriptViaAjax=new ajaxObject('http://mydomain.com/somescript.js',attachScript);
    getScriptViaAjax.update();
var anotherScript = new ajaxObject('http://mydomain.com/anotherScript.php',attachScript);
    anotherScript.update('userId=4323','POST');

It's important to note that we don't need a server-side script here. When we create our ajaxObject we pass it the url of a javascript file which is the exact same as any file you'd set up as an external <script>. The ajaxObject will retrieve the file then pass it to attachScript which then passes the file through eval which executes it exactly as if it were being loaded in via the <script> tag. You can call a script if you want and have php or some other server-side scripting language generate custom, dynamic javascript on the fly, but you can also call a static javascript file as well.

Tricks and Tips

One of the key attractions to dynamic script insertion is that scripts, unlike ajax calls, are not bound by the "same domain" rule. If you try to contact Flickr directly with an Ajax call your code will throw a security error. If you try to include Flickr's json feeds however, you'll get a nicely packed object stuffed full of data.

If you're a Flickr junkie you probably already know that you can do a keyword RSS search with the following url (in this case, sunsets).

http://api.flickr.com/services/feeds/photos_public.gne?tags=sunset&format=rss_200

You probably also know that to use that, you're going to need a server-side script to suck the data down and then format it. With JSON however you can simply use an undocumented Flickr API to do the same search and get the results directly back in the browser and then let javascript process, format, and display the data.

var headID = document.getElementsByTagName("head")[0];         
var newScript = document.createElement('script');
newScript.type = 'text/javascript';
newScript.src = 'http://api.flickr.com/services/feeds/photos_public.gne?tags=sunset&format=json';
headID.appendChild(newScript);

The script flickr sends you is nothing more than a function call -- jsonFlickrFeed(LOTS of data as an object) -- with a lot of data formatted as a javascript object passed in the first argument. When the script is loaded it will call a function called jsonFlickrFeed which you will need to write. The first parameter of jsonFlickrFeed will contain the record with your search results. So if you create a function titled jsonFlickrFeed(feed) then "feed" will have the results you requested.

Feed's structure is fairly simple

"title": "sunset - Everyone's Tagged Photos"
"link": "http://www.flickr.com/photos/tags/sunset/" 
"description": "A feed of sunset - Everyone's Tagged Photos"
"modified": "2007-01-29T16:45:24Z"
"generator": "http://www.flickr.com/"
"items": [ { "title": "IMG_0697"
             "link": "http://www.flickr.com/photos/32655671@N00/373458148/"
             "media": { "m":"http://farm1.static.flickr.com/123/373458148_70dbabf167_m.jpg" }
             "date_taken": "2006-07-22T22:51:18-08:00"
             "description": "Long description -- contians HTML"
             "published": "2007-01-29T16:45:24Z"
             "author": "nobody@flickr.com (mae2007)"
             "tags": "africa sunset water" 
           }
          ]

This is very similar to an rss layout however it is in object notation. So you can access the main title with feed.title, the link as feed.link. "items" is just an array of objects so feed.items[x].title will give you the title of item #x in the array. There's a little oddity in that "media" is another object, perhaps anticipating supplying urls to the different sized resolutions of the images. To get the url of the first image you would access it through "feed.items[0].media.m".

You can see the complete JSON data file here. (Thanks to the pros at flickr for tweaking their servers to send the data nicely formatted!)

Here is a simple shell that you can use and expand on. All it does is accept the data and output a list of all the items it recieved.

function jsonFlickrFeed(feed){
  for (x=0; x<feed.items.length; x++) {
     document.writeln(feed.items[x].title + '-' + feed.items[x].link + '<BR>');
  }
}

function startup() {
   var headID = document.getElementsByTagName("head")[0];         
   var newScript = document.createElement('script');
   newScript.type = 'text/javascript';
   newScript.src = 'http://api.flickr.com/services/feeds/photos_public.gne?tags=sunset&format=json';
   headID.appendChild(newScript);  
}
<body onload='startup()'>

Extending this, I added a simple form and handler to accept user input, generate the search url and make the JSON call. Here's the source code.

<script type="text/javascript">
function jsonFlickrFeed(feed){
  z='';
  for (x=0; x<feed.items.length; x++) {
     tmp=feed.items[x].media.m;
     tmp=tmp.replace(/_m\.jpg/g,'_s.jpg');
     z+='<img src="'+tmp+'" alt="some img" width="75px" height="75px" style="margin: 2px;">';
  }
  document.getElementById('pics').style.display='block';
  document.getElementById('pics').innerHTML=z;
}
function searchFlickr() {
   var headID = document.getElementsByTagName("head")[0];         
   var newScript = document.createElement('script');
   tagID = escape(document.getElementById('tags').value);
   document.getElementById('tags').value='';
   newScript.type = 'text/javascript';
   newScript.src = 'http://flickr.com/services/feeds/photos_public.gne?tags=' + tagID + '&format=json';
   headID.appendChild(newScript);
   document.getElementById('pics').style.display='block';
   document.getElementById('pics').innerHTML="Loading...";
   return false;  
}
</script>
<form action = "#" onsubmit="return searchFlickr();">
<input type='text' size='40' id='tags'>&nbsp;&nbsp;&nbsp;<input type='submit'>
</form>
<div style='border: 1px solid black; width: 100%; display: none;' id='pics'></div>

Here's the working demonstration. Type a tag (like "sunset" or "mountain", and hit enter to see a list of images flickr finds.

   

Note that at no point did this web page contact www.hunlock.com to process this request. The request to flickr was made directly inside this web page, the data received directly, and then processed and displayed (however inadequately).

This is probably the future of RSS feeds. Rather than a browser sending a request to a server to contact ANOTHER server to get an RSS feed and then format it before sending it back to the browser, the browser itself will directly extract the RSS data in JSON, format it and display it directly on the page. Yahoo and Flickr are already preparing for the day, the rest of the net will be a few years behind the curve.

Conclusion

And there you have it, a crash course in leveraging dynamic javascript and stylesheet insertion.