doStuffWith();doStuffWith(thatThing);
To separate these three layers, we will call the CSS from a link in the document <head> and the JavaScript from a link just before the closing </body> tag. As has been repeatedly proven by Mr. Steve Souders--our performance guru at Yahoo!, who literally wrote the book on the subject--this is the method that works best for the greatest number of Web browsers.<html> <head> <title>BBC News Search</title> <link rel="stylesheet" type="text/css" href="presentation.css" /> </head> <body> <h1>Hello, World!</h1> <script type="text/javascript" src="behavior.js"></script> </body> </html>
body { background-color:yellow; }var BBCSEARCH = function() {
return {
init : function() {
BBCSEARCH.doStuff();
},
doStuff : function() {
alert('Hello, world!');
}
};
}();
window.onload = function() {
BBCSEARCH.init();
};
Object-oriented programming has yet to be documented in a way that makes sense to me, and what you're about to read is, sadly, no different.
For this presentation I'm going to briefly show what you need to know to have the example make sense. Please keep in mind that this is the tip of a very large iceberg:
I am still refining this, and working on other diversions around topics like scope. Comments would be welcome!
Consider the following sentences:
"Having the one-off function call return the class constructor forms a closure that can hold private static members and assign privileged static methods as properties of the returned constructor."
... and then, there's this:
"When the tweedle beetles battle with their paddles in a bottle full of water on a noodle-eating poodle, this is what we call a tweedle beetle noodle poodle water bottle paddle battle."
The second sentence is from a children's book by Dr. Seuss. The first comes straight from the maintainer of comp.lang.javascript FAQ, and is representative of the general level of language you're going to find around this topic. To someone approaching object-oriented programming with an empty mind and a clean slate, the semantic value of these two sentences is equivalent: zero!
There is one and only one good book on JavaScript, David Flanagan's JavaScript: The Definitive Guide, now out in its fifth edition from O'Reilly Books. You'll know it by the rhino on the cover. I also strongly recommend the ydn-javascript discussion group at groups.yahoo.com, and the YUI blog, at yuiblog.com.
Up next: getting our hands dirty with objects.
Objects are collections of values, which can be null, undefined, booleans, numbers, and strings, plus other objects, which include arrays of any of the above.
Objects can contain named values:
var a = { "name" : "Bob Dobbs" };
alert(a.name);
Objects can contain arrays:
var a = { "id" : [6, 19, 465] };
alert(a.id[1]);
Objects can contain other objects:
var a = { "people" : [
{ "name":"Bob Dobbs", "email":"bob@dobbs.com" },
{ "name":"Ted Knight", "email":"ted@knight.com" },
{ "name":"Carol Merrill", "email":"carol@merrill.com" },
{ "name":"Alice Springs", "email":"alice@springs.com" }
]};
alert(a.people.length);
alert(a.people[2].email);
But that's not all. Objects can also contain functions.
var a = {
doStuff : function() {
alert(a.people[3].name);
},
people : [
{ "name":"Bob Dobbs", "email":"bob@dobbs.com" },
{ "name":"Ted Knight", "email":"ted@knight.com" },
{ "name":"Carol Merrill", "email":"carol@merrill.com" },
{ "name":"Alice Springs", "email":"alice@springs.com" }
]
};
a.doStuff();
Here we see a mixed object a, which contains function doStuff and array people, which in turn contains separate name and email objects for each person. When doStuff fires, we should see an alert for person 3's name.
Very important: while all arrays are objects, not all objects are arrays! So the order in which the parts of a are listed is not important. Reading this suggests that doStuff should error out, since the people object hasn't yet been seen by the interpreter. Not true, since a.doStuff(); happens outside the object a.
Next up: security concerns.
A lot can go wrong on a Web page, especially one that might be hosting third-party advertising. Here's how to shield our application from outside meddling, and vice versa:
var a = function() {
return {
doStuff : function() {
alert(a.people[3].name);
},
people : [
{ "name":"Bob Dobbs", "email":"bob@dobbs.com" },
{ "name":"Ted Knight", "email":"ted@knight.com" },
{ "name":"Carol Merrill", "email":"carol@merrill.com" },
{ "name":"Alice Springs", "email":"alice@springs.com" }
]
};
}();
Additions are in white; we've wrapped our functions in an anonymous return, and added the empty parentheses at the end. This invokes closure, which runs the function immediately and keeps its results in memory for future access.
There are, of course, more complications. Now that we've shielded our function from the outside world, we're going to have a hard time passing data back and forth between our subfunctions. For that we'll be using private variables.
Now that we have an impenetrable shield around our function, we need to be able to pass data back and forth between its parts. We'll do that with a private variable:
var a = function() {
var myName = 'Earl';
return {
doStuff : function() {
alert(myName);
},
people : [
{ "name":"Bob Dobbs", "email":"bob@dobbs.com" },
{ "name":"Ted Knight", "email":"ted@knight.com" },
{ "name":"Carol Merrill", "email":"carol@merrill.com" },
{ "name":"Alice Springs", "email":"alice@springs.com" }
]
};
}();
If we put myName outside this function, it would be available for any other script on the page to monkey around with. Because it's outside our anonymous return, it acts like a global variable available only to us.
So. We've got this bullet-proof set of functions with stable data inside. How do we start it up? It's not going to fire off automatically any more.
Although there are several ways of doing this, grabbing the window.onload event and firing off the anonymous function seems to be the most solid method.
window.onload = function() {
a.doStuff();
};
If we need to pass a variable in later, here's how:
window.onload = function() {
a.doStuff('withMyStuff');
};
If you don't have access to window.onload, you can try dropping it in inline:
a.doStuff();
This is fraught with peril, especially if your function relies on the page already being fully rendered, and definitely not recommended for production environments.
That's it for our brief side trip into object-oriented JavaScript. My best advice: keep reading, keep practicing, try to pick up patterns in structure, and let it sort of wash over you and soak in. It'll come, eventually.
var BBCSEARCH = function() {
var $ = {};
return {
init : function(el) {
$.badge = document.getElementById(el);
$.q = document.createElement('INPUT');
$.q.onchange = function() {
BBCSEARCH.runSearch(this.value);
}
$.badge.appendChild($.q);
},
runSearch : function(query) {
alert(query);
}
};
}();
window.onload = function() {
BBCSEARCH.init('bbcSearch');
};bbcSearch division is where our search box is going to live. We'll be using the same ID in all three layers, to keep things straight.
<h1>Hello, World!</h1>
» Create a request.
» Run it on a remote server.
» Wait for the results to show up.
» Format and display the results, once they appear.
init function, add this:BBCSEARCH.ping = [];
runSearch--that alert command--and substitute this:runSearch : function(query) {
var callback = 'BBCSEARCH.ping[' + n + ']';
var url = 'http://search.yahooapis.com/WebSearchService/V1/webSearch?';
url += 'appid=bbcSearch';
url += '&site=news.bbc.co.uk';
url += '&results=20';
url += '&output=json';
url += '&query=' + $.q.value;
url += '&callback=' + callback;
BBCSEARCH.runScript(url, callback);
}BBCSEARCH.ping[0], or one of its siblings. (We'll have a value for n in just a moment.) BBCSEARCH.runScript(url, callback);
},
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) {
if (document.getElementById(id)) {
var s = document.getElementById(id);
s.parentNode.removeChild(s);
}
}Consider the following:
<script type="text/javascript" src="http://foo.com/bar.js">
When the page loads, bar.js runs. Now, consider this:
var s = document.createElement('script');
s.type ='text/javascript';
s.src = 'http://foo.com/bar.js';
document.getElementsByTagName('body')[0].appendChild(s);
These two examples are functionally identical. In both cases, http://foo.com/bar.js loads and executes.
The first example works in cases where bar.js is dynamically generated at the server end. This is how most ad servers do their heavy lifting.
The second example is much more useful in cases where you don't know the exact URL you're going to load before the page renders.
There's no reason why we can't just make an infinite stream of script nodes, but deleting them when we're done is a nice, neighborly thing to do.
There are many ways to delete a DOM node, but we need to be very careful when deleting script nodes. Different browsers crash for different reasons; so far, the most reliable method I've found looks like this:
var s = '';
if (s = document.getElementById('myScript')) {
s.parentNode.removeChild(s);
}
Here we're creating empty object s and attempting to assign to it the script node myScript. If this works, we look one step up in the document for its parent node and remove it from there.
This won't work for us unless the script node we want to remove has an ID, and we know what it is. (In this case it's myScript, which we've hard-coded in ... but it could be anything.)
Because we're using an API that returns results wrapped in a callback function, however, we can sneakily use that same callback function as our script node id, and remove the function and the script node in one swoop. If we watch the demo with Firebug's HTML tab, we'll see script nodes with IDs like BBCSEARCH.ping[0] being created and destroyed.
runSearch and add this at the top:runSearch : function(query) {
var n = BBCSEARCH.ping.length;
BBCSEARCH.ping[n] = function(result) {
delete BBCSEARCH.ping[n];
BBCSEARCH.removeScript('BBCSEARCH.ping[' + n + ']');
if (result.ResultSet.totalResultsAvailable) {
alert('Results found: ' + result.ResultSet.totalResultsAvailable);
} else {
alert('Nothing found, sorry!');
}
};
var callback = 'BBCSEARCH.ping[' + n + ']';BBCSEARCH.ping. If that's not clear, it's time for another side trip, this time into creating functions.BBCSEARCH.ping runs, it deletes itself, runs removeScript to remove the script node that returned the data and then displays search results.One of JavaScript's many hidden powers is the ability to modify itself while it's running, by adding, changing, and deleting functions. Consider the following:
var dynaFunk = []; var n = dynaFunk.length; alert(n); dynaFunk[n] = function(myStuff) { alert(myStuff); } alert(dynaFunk.length); dynaFunk[0]('Ping!');
What we've created above is functionally identical to this:
dynaFunk[0] = function(myStuff) {
alert(myStuff);
}
dynaFunk[0]('Pong!');
In either case, if we try to run a function we haven't defined yet, we'll get into trouble:
dynaFunk[6]('Woot!');
Next up: Using the Function Array Index
A lovely extra bonus in creating dynamic arrays of functions is this: when called, each function will know its own array index.
var dynaFunk = [];
for (var i = 0; i < 4; i++){
dynaFunk[i] = function(myNumber) {
alert(myNumber * i);
}
}
dynaFunk[3](9);
dynaFunk[1](97);
dynaFunk[0](6);
dynaFunk[2](3);
In this case we're taking the number we're passing in and multiplying it by the function's array index, proving that it knows its own index.
This is very important when dealing with APIs that return data in JSON format. Chances are excellent your data provider won't want to send you back arbitrary text in addition to their nicely-sanitized data; that way lies extreme insecurity. In Yahoo!'s case, they compromise by allowing you to request callbacks with square brackets.
Most dynamic functions run once. In order to keep from eating up available memory, we need to know how to delete them when we're done.
We delete functions the same way we delete any other array member, with the delete function. Here's our function again:
var dynaFunk = [];
for (var i = 0; i < 4; i++){
dynaFunk[i] = function(myNumber) {
alert(myNumber * i);
delete dynaFunk[i];
}
}
dynaFunk[2](12);
dynaFunk[2](12);
What just happened?
We made four dynamic functions, dynaFunk[0] through dynaFunk[3]. When we ran dynaFunk[2](12), it ran, alerted its array index times its parameter, and then deleted itself. When we tried to run it again, it wasn't there.
We're deleting the function at the end of its own body for reasons of clarity here; source order does not matter, however, and it's much easier to read if the delete function is up top, so that's where it's going to go for our production widget.
That's it for our quick trip through dynamic functions; when you're ready, close this and we'll get back to work.
$.badge.appendChild($q), add this: $.badge.appendChild($.q);
$.r = document.createElement('DL');
$.badge.appendChild($.r);
BBCSEARCH.ping = [];runSearch and remove this: if (result.ResultSet.totalResultsAvailable) {
alert('Results found: ' + result.ResultSet.totalResultsAvailable);
} else {
alert('Nothing found, sorry!');
}runSearch, substitute what's in white for what you just removed: BBCSEARCH.removeScript('BBCSEARCH.ping[' + n + ']');
$.r.innerHTML = '';
if (result.ResultSet.Result.length) {
for (var i = 0; i < result.ResultSet.Result.length; i++) {
var dt = document.createElement('DT');
var a = document.createElement('A');
a.rel = result.ResultSet.Result[i].Title;
var t = result.ResultSet.Result[i].Title.split(' | ')
a.innerHTML = t[t.length-1];
a.href = result.ResultSet.Result[i].Url
dt.appendChild(a);
$.r.appendChild(dt);
var dd = document.createElement('DD');
dd.innerHTML = result.ResultSet.Result[i].Summary;
$.r.appendChild(dd);
}
} else {
var dt = document.createElement('DT');
dt.innerHTML = 'Nothing found, sorry!';
$.r.appendChild(dt);
}
#bbcSearch * {
color:#000; font-family:arial; font-size:13px;
}
#bbcSearch {
position:relative; background-color:#a00; border:1px solid #ffa; width:180px;
}
#bbcSearch dl{
background-color:#fff; margin:0 5px 5px 5px; position:relative; width:170px;
}
#bbcSearch dl dd {
background:#ffd; border:1px solid #000; display:none; margin:0 5px; overflow:hidden;
position:absolute; width:160px;
}
#bbcSearch dl dt {
position:relative; text-indent:15px; overflow:hidden; white-space:nowrap;
width:170px;
}
#bbcSearch input {
border:0; margin:5px; height:16px; width:170px;
}behavior.js and modify runSearch:a.href = result.ResultSet.Result[i].Url;
a.target = '_blank';
a.onmouseover = function() {
this.parentNode.nextSibling.style.display = 'block';
}
a.onmouseout = function() {
this.parentNode.nextSibling.style.display = 'none';
};
dt.appendChild(a);
$.r.appendChild(dt);
var dd = document.createElement('DD');
dd.innerHTML = result.ResultSet.Result[i].Summary;
dd.style.zIndex = i + 100;
$.r.appendChild(dd);mouseover, hide them on mouseout, and increase their Z index by 100, to make sure they show up on top of all of the other entries that might be dynamically generated afterwards. (This is also known as the "be nice to Opera" rule.)behavior.js and add this in init:$.r = document.createElement('DL');
$.r.style.display = 'none';runSearch, we need to remember to undo what we did above and show the results as a block when they come through:$.r.innerHTML = ''; $.r.style.display = 'block';
dt.appendChild(a);
if (i % 2) { dt.className = 'odd'; }
$.r.appendChild(dt);
var dd = document.createElement('DD');
var p = document.createElement('P');
p.innerHTML = result.ResultSet.Result[i].Summary;
dd.appendChild(p);
dd.style.zIndex = i + 100;odd to all odd-numbered rows, and creating a paragraph inside the DD, to hold the summary. Don't run it yet; we still need one more fix .#bbcSearch a {
cursor:pointer; text-decoration:none;
}
#bbcSearch dl dt.odd {
background-color:#eee;
}
#bbcSearch dl dd p {
margin:5px; font-size:87%;
}#bbcSearch dl dt a.bookmark {
background:transparent
url('http://l.yimg.com/us.yimg.com/i/ypicks/icons/delicious12.gif')
0 50% no-repeat;
left:2px; height:1.2em; position:absolute; top:0; width:12px;
}#bbcSearch dl dt { position:relative; overflow:hidden;
text-indent:15px; white-space:nowrap; width:170px; }<dt>, before the existing one that shows the title:var dt = document.createElement('DT');
var a = document.createElement('A');
a.className = 'bookmark';
a.title = 'save to del.icio.us';
a.onmouseup = function() {
BBCSEARCH.saveBookmark(this);
}
dt.appendChild(a);runScript:saveBookmark : function(el) {
var ns = el.nextSibling;
var url = 'http://del.icio.us/post/?';
url += 'url=' + escape(ns.href);
url += '&title=' + escape(ns.rel);
url += '&jump=no';
window.open(url,'popup','width=720px,height=420px',0);
},
runScript : function(url, id) {
rel attribute instead of title?init function, right before the line that creates the input box:$.badge = document.getElementById(el);
$.h = document.createElement('A');
$.h.className = 'home';
$.h.target = '_blank';
$.badge.appendChild($.h);
$.q = document.createElement('INPUT');runSearch, change the class name while it's running and add the URL to visit when results come back:var n = BBCSEARCH.ping.length;
$.h.className = 'loading';
BBCSEARCH.ping[n] = function(result) {
delete BBCSEARCH.ping[n];
BBCSEARCH.ping[n] = function(result) {
$.h.href = 'http://search.bbc.co.uk/cgi-bin/search/results.pl?q=' + $.q.value;$.r.appendChild(dt); } $.h.className = 'home'; };
#bbcSearch a.loading, #bbcSearch a.home {
display:block;
background:transparent
url('http://l.yimg.com/us.yimg.com/i/us/my/mw/anim_loading_sm.gif')
50% 50% no-repeat;
position:absolute;
top:5px;
left:5px;
height:16px;
width:16px;
}
#bbcSearch a.home {
background-image:url('http://news.bbc.co.uk/favicon.ico');
}#bbcSearch input {
border:0; margin:5px 5px 5px 26px; height:16px; width:149px;
}runSearch, a conditional block that only runs if we're making a new query:if ($.q.value) {
if ($.q.value != $.lastQuery) {
$.lastQuery = $.q.value;
var n = BBCSEARCH.ping.length;runSearch, close your new conditional block and tell it what to do if the user blanks out the query box after running a query: BBCSEARCH.runScript(url, callback);
}
} else {
if ($.lastQuery) {
$.r.style.display = 'none';
$.r.innerHTML = '';
$.h.href = 'http://news.bbc.co.uk';
}
}$.q.value) which is different from the last query we ran, which we'll store in $.lastQuery.$.q.onchange = function() {
BBCSEARCH.runSearch(this.value);
}BBCSEARCH.ping = []; setInterval(BBCSEARCH.runSearch, 500); },
onchange event to fire, we're going to check whether we need to run a search, every 500 milliseconds.http://kentbrewster.com/bbc-search-widgethttp://developer.yahoo.comhttp://careers.yahoo.comhttp://hackday.org