[just about everything in here has been overcome by broken APIs. Please see my GitHub repo for the current state of case-hardened JavaScript.]
[updated 6/17/2008: the related presentation from Web 2.0 is finally online. Please go check it out; much of what's here is outdated.]
Everybody likes a nice little Web badge. Myspace is crawling with spiffy Flash boxes. Facebook has an entire API dedicated to creating applications. Google's just released their embedded maps product. Yahoo!'s had a customizable home page and off-network search badges for over ten years. Ning and Pageflakes are nothing but badges. Here's a bookmark badge from del.icio.us, which you ought to use right now:
Instant social networking, no waiting.
This we know: global variables are evil, so we shouldn't depend on them.
Anonymizing our root global object, and getting it to show up where we want it.
JSON is your friend.
In which we add some interaction, and learn something cool about scope.
Safely importing external data.
Yahoo's top five search results for the term of your choice, with optional site restriction.
Up next: including stylesheets, ignoring invalid arguments, and what to do if they won't give you a proper callback.
Things I learned on the way to looking up other things.
I owe everybody. Bigtime.
Up next to the table of contents (if you're using an A-grade browser, and if r8ar.com stands up under the load) you ought to see the Twitterati badge. I've done a bit of customizing to make it look just like this page
user
: defaults to my account, kentbrew
.height
: defaults to 350width
: defaults to 250border
: defaults to 2px solid blue
background
: defaults to white
; if you set evenBackground
, oddBackground
, and headerBackground
, you won't see it at all.userBorder
: defaults to 1px dashed gray
userColor
: defaults to whatever link color's on your pageheaderBackground
: defaults to your main background.headerColor
: defaults to black
.evenBackground
: defaults to your main background.oddBackground
: defaults to gray
.friends_timeline
, and happen to request the same user id you saw here, you might get another copy of your cached call, with my callback twitBack
and not the other guy's.Anybody got an iPhone? I bet this would look rilly kewl on an iPhone....
Here are the settings for my badge:
<script type="text/javascript" src="http://r8ar.com/twitterati.js"> { "user":"kentbrew", "border":"3px solid #442", "headerBackground":"#055", "headerColor":"#fff", "userBorder":"2px solid #f4bb2e", "oddBackground":"#ffe" } </script>
White space doesn't matter; I've spread this one out for readability. Substitute your own Twitter id, and fiddle with the settings until it look like your page.
Currently, many advanced JavaScript applications look like this:
var MYGLOBAL = window.MYGLOBAL || {}; MYGLOBAL.myProgram = function() { return { myFunction : function() }{ alert('Hello, world!'); } }; }(); window.onload = function() { MYGLOBAL.myProgram.myFunction(); }
This is all familiar stuff, straight from the Module Pattern, first attributed to Cornford and Crockford, explained in such a manner that my tiny brain could wrap around it by Dustin Diaz in JSON for the Masses, used here many times, and ably summarized by Eric Miraglia on the YUIBlog, in A JavaScript Module Pattern.
Much of this is based on the (correct) idea that global variables are evil. The example program above could be compromised by any application rendered further down the stream that happened to redefined MYGLOBAL
or grab the window.onload
event, which is based on the global variable window
.
If you're lucky enough to be in full control of your page, none of this matters, at least not right here and now. You can do whatever you want.
If, on the other hand, there's any chance your page might include somebody else's code--perhaps you have a Wordpress blog?--or you forsee a need to make changes to it sometime in the future, you might want to consider another approach.
The basic script does nothing but find itself in the page, create a DIV
to play in, run itself, and delete itself.
Here's how we're calling it:
<script type="text/javascript" src="http://r8ar.com/cha.js"></script>
Here's our result:
Magnificent, no? Reload the page to see a different sixteen-character string of gibberish. If you view the post-rendered DOM tree (use Firebug, please) you won't actually see a script node anywhere, just a DIV
containing the script's output.
( function() { myFunction } )();
trueName
and base the application on it: window[trueName] = {};
trueName
from two different runs on the same page. I like the odds, which are 26^16--about 43 octillion--to one.window[trueName]
set, we assign private variable $
, and return our subfunctions under our main function, $.f
.f
is purely arbitrary on my part; they could be any valid object names.document.getElementsByTagName('SCRIPT')
, running through them one at at time, and finding one that matches our target, specified at the bottom in $.f.init(thisScript);
thisScript
forces a match to the whole string with the ^
in front and the $
in back. In the middle we've got a little regexp magic (thanks to JR Conlin) that matches any sub-domain in front of r8ar.com
. I could force it to exactly match my domain name, but I'm leaving the pattern in for folks who don't have a choice. It should work for most other domains; use it if there's any possibility that your domain will respond to www.yourthing.com
and yourthing.com
.DIV
in our private object $
, name it $.w
, and insert it before the script node. $.w
becomes our main structure object in the DOM; everything else the user sees gets appended here.document.onload
from everything else on the page or freezing everything else up during load while it renders--attaches an event listener and waits to render until the page has loaded.Sorry that took so long; it gets easier further down. Here's the script, which is shorter than its explanation:
( function() { var trueName = ''; for (var i = 0; i < 16; i++) { trueName += String.fromCharCode(Math.floor(Math.random() * 26) + 97); } window[trueName] = {}; var $ = window[trueName]; $.f = function() { return { init : function(target) { var theScripts = document.getElementsByTagName('SCRIPT'); for (var i = 0; i < theScripts.length; i++) { if (theScripts[i].src.match(target)) { $.w = document.createElement('DIV'); $.w.innerHTML = 'Hello, world. My name is ' + trueName + '.'; theScripts[i].parentNode.insertBefore($.w, theScripts[i]); theScripts[i].parentNode.removeChild(theScripts[i]); break; } } } }; }(); var thisScript = /^https?:\/\/[^\/]*r8ar.com\/cha.js$/; if(typeof window.addEventListener !== 'undefined') { window.addEventListener('load', function() { $.f.init(thisScript); }, false); } else if(typeof window.attachEvent !== 'undefined') { window.attachEvent('onload', function() { $.f.init(thisScript); }); } } )();
Just for fun, run this and pop open Firebug and examine the post-rendered HTML. You'll notice that you can't actually see a reference to the script that created the node above, just a DIV with some content inside.
We'll want to allow the user to pass in an argument or two, or six. There are several ways to do this; we'll test the innerHTML of the script for malicious-looking contents, and attempt to treat it as JSON and parse it into an object if it passes.
Here are our calls and results:
<script type="text/javascript" src="http://r8ar.com/chb.js"></script>
<script type="text/javascript" src="http://r8ar.com/chb.js"> { "color":"red" } </script>
<script type="text/javascript" src="http://r8ar.com/chb.js"> { "color":"green" } </script>
eval
ing it. If the result is an object, we store it in $.a
, and assume it's a bunch of user arguments. As $.w
became our structure object, $.a
becomes our arguments object. Anything the user wants the script to do will be stored as a member of $.a
.color
, we're applying it to our structure through another try{} catch{}
block. This is very important; if we attempt to apply an invalid value to part of our DOM tree, we could mess things up badly.JSON stands for "JavaScript Object Notation," a structure first discovered by Douglas Crockford, and documented at JSON.org. Here's the ten-cent tour: JSON is data expressed as native JavaScript code, consisting of an object surrounded by curly brackets. Inside the object are key-value pairs, delimited by double-quotes, punctuated by colons, and separated by commas. Values can be null, boolean, numeric, or string, plus arrays or objects containing any of the above, so JSON objects can be much more complex than the usual string of key-value pairs passed to JavaScript objects.
The bottom line: by using JSON, we are able to pass much more complex information to our script, and parse it and test it for validity with fewer lines of code.
Here's the script:
( function() { var trueName = ''; for (var i = 0; i < 16; i++) { trueName += String.fromCharCode(Math.floor(Math.random() * 26) + 97); } window[trueName] = {}; var $ = window[trueName]; $.f = function() { return { init : function(target) { var theScripts = document.getElementsByTagName('SCRIPT'); for (var i = 0; i < theScripts.length; i++) { if (theScripts[i].src.match(target)) { $.a = {}; if (theScripts[i].innerHTML) { if (/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]+$/.test(theScripts[i].innerHTML.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, ''))) { try { $.a = eval( '(' + theScripts[i].innerHTML + ')' ); } catch(err) { } } } if (typeof $.a !== 'object') { $.a = {}; } $.w = document.createElement('DIV'); $.w.innerHTML = 'Yo! My name is ' + trueName + '.'; if ($.a.color) { try { $.w.style.color = $.a.color; } catch(err) { } } theScripts[i].parentNode.insertBefore($.w, theScripts[i]); theScripts[i].parentNode.removeChild(theScripts[i]); break; } } } }; }(); var thisScript = /^https?:\/\/[^\/]*r8ar.com\/chb.js$/; if(typeof window.addEventListener !== 'undefined') { window.addEventListener('load', function() { $.f.init(thisScript); }, false); } else if(typeof window.attachEvent !== 'undefined') { window.attachEvent('onload', function() { $.f.init(thisScript); }); } } )();
If our JSON validity test fails, or the result won't eval
into an object, we set $.a
to an empty object and run the script without any arguments.
Let's make it actually do something, shall we? Here we generate a link that calls a function to reveal the script's true name when clicked. This illustrates two things: simplification of scope concerns--$.f
is one character shorter than this
, and works for all scopes--and that the value of trueName
is available throughout the script.
Here are our calls and results:
<script type="text/javascript" src="http://r8ar.com/chc.js"></script>
<script type="text/javascript" src="http://r8ar.com/chc.js"> { "color" : "gold" } </script>
<script type="text/javascript" src="http://r8ar.com/chc.js"> { "color" : "mcfoobar" } </script>
try{}
and catch{}
to apply styles, bad values shouldn't break anything. (Right, we could use that catch
block to assign bad arguments to a bad-argument container and do something with them later, if needed.)YOURGLOBAL.yourDomain.yourScript.yourFunction
whenever you made a significant update, or wanted to plug in a piece of somebody else's library? Gone. From now on, all you need to call is $.f.yourFunction
.YOURGLOBAL.yourDomain.yourScript.yourFunction
again--or could get away with using this.yourFunction
. As above, from now on, all you need is $.f.yourFunction
..
Here's the script:
( function() { var trueName = ''; for (var i = 0; i < 16; i++) { trueName += String.fromCharCode(Math.floor(Math.random() * 26) + 97); } window[trueName] = {}; var $ = window[trueName]; $.f = function() { return { init : function(target) { var theScripts = document.getElementsByTagName('SCRIPT'); for (var i = 0; i < theScripts.length; i++) { if (theScripts[i].src.match(target)) { if (theScripts[i].innerHTML) { if (/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]+$/.test(theScripts[i].innerHTML.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, ''))) { try { $.a = eval( '(' + theScripts[i].innerHTML + ')' ); } catch(err) { } } } if (typeof $.a !== 'object') { $.a = {}; } $.w = document.createElement('DIV'); var a = document.createElement('A'); a.innerHTML = 'Click me to reveal my true name and color, if any.'; a.onmousedown = function() { $.f.revealName(); }; if ($.a.color) { try { a.style.color = $.a.color; } catch(err) { } } $.w.appendChild(a); theScripts[i].parentNode.insertBefore($.w, theScripts[i]); theScripts[i].parentNode.removeChild(theScripts[i]); break; } } }, revealName : function() { var msg = 'My true name is ' + trueName; if ($.a.color) { msg += ' and my color is ' + $.a.color; } alert(msg); } }; }(); var thisScript = /^https?:\/\/[^\/]*r8ar.com\/chc.js$/; if(typeof window.addEventListener !== 'undefined') { window.addEventListener('load', function() { $.f.init(thisScript); }, false); } else if(typeof window.attachEvent !== 'undefined') { window.attachEvent('onload', function() { $.f.init(thisScript); }); } } )();
Creating and destroying dynamic functions is the key to interactivity with outside data sources. To be safe, every time we tap outside data, we should create a separate bucket to hold the possibly-corrupt reply. Further on down the line we'll create a new function which will wait for something to happen, make a change in the structure of the page, and then delete itself.
Here we'll wait for a click and then generate a unique instance of a dynamic function. Each instance of the function will be a new member of a single array; to see the index climbing, click a link more than once.
Calls and results:
<script type="text/javascript" src="http://r8ar.com/chd.js"> { "color" : "red" } </script>
<script type="text/javascript" src="http://r8ar.com/chd.js"></script>
<script type="text/javascript" src="http://r8ar.com/chd.js"> { "color" : "teal" } </script>
$.f
, we're adding an array, runFunction
. This array will hold all of our dynamic functions as they are created, and, since it's an array, the value of each member's index--the number inside the square brackets--will be accessible to functions further down the line.makeFunction
gets the next available slot in runFunction
and uses it in several places: first, to determine which slot in the array should be filled with a function, second as part of the string rendered as the function, and finally as the index pointing to whichever member of the (potentially very large) array of dynamically-generated functions should be run.renderResult
doesn't do much; it just alerts whatever's been sent to it, and lets us know if the optional color
argument has been passed.Here's the script:
( function() { var trueName = ''; for (var i = 0; i < 16; i++) { trueName += String.fromCharCode(Math.floor(Math.random() * 26) + 97); } window[trueName] = {}; var $ = window[trueName]; $.f = function() { return { runFunction : [], init : function(target) { var theScripts = document.getElementsByTagName('SCRIPT'); for (var i = 0; i < theScripts.length; i++) { if (theScripts[i].src.match(target)) { if (theScripts[i].innerHTML) { if (/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]+$/.test(theScripts[i].innerHTML.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, ''))) { try { $.a = eval( '(' + theScripts[i].innerHTML + ')' ); } catch(err) { } } } if (typeof $.a !== 'object') { $.a = {}; } $.w = document.createElement('DIV'); var p = document.createElement('P'); var a = document.createElement('A'); a.innerHTML = 'Click me to run a unique instance of a dynamically generated function.'; a.onmousedown = function() { $.f.makeFunction(this); }; if ($.a.color) { try { a.style.color = $.a.color; } catch(err) { } } $.w.appendChild(a); theScripts[i].parentNode.insertBefore($.w, theScripts[i]); theScripts[i].parentNode.removeChild(theScripts[i]); break; } } }, makeFunction : function(el) { var n = $.f.runFunction.length; $.f.runFunction[n] = function() { $.f.renderResult(trueName + '.runFunction['+ n + '] has run'); delete($.f.runFunction[n]); }; $.f.runFunction[n](); }, renderResult: function(r) { if ($.a.color) { r += ' from a lovely ' + $.a.color + ' link'; } alert(r); } }; }(); var thisScript = /^https?:\/\/[^\/]*r8ar.com\/chd.js$/; if(typeof window.addEventListener !== 'undefined') { window.addEventListener('load', function() { $.f.init(thisScript); }, false); } else if(typeof window.attachEvent !== 'undefined') { window.attachEvent('onload', function() { $.f.init(thisScript); }); } } )();
Where might code like this be useful? In any script that makes repeated calls to an outside data source--such as, say, oh, I don't know ... an inline search application--it's important to know which incoming reply corresponds to which outgoing request. Having access to the index of runFunction
allows the script to definitively match reponses to requests, so we never serve up the wrong set at the wrong time.
Here are three instances of the same script, with different arguments passed to each and stored in $.a
. The first is plain old Web search, with no default query. The second and third have domains and default queries specified, so they're more fun to demo. Enter something into each box and either hit Enter or click Search. If Yahoo! Search finds any results, you'll see up to the top five. (No, this isn't my usual search widget; this one is intentionally small and primitive, for clarity of instruction.)
<script type="text/javascript" src="http://r8ar.com/che.js"></script>
<script type="text/javascript" src="http://r8ar.com/che.js"> { "site" : "en.wikipedia.org", "query" : "Nixon" } </script>
<script type="text/javascript" src="http://r8ar.com/che.js"> { "site" : "developer.yahoo.com", "query" : "JavaScript" } </script>
$.w
. We can read from or write to them from anywhere in the script.buildStructure
removes the sticky business of building the thing from the main loop in init
.runScript
creates and runs a new script node, by appending it to the body of our document.removeScript
removes the script of our choice.makeFunction
to runSearch
, which is what it's really doing.onkeypress
and onmousedown
. These are both attached them to runSearch
. (To make the application behave more like a form, we're capturing onkeypress
over the input blank and watching for event.keyCode
13, which means the user has hit Enter while inside the input box.)Here's the script:
( function() { var trueName = ''; for (var i = 0; i < 16; i++) { trueName += String.fromCharCode(Math.floor(Math.random() * 26) + 97); } window[trueName] = {}; var $ = window[trueName]; $.f = function() { return { runFunction : [], init : function(target) { var theScripts = document.getElementsByTagName('SCRIPT'); for (var i = 0; i < theScripts.length; i++) { if (theScripts[i].src.match(target)) { if (theScripts[i].innerHTML) { if (/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]+$/.test(theScripts[i].innerHTML.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, ''))) { try { $.a = eval( '(' + theScripts[i].innerHTML + ')' ); } catch(err) { } } } if (typeof $.a !== 'object') { $.a = {}; } $.f.buildStructure(); theScripts[i].parentNode.insertBefore($.w, theScripts[i]); theScripts[i].parentNode.removeChild(theScripts[i]); break; } } }, buildStructure : function() { $.w = document.createElement('DIV'); $.w.q = document.createElement('INPUT'); if ($.a.query) { $.w.q.value = $.a.query; } $.w.q.onkeypress = function(e) { if ( (e ? e.which : event.keyCode) == 13) { $.f.runSearch(); } }; $.w.appendChild($.w.q); $.w.b = document.createElement('BUTTON'); $.w.b.innerHTML = 'Search'; if ($.a.site) { $.w.b.innerHTML += ' ' + $.a.site; } $.w.b.onmouseup = function() { $.f.runSearch(); }; $.w.appendChild($.w.b); $.w.r = document.createElement('UL'); $.w.appendChild($.w.r); }, runSearch : function() { $.w.r.innerHTML = ''; if ($.w.q.value) { var n = $.f.runFunction.length; var id = trueName + '.f.runFunction[' + n + ']'; $.f.runFunction[n] = function(r) { delete($.f.runFunction[n]); $.f.removeScript(id); $.f.renderResult(r); } var url = 'http://search.yahooapis.com/WebSearchService/V1/webSearch?'; url += '&appid=YahooSearch'; url += '&results=5'; url += '&output=json'; url += '&query=' + $.w.q.value; url += '&callback=' + id; if ($.a.site) { url += '&site=' + $.a.site; } $.f.runScript(url, id); } }, renderResult: function(r) { for (var i = 0; i < r.ResultSet.Result.length; i++) { var li = document.createElement('LI'); var a = document.createElement('A'); a.innerHTML = r.ResultSet.Result[i].Title; a.href = r.ResultSet.Result[i].ClickUrl; a.target = '_blank'; li.appendChild(a); $.w.r.appendChild(li); } }, runScript : function(url, id) { var s = document.createElement('script'); s.id = id; s.type ='text/javascript'; s.src = url; document.getElementsByTagName('body')[0].appendChild(s); }, removeScript : function(id) { var s = ''; if (s = document.getElementById(id)) { s.parentNode.removeChild(s); } } }; }(); var thisScript = /^https?:\/\/[^\/]*r8ar.com\/che.js$/; if(typeof window.addEventListener !== 'undefined') { window.addEventListener('load', function() { $.f.init(thisScript); }, false); } else if(typeof window.attachEvent !== 'undefined') { window.attachEvent('onload', function() { $.f.init(thisScript); }); } } )();
If you're running Firebug, open it up, click the HTML tab, go all the way to the bottom of the body, and run it again. You'll see your dynamic script tag pop up, wait, and go away.
Topics to be discussed next: including stylesheets, ignoring invalid arguments, and what to do if the API won't give you a proper callback.
(Actually ... you should check out the Twitterati badge at the top of the page, which takes on much of this stuff.)
http://www.amazon.com/True-Names-Opening-Cyberspace-Frontier/dp/0312862075/
http://www.mikeindustries.com/blog/archive/2007/06/widget-deployment-with-wedje
http://paulbakaus.com/2007/08/17/advanced-ternary-conditions-in-javascript/
Friends and family, past, present, and future: Adam Platti, Andy Baio, Ava Hristova, Barney Mok, Bill Scott, Chad Dickerson, Chanel Wheeler, Christian Heileman, Dan Theurer, Dave Balmer, Douglas Crockford, Ed Ho, Eric Miraglia, Gina Groom, Hedger Wang, Heidi Pollack, Isaac Schleuter, JR Conlin, Jason Levitt, Jeremy Gillick, Jeremy Zawodny, Jimmy Byrum, Jonathan Trevor, Leonard Lin, Matt McAlister, Matt Sweeney, Micah Laaker, Mike Davidson, Mike Lee, Nate Koechley, Pasha Sadri, Paul Bakaus, Peter Michaux, Rasmus Lerdorf, Scott Schiller, Steve Carlson, Taylor McKnight, and Vickie Brewster.
Any ideas/suggestions on how this might be expanded to have similar resiliency? It looks like youtube is using the object and embed tag. I wonder if these could be employed to make it even more case hardened?
This is the actual script tag...
<script type="text/javascript" src="http://clearimageonline.com/pages/start/pasteboard/p8ste.js">{ "p8ste": "f28", "text": "Show" }</script>
And here is what it should render...
{ "p8ste": "f28", "text": "Show" }
http://fe.shortcuts.search.yahoo.com/widget/buzz.php
Some interesting techniques I hadn't thought of; need to go ponder.
http://decafbad.com/blog/2008/06/16/firefox-3-download-day-mega-widget