Soon I’m meeting with a couple of fellow web geeks and they’ve asked me to provide a quick talk about modern JavaScript including that whole pesky AJAX thing. In return they’ll be giving me some help with the .net thing. Anyway, I thought I’d better write something down so I don’t look like a complete muppet, so here’s what I intend to say.
The old way – onclick and javascript: hell
Let’s face it, separation is a good thing. And just as all good web people use HTML to describe the page content, and a separate CSS file(s) to style that content, so a separate JavaScript file(s) to describe the behaviour of the page should be used. That means no more ‘onclick=’ attributes or ‘a href=”javascript:”‘ psuedo-links. Those things were OK for a time, but people who know better now use different methods to apply certain JavaScript behaviours to elements using element type names, IDs and classes.
It’s better for lots of reasons: reusability of code, readibility of HTML, compatibility with a wider array of devices, accessibility to a wider range of users, separateion of content and behaviour, and quite a few more. It’s simply the Right Thing To Do.
The new way – listen up!
So how do we make that funky JavaScript function happen when someone clicks the link? An example would be useful. Let’s say we want to change an image when someone clicks a link, like in a simple image gallery. Here’s our link and image HTML:
And here it is, in all it’s glory. What we want to do is listen for when a user clicks the link, then hijack the request and load the right image in using JavaScript. Eh? Listen? What are you going on about?
Listeners are the unobtrusive way to assign behaviours to events performed on elements. An element is any HTML element – a link, a button, an image, the page body itself etc. An event is something that can happen to an element – a link can be clicked, an button pressed, a the page body loaded. And a behaviour is something that happens when that event is fired – a popup window appearing, changing the text size, checking the validity of a forms fields.
It’s pretty simple, really. Rather than shove a link to the JavaScript function in the actual link or button (as described above) we simply set a load of listeners that sit there patiently until something happens. They then jump up and say “Hey! Someone clicked that link! Now I have to do something!”. That’s separation of behaviour and content in action.
So how do we actually set listeners? Here’s a simple one that listens for our link with the id ‘image2link’ being clicked:
// the addEvent and removeEvent functions are from http://ejohn.org/projects/flexible-javascript-events/
// This function adds a listener for a particular event
// (from http://ejohn.org/projects/flexible-javascript-events/)
function addEvent( obj, type, fn ) {
if ( obj.attachEvent ) {
obj['e'+type+fn] = fn;
obj[type+fn] = function(){obj['e'+type+fn]( window.event );}
obj.attachEvent( 'on'+type, obj[type+fn] );
} else
obj.addEventListener( type, fn, false );
}
function removeEvent( obj, type, fn ) {
if ( obj.detachEvent ) {
obj.detachEvent( 'on'+type, obj[type+fn] );
obj[type+fn] = null;
} else
obj.removeEventListener( type, fn, false );
}
// And here's where it happens
// this line adds a listener to listen for then the page loads and then runs the getImageLink function
addEvent(window,'load',getImageLink,false);
// Here's where we get the image link from it's ID
function getImageLink(){
// This line gets a reference to the element with the ID 'image2link'
var thisLink = document.getElementById('image2link');
// this line adds the listener to listen for then that element is clicked, then runs the 'showImage' function
addEvent(thisLink,'click',showImage,false);
thisLink.onclick = cancelClick;
}
// this function shows the image
function showImage() {
// get a reference to the element with the ID 'image'
var imagePlaceholder = document.getElementById('image');
// set the new src attribute
imagePlaceholder.setAttribute('src','image_2.jpg');
// set the new alt attribute
imagePlaceholder.setAttribute('alt','Image 2');
// stop anything else from happening
cancelClick();
}
// This function stops the default click from happening
function cancelClick() {
return false;
}
Make sense? That’s pretty much it, to be honest. I’ve added this JavaScript into our simple page and you can see the results here. Clicky linky! I love it long time. The problem is it’s long and complicated and way beyond the means of any but the most geeky of coders. This is where frameworks come in, which I’ll talk about in a little while.
Graceful degradation and progressive enhancement
Using this method you can set up as many functions as you like, and set listeners to any event on any element to start them. And perhaps the best bit about this is that because you’re not messing about with the actual href of the link at all, it can point to the actual image you want to load so that if a user doesn’t support JavaScript (or has it disabled) they can still follow the link as normal and see the image.
That is known as graceful degradation, or progressive enhancement. What it means in this example is that if a user can’t or won’t support JavaScript they at least get something. It’s another way for websites to be accessible and, again, is simply the Right Thing To Do.
Manipulating the DOM
In that example we changed the src and alt attributes of an image element. That’s an example of manipulating the DOM. The DOM, or Document Object Model, is the current rendering of a web page – the treeview of nested element, if you like. For example, the html element has a body element inside it. The body element has li and p elements inside it. We can walk through the DOM just like we’re walking through an XML document. In fact, a properly-written XHTML is an XML document.
So what else can we do with JavaScript to change our web page? Here’s a partial list of functions that are part of the core JavaScript language. Try to guess what they do:
- getElementById
- getElementsByName
- getElementsByTagName
- createAttribute
- setAttribute
- removeAttribute
- createTextNode
- appendChild
- removeChild
- cloneNode
A more complete list can be seen at JavaScriptKit.com.
Now, this is where it gets a bit complicated, and where opinion on modern JavaScript best practices hits a fork in the road. I’ll give you an example. Let’s say that we have a list of options, like this:
- Item 1
- Item 2
- Item 3
And here it is, looking lovely. We want to add another item to the bottom of the list, cleverly called ‘Item 4’. One way to do this could be:
// create the new list item
var newitem = document.createElement('li');
// set the text for this new list item
var newtext = document.createTextNode("Item 4")
// add the new text to the new list item
newitem.appendChild(newtext);
// get a reference to the list and add the new item with it's text to the list
// this line demonstrates the ability to string together object methods and properties
// this method can save a lot of typing and bandwidth
document.getElementById('list').appendChild(newitem);
That should all make sense, and while it is perfect from a code point of view (well, notwithstanding my fumbling fingers ;0) If you want to see this in action you can’t do better thank to click here. This page adds a new item to the list 3 seconds after the page is loaded. I won’t pretend it’s quick and easy. Can you imagine adding a complex block of HTML to a page this way? Nasty, I think you’ll agree. But that’s the way the DOM cookie has crumbled, and we’re stuck with it.
Or are we? There is another non-standard method which is helpfully supported by all the major browsers and is used extensively by thousands of modern sites. It’s called ‘innerHTML’ and I bet you can guess what it does from the name. That’s right, it can get and set the entire HTML code inside a referenced element. Here’s an example:
// here's the HTML
This is my text!
// get a reference to our paragraph and get the innerHTML property
var text = document.getElementById('innertest').innerHTML;
// the variable 'text' now contains 'This is my text!'
// set some new text
document.getElementById('innertest').innerHTML = "No! It's my text!";
// the HTML will now be
No! It's my text!
Now, is that easier or what? You can see it working here. The innerHTML property can contain any HTML you want – links, images, tables, forms, the lot. So it’s a really easy way to get and set large amounts of HTML. And there’s even some evidence to say it’s faster than a totally DOM-only method for writing HTML.
Of course, not everyone agrees that this is a good method, but it works, is well supported, and is likely to be included in the official DOM specification eventually.
AJAX
So now we can change huge chunks of a web page using JavaScript easily, surely there’s some really cool way we could use this? Why, yes there is, and it’s called AJAX. That stands for Asynchronous JavaScript and XML and was coined by the clever chaps at Adaptive Path to describe a collection of technologies working together to do a simple thing: send messages to and from the web server without refreshing the page.
It might not sound much, but that technology (which was originally developed by Microsoft several years ago) is the breakthrough that web developers needed to get on an (almost) level footing with desktop application developers. For too many years we’ve provided cool online apps that look great, do great stuff, but every time we wanted to ‘do’ something – change a password, request a different ordering for a data table, send a form – we’ve had to sit back and wait for the whole damn page to be sent, and then another whole damn page to be returned. Only to tell us that ‘Sorry, your password is not valid. It must contain letters, numbers and a Klingon war-cry’.
No more! We’re even. We’ve got it all: the looks, the worldwide reach, the sexiness, the coolness and now the responsiveness. AJAX has made this possible, and it’s all down to a little set of functions like this:
// these are my AJAX functions from my projectGenie application
// function to send some data and receive the response
function sendReceive(divID,scriptPath,variables,divClass)
{
if (document.getElementById){
var thedate = new Date;
var datestamp = escape(thedate.getTime());
url = scriptPath + '?datestamp=' + datestamp + '&' + variables + '';
if (window.XMLHttpRequest)
{
request = new XMLHttpRequest();
}
else if (window.ActiveXObject)
{
request = new ActiveXObject("Microsoft.XMLHTTP");
}
request.onreadystatechange = processReqChange;
request.open("GET", url, false);
request.send(null);
contentText = request.responseText;
updateDiv(divID,contentText,divClass);
}
}
// function to process the request status change
function processReqChange()
{
if (request.readyState == 4){
if (request.status == 200){
} else {
if (request.status == 404) {
alert("There was a problem retrieving the data, the page was not found.");
} else if (request.status == 500) {
alert("There was a problem retrieving the data, there was a server error.");
} else {
alert("There was a general problem and the data could not be loaded.");
}
}
}
}
// function to update an element (notice the innerhtml?)
function updateDiv(divID,textContent,divClass)
{
if (document.getElementById){
var element = document.getElementById(divID);
element.innerHTML = textContent;
element.className = divClass;
addListeners();
}
}
There’s not really much there, so I won’t explain it. Oh, OK then, I will :0)
// function to send some data and receive the response
// this takes the following parameters:
// divID = the element that should receive the response
// scriptPath = the path (relative or absolute) of the server-side processing page
// variables = the variables to send to the processing page
// divClass = the class to set the receiving element when the response has been received
// (this is useful for unhiding things, highlighting things etc)
function sendReceive(divID,scriptPath,variables,divClass)
{
// check if the browser supports the document.getElementById method
// if not the function is not run
if (document.getElementById){
// set a new datestamp, this ensures we don't see cached versions of the processing page
var thedate = new Date;
var datestamp = escape(thedate.getTime());
// set up the URL we will be sending to and receiving from
url = scriptPath + '?datestamp=' + datestamp + '&' + variables + '';
// check if the browser supports the window.XMLHttpRequest method
// if so use this method
if (window.XMLHttpRequest)
{
// create the new request
request = new XMLHttpRequest();
}
// otherwise check if the browser supports the window.ActiveXObject method
// if so use this method as we're in IE
else if (window.ActiveXObject)
{
request = new ActiveXObject("Microsoft.XMLHTTP");
}
// set the function to run whenever the state of the processing page changes
request.onreadystatechange = processReqChange;
// open the connection to the processing page using GET
// the 'false' means this is asynchronous, so the browser doesn't lock up while we process the data
request.open("GET", url, false);
// send the data - this is null because all the variables we are sending are by GET (querystring)
request.send(null);
// get the response of the processing page
contentText = request.responseText;
// run the updateDiv function to write the response to the page
updateDiv(divID,contentText,divClass);
}
}
// function to process the request status change
function processReqChange()
{
// these are all pretty obvious, as they are standard HTTP status codes
if (request.readyState == 4){
if (request.status == 200){
} else {
if (request.status == 404) {
alert("There was a problem retrieving the data, the page was not found.");
} else if (request.status == 500) {
alert("There was a problem retrieving the data, there was a server error.");
} else {
alert("There was a general problem and the data could not be loaded.");
}
}
}
}
// function to update an element (notice the innerhtml?)
function updateDiv(divID,textContent,divClass)
{
// check if the browser supports the document.getElementById method
// if not the function is not run
if (document.getElementById){
// get a reference to the element
var element = document.getElementById(divID);
// set the innerHTML of the element to be the textContent
element.innerHTML = textContent;
// change the class name of the element (this could be done as element.setAttribute('class', divClass);)
element.className = divClass;
// now we reset the listeners so we can catch the next click
startListeners();
}
}
// and to kick all this off we do this:
sendReceive('result','add.php','a=1&b=2','success');
// of course, this line could be included in a function that has a listener attached to it
The ‘add.php’ file Would then take the values for the variables ‘a’ and ‘b’ and add them together (giving us 3 in total, in case you didn’t know) and then send the result back to the requesting page to be displayed in the ‘result’ element. While that example is pretty simple, there really is everything in there to build advanced web applications with the responsiveness of desktop apps. Depending on connection speed, responsiveness of your web server, of course. There is loads more help on this on the web, a lot of it very very complicated, but it all boils down to that principle.
There’s a really simple example here that just loads a line of text from an HTML file.
Object-oriented JavaScript
I want to touch on object-oriented JavaScript, as some of you might not believe that JavaScript is a powerful object-oriented language. Well it is, in fact everything in JavaScript is an object – functions, the lot. Perhaps the best way to demonstrate this is to use an example from The Wild.
JavaScript frameworks – Prototype
A framework, as you may know, is ‘defined support structure in which another software project can be organized and developed‘ – basically a way to stop you from having to reinvent the wheel every time you create some code. The Prototype JavaScript framework provides an easy way to use all of the functions I’ve touched on above – and a huge boatload more – in just a few simple lines of code.
I can’t possibly do justice to Prototype’s fantastic feature set, for that you need to consult the Prototype documentation. All you need to know about AJAX with Prototype is fantastically demonstrated at 24ways.org, in an article written by Drew McLennan. Is that easy or what?
An example – the ODE application
A while ago I wrote a little PHP/JavaScript application called ODE – Online Development Environment. It does just what it says on the tin, allowing you to browse a folder structure on a web server, load multiple text files, edit and save them. It has quite a few nice features such as showing an error if you’re about to close a file that you’ve edited without saving, and runs completely from one HTML (well, PHP) file with no page refreshes.
One thought on “Modern JavaScript – a primer…”
Comments are closed.