Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

MiniRacer - Extending W3C DOM Elements using JavaScript (IE)

0.00/5 (No votes)
16 Feb 2005 1  
A JavaScript table-based driving game to demonstrate how to extend W3C DOM Elements to add your own functionality.

MiniRacer Screenshot

Introduction

The code behind this short JavaScript game is aimed at the intermediate level and is intended to demonstrate the useful techniques of:

  • programming JavaScript in an object-oriented fashion
  • applying that technique to, in effect, extend W3C DOM HTMLElements (table, table row, and table cell elements on this occasion)
  • thereby creating instances of our 'classes' (strictly speaking, functions) which are also instances of HTMLElement, and which therefore can be added directly to our web-page whilst still exhibiting our added functionality.

As such, this game is aimed at Internet Explorer 6 (& compatible) browsers. For simplicity (and an easy life), I have not stepped into the muddy waters of browser compatibility, but one big reason for supporting the W3C DOM is the hope that in time code across browsers will become increasingly standardized.

This article starts with an introduction to the game and the general principles of inheritance (prototyping in JavaScript). For the specific details regarding extending HTMLElements, take a jump down towards the end of this article.

I should make clear that whilst, hopefully, this game may be a good example of a few useful techniques, they are not techniques of my own invention. Some are well known; others less so. The References section at the bottom of this article directs the reader's attention to the four sources which were the most useful to me in writing this game.

The Game

The game is a simple driving game that I first met on my Amstrad CPC464, quite a few years ago. It utilizes a table to represent the race-track, with table cells making up the squares of the race-track. Four buttons allow you to steer the car in the four possible directions. The aim is to drive to the finish (indicated in red) without crashing into the walls.

You can also design a new track to race on, change the design of the current track, and even save a designed track by adding the URL to your Favorites (the track is stored in the querystring).

You can play the game online, or download the ZIP file which contains all of the code.

The Design

It would be possible, of course, to program games like this using a number of global functions that test various conditions and write HTML to the document using document.write and modifications to the innerHTML property of elements.

However, the benefits of a more object-oriented approach are substantial. In particular:

  • Debugging in JavaScript is notoriously messy. Large numbers of global functions and spurious splurging of raw HTML into the document are a recipe for messy code - difficult to understand, awful to debug, and a nightmare to maintain.
  • In contrast, classes (represented by JavaScript functions) are easy (or easier) to unit test, and serve to keep the code tidy and compartmentalized.
  • With the increasing popularity of the W3C DOM (supported to a decent extent in IE 6), we can build on these elements to create objects that share their base functionality but are extended to fill the needs of our program. In making use of these, I feel we support the essential work W3C is doing to standardize the document object model across browsers.

Because JavaScript does not force any kind of OO structure upon your code and, in addition, is a loosely typed language, an application of sheer discipline on your part is required to get started on this technique.

The basic principles shown below need some careful study to master, but, once mastered, offer big rewards. Although it may look complicated at first, it is also easy to step back and admire the basic simplicity of an object-oriented solution. The UML diagram for this solution is shown below.

UML Diagram

UML Diagram

The advantages of 'inheriting' from W3C DOM elements are two-fold:

  • Firstly, we can make use of their (helpful) methods. For instance, the insertRow method of the HTMLTableElement handles all of the pain in adding a new empty row to our table at any index position.
  • Secondly, we can add the instances of our objects directly to the web-page. Because our object is also an instance of HTMLElement, it can be added to the web-page using the appendChild or replaceChild methods of any other HTMLElement.

Design Principles

We can implement this approach using the following general principles:

  1. As in any object-oriented approach, decide on the classes needed for your program. On this occasion, I've used:
    • RaceTrack - to represent the track on which the game takes place.
    • RaceRow - to represent a row of cells on the racetrack.
    • RaceCell - to represent an individual cell on the racetrack.
    • Car - to handle the current position of the car, and to deal with move updates, steering, and checking for crashes and finish points.
    • QueryStringParser - to parse the querystring passed to the HTML pages.
    • SpeedSelector - a wrap around the select element to set the car's speed.
  2. Create a js file for each of your classes, e.g., RaceTrack.js. Each class js file is set out in the following fashion:
    • First comes the constructor, which is given the same name as the class itself, e.g.: RaceTrack.
      function RaceTrack()
      {
        this.init();
      }
    • The constructor simply calls an initialization function:
      this.init();

      The init function does all the set-up for the object which usually is simply a matter of initializing member variables, e.g., this._rows = 0;.

      [There is a reason for calling an init function rather than putting the init code into the constructor itself. Without going into detail, it is to ensure that if you later sub-class this function, then each object instance of the sub-class can still obtain its own member variables as opposed to sharing the variables between all instances of the function.]

    • Following the constructor is a series of prototype statements that attach member functions to your class and give the function name of their implementation, e.g.:
      RaceTrack.prototype.init = RaceTrack_init;
      RaceTrack.prototype.clearTrack = RaceTrack_clearTrack;

      Notice that there are no parameter brackets in any of these assignments!!

    • Following these is the implementation code for the member functions� one for each prototype statement, e.g.:
      function RaceTrack_init()
      {
        this._rows = 0;
        this._cols = 0;
      }
      
      function RaceTrack_clearTrack()
      {
        �
      }
      
      �
  3. Create another js file for each of your web-pages to contain the static code for that page. For instance, in this project, I have two web-pages: miniRacer.htm and designTrack.htm, so I create two matching js files: miniRacer.js and designTrack.js which contain the static functions for the page.

    As an example, miniRacer.js contains the following global functions and variables:

    var tbl;    // stores the race track table
    
    var car;    // stores as instance of the Car class
    
    
    // The default track to race on
    
    var DefaultRaceTrack = "wwwwwwwwwwwwrrr...";
    var DefaultRowLength = 11;
    
    function setup()
    {
      // Load racetrack in from querystring,
    
      // using default track if not available
    
      var qs = new QuerystringParser();
      var track = qs.getRaceTrack(DefaultRaceTrack);
      var rowLength = qs.getRowLength(DefaultRowLength);
    
      tbl = document.createElement("RaceTrack");
      tbl.load(track, rowLength, DISABLE_CELL_CLICK_HANDLER);
    
      // add racetrack to webpage
    
      var RaceTrackContainer = document.getElementById("RaceTrackContainer");
      RaceTrackContainer.replaceChild(tbl, RaceTrackContainer.firstChild);
    
      // create a new instance of our racecar
    
      car = new Car(tbl, "car", new SpeedSelector("speedSelect"));
    }
    
    function designTrack()
    {
      // called when "Design This Track" link
    
      // is clicked...redirects to designTrack.htm
    
      car.reset();
      window.location = "designTrack.htm?track=" + 
         tbl.serialise() + "&rowLength=" + tbl.getRowLength();
    }

    Notice how little code there is in the global functions. In an ideal situation, these do little more than creating the needed instances for the classes and then hooking them up as needed. Essentially, all of the code has been delegated into the most appropriate method of the most appropriate class. Each method can then be fairly easily unit tested when things go wrong, making for much easier testing and debugging.

  4. Create a constants.js file to store code constants. E.g.:
    var ENABLE_CELL_CLICK_HANDLER = true;
    var DISABLE_CELL_CLICK_HANDLER = false;
    var IGNORE_QUERYSTRING = true;

    [Alternatively, you can attach the constants to their closest connected functions but I haven't gone that far.]

  5. Finally then, we need to create DOM_override.js file to implement the override needed in the document.createElement method provided by the W3C DOM. The need for this and an explanation is given below.

Inheriting from DOM Elements

Here is the small catch. Usually, to make use of JavaScript's function-based equivalent to inheritance, we simply set the prototype object of a function to be the function that we wish to inherit from:

Dog.prototype = new Animal();

However, in Internet Explorer, DOM elements are not implemented using full JavaScript functions. This leads to error messages when using the prototype technique.

All is not lost though. Instead, we do two things in DOM_override.js:

  • We replace the default implementation of the document.createElement function with our own implementation.
    // store a reference to the original document.createElement
    
    var __IEcreateElement = document.createElement;
    
    // and now re-define the original
    
    document.createElement = function (tagName) {
    
      if(tagName=='RaceTrack')
      {
        return document.applyInherit(__IEcreateElement("table"), new RaceTrack());
      } else
      {
        return __IEcreateElement(tagName);
      }
    }

    Our replacement function usually delegates to the original. However, if it spots one of our class names (RaceTrack), it calls applyInherit, passing in a new instance of the correct HTMLElement and a new instance of our class (RaceTrack).

  • So what does applyInherit do? Well, we define this below:
    document.applyInherit = function(original, interface)
    {
      for (method in interface)
        original[method] = interface[method];
    
      return original;
    }

It simply makes use of the fact that all properties and methods of a JavaScript function are available through the index notation [].

We iterate through all the properties and methods of our class, copying them to the instance of the HTMLElement. We then return the instance of the HTMLElement which is now equipped with all the properties and methods of our class. This achieves the inheritance, albeit in a rather unorthodox and manual way.

We can now create instances of our classes. We create an instance of our top level class RaceTrack using, e.g.:

var tbl = document.createElement("RaceTrack");

To enable the creation of instances of RaceRow and RaceCell, I add factory methods to RaceTrack and RaceRow respectively:

function RaceTrack_insertRaceRow()
{
  return document.applyInherit(this.insertRow(), new RaceRow());
}

function RaceRow_insertRaceCell()
{
  return document.applyInherit(this.insertCell(), new RaceCell());
}

this.insertRow and this.insertCell are defined for us within HTMLTableElement and HTMLRowElement respectively.

These neat functions now enable us to add the RaceCells to the track, using the following example code:

for(var i=0;i<rows;i++)
{
  var row = this.insertRaceRow();
  for(var j=0; j<cols;j++)
  {
    var cell = row.insertRaceCell();
    cell.setX(j);
    cell.setY(i);
  }
}

The instance of RaceTrack (referenced below by tbl) can be plugged into the web-page using:

var RaceTrackContainer = document.getElementById("RaceTrackContainer");
RaceTrackContainer.replaceChild(tbl, RaceTrackContainer.firstChild);

For this to work, we need an element in the web-page with an id of "RaceTrackContainer".

<td id="RaceTrackContainer">
  Racetrack goes here
</td>

References

I hope this article has been of some help to you. Some references are given below, all of which have been helpful in constructing this article.

  1. Emulating Prototyping of DOM Objects in Internet Explorer.

    This web-page covers extending the DOM elements with IE and JavaScript. It also shows a neat trick for adding functionality using Behaviors. The technique shown here is basically the idea used in this game.

  2. JavaScript Objects [Nakhimovsky & Myers, Wrox Press, ISBN 1861001894]

    A clever book covering an OOP approach to JavaScript. I've used some of the ideas from this book in this game.

  3. Access the Querystring client-side.

    Using a QueryString object to parse the QueryString. My simple QuerystringParser class comes basically from here with some negligible modifications.

  4. JavaScript: The Definitive Guide [David Flanagon, ISBN 0-596-00048-0, 4th Edition]

    An excellent, general JavaScript Reference. This book also includes a useful reference to the W3C DOM.

History

  • Feb 05 - Article published.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here