Here's my presentation from the 2008 edition of Web 2.0, originally given at the crack of dawn, 8:30am, Wednesday, 23 April 2008.
Quite a bit more on the subject is available here:
Some Examples:
Examples found in the wild:
Thank You:
If anyone comes in late, please point them at the URL at the bottom of the screen. Thanks!
Build a script that:
Your basic struture:
<html> <head> <title>Badge Me!</title> </head> <body> <script src="behavior.js"></script> </body> </html>
Save this as behavior.js and run it, by dragging structure.html into your browser:
var MYGLOBAL = window.MYGLOBAL || {};
MYGLOBAL.myProgram = function() {
return {
init : function() {
alert('Hello, world!');
}
};
}();
window.onload = function() {
MYGLOBAL.myProgram.init();
};
Don't close behavior.js or structure.html. We have work to do.
We can start to fix this, by obfuscating our root global variable. Here's one way, with a random function name:
var trueName = '';
for (var i = 0; i < 16; i++) {
trueName += String.fromCharCode(Math.floor(Math.random() * 26) + 97);
}
window[trueName] = {};
window[trueName].myProgram = function() {
return {
init : function() {
alert('Hello, ' + trueName + '!');
}
};
}();
window.onload = function() {
window[trueName].myProgram.init();
};
Save this and run it, to learn your function's true name.
Here we put the whole thing into an anonymous function, and add an internal shortcut. Copy, paste, and run this:
( 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() {
alert('Hello again, ' + trueName + '!');
}
};
}();
window.onload = function() {
$.f.init();
};
})();
To make a badge work, we'll need a container element. We'll also want to clean up after the badge loads, just to be sure.
( 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 = /behavior.js/;
window.onload = function() {
$.f.init(thisScript);
};
})();
Here's where you really do want to be copying and pasting. :)
It's good manners (and critical for low percieved load times) for your badge to wait for the rest of the page to load before firing off. Unfortunately, we can't depend on document.onload.
( 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 = /behavior.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); });
}
})();
Here we check for the script's innerHTML attribute, and attempt to parse it as JSON if it's there.
( 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 + '.';
$.a = {};
if (theScripts[i].innerHTML) {
$.a = $.f.parseJson(theScripts[i].innerHTML);
}
if ($.a.err) {
alert('bad json!');
}
if ($.a.color) {
$.w.style.color = $.a.color;
}
theScripts[i].parentNode.insertBefore($.w, theScripts[i]);
theScripts[i].parentNode.removeChild(theScripts[i]);
break;
}
}
},
parseJson : function(json) {
this.parseJson.data = json;
if ( typeof json !== 'string') {
return {"err":"trying to parse a non-string JSON object"};
}
try {
var f = Function(['var document,top,self,window,parent,Number,Date,Object,Function,',
'Array,String,Math,RegExp,Image,ActiveXObject;',
'return (' , json.replace(/<\!--.+-->/gim,'').replace(/\bfunction\b/g,'function') , ');'].join(''));
return f();
} catch (e) {
return {"err":"trouble parsing JSON object"};
}
}
};
}();
var thisScript = /behavior.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); });
}
})();
Don't run this yet; we need to change structure.html first.
<html>
<head>
<title>Badge Me!</title>
</head>
<body>
<script src="behavior.js">{"color":"red"}</script>
<script src="behavior.js">{"color":"green"}</script>
<script src="behavior.js">{"color":"blue"}</script>
</body>
</html>
Run this, and you ought to see three separate instances of the script on the same page.
Up next: getting data from an outside Web service.
This next little bit is explained in much greater detail in Build a Wikipedia Search Widget in an Hour.
( 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');
$.a = {};
if (theScripts[i].innerHTML) {
$.a = $.f.parseJson(theScripts[i].innerHTML);
}
if ($.a.err) {
alert('bad json!');
}
$.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);
theScripts[i].parentNode.insertBefore($.w, theScripts[i]);
theScripts[i].parentNode.removeChild(theScripts[i]);
break;
}
}
},
runSearch : function() {
$.w.r.innerHTML = '';
if ($.w.q.value) {
if (!$.f.runFunction) {
$.f.runFunction = [];
}
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].Url;
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);
}
},
parseJson : function(json) {
this.parseJson.data = json;
if ( typeof json !== 'string') {
return {"err":"trying to parse a non-string JSON object"};
}
try {
var f = Function(['var document,top,self,window,parent,Number,Date,Object,Function,',
'Array,String,Math,RegExp,Image,ActiveXObject;',
'return (' , json.replace(/<\!--.+-->/gim,'').replace(/\bfunction\b/g,'function') , ');'].join(''));
return f();
} catch (e) {
return {"err":"trouble parsing JSON object"};
}
}
};
}();
var thisScript = /behavior.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); });
}
})();
Don't run this yet; we need to fix up the structure.
<html>
<head>
<title>Badge Me!</title>
</head>
<body>
<script src="behavior.js"></script>
<script src="behavior.js">{ "site":"en.wikipedia.org" }</script>
<script src="behavior.js">{ "query":"madonna", "site":"mtv.com" }</script>
</body>
</html>
Run this, and you ought to see three separate search widgets, with different default sites and queries.
( 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');
$.a = {};
if (theScripts[i].innerHTML) {
$.a = $.f.parseJson(theScripts[i].innerHTML);
}
if ($.a.err) {
alert('bad json!');
}
$.d = {"background":"#fff", "border":"1px solid #000"};
for (var k in $.d) {
if ($.a[k] === undefined) {
$.a[k] = $.d[k];
}
}
var ns = document.createElement('style');
document.getElementsByTagName('head')[0].appendChild(ns);
if (!window.createPopup) {
ns.appendChild(document.createTextNode(''));
ns.setAttribute("type", "text/css");
}
var s = document.styleSheets[document.styleSheets.length - 1];
var rules = {
"" : "{zoom:1;padding:5px;margin:5px;background:" + $.a.background + ";border:" + $.a.border + "}",
"button":"{margin-left:5px;}",
"ul" : "{margin:0; padding:0;}",
"ul li" : "{list-style:none;}",
};
var ieRules = "";
for (r in rules) {
var selector = '.' + trueName + ' ' + r;
if (!window.createPopup) {
var theRule = document.createTextNode(selector + rules[r]);
ns.appendChild(theRule);
} else {
ieRules += selector + rules[r];
}
}
if (window.createPopup) {
s.cssText = ieRules;
}
$.w.className = trueName;
$.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);
theScripts[i].parentNode.insertBefore($.w, theScripts[i]);
theScripts[i].parentNode.removeChild(theScripts[i]);
break;
}
}
},
runSearch : function() {
$.w.r.innerHTML = '';
if ($.w.q.value) {
if (!$.f.runFunction) {
$.f.runFunction = [];
}
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].Url;
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);
}
},
parseJson : function(json) {
this.parseJson.data = json;
if ( typeof json !== 'string') {
return {"err":"trying to parse a non-string JSON object"};
}
try {
var f = Function(['var document,top,self,window,parent,Number,Date,Object,Function,',
'Array,String,Math,RegExp,Image,ActiveXObject;',
'return (' , json.replace(/<\!--.+-->/gim,'').replace(/\bfunction\b/g,'function') , ');'].join(''));
return f();
} catch (e) {
return {"err":"trouble parsing JSON object"};
}
}
};
}();
var thisScript = /behavior.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); });
}
})();
Don't run this yet; we're going to pass some different arguments in for the stylesheet creator.
Here we're dropping in a couple of basic CSS selectors, background and border.
<html>
<head>
<title>Badge Me!</title>
</head>
<body>
<script src="behavior.js"></script>
<script src="behavior.js">{"site":"en.wikipedia.org", "background":"#ffa"}</script>
<script src="behavior.js">{"query":"madonna", "site":"mtv.com", "border":"1px solid red"}</script>
</body>
</html>
Save this and run it; we're almost done.
http://kentbrewster.com/twitterati
http://kentbrewster.com/put-your-digg-in-a-box
Any Yahoo! Pipes Badge
Brady Forrest, Doug Crockford, Hedger Wang, Scott Schille, PPK, Isaac Schleuter, Brothercake, Nate Koechley, Thomas Sha, Matt Sweeney, and Dave Balmer.
Meet Me at the Y! Booth This Afternoon