How can I separate the HTML mark-up from my JavaScript programming logic?

August 11th, 2009

Wait, hold up. Why do I even want to take the time to separate HTML from JavaScript?

To remove mental overhead.

By abstracting the form away from the function, a coder can focus on coding, rather than the repetitive process of including common elements that are needed for the design. Or maybe you want to hire a designer for your website: you don't want to expose them to the JavaScript code, you want them to cleanly and quickly modify mark-up and CSS.

What is the easiest way to do this, then?

You're using objects in JavaScript right? Probably the easiest way to do this is to have objects that know how to render themselves with a render method. Let's use cars as an example.

Now suppose we want to let users drive a car n number of miles, display for them how much gas is left, and how far they have driven. First, let's demonstrate the wrong way:

<html>
<body>
    <form>
        Drive the car <input type="text" width="10" id="myCar-miles" /> miles. 
        <input type="submit" value="Drive!" id="myCar-submit" />
    </form>
    <div id="myCar">
        <ul> <!-- Hard coding the first render of the car is BAD -->
            <li>The car is a Toyota Corolla</li>
            <li>There is 10 gallons of gas in the tank</li>
            <li>The car gets 20 miles per gallon</li>
            <li>The car has driven 0 miles</li>
        </ul>
    </div>
    <script>
    
    var type = "Toyota Corolla";
    var gas = 10;
    var mpg = 20
    var driven = 0;
    
    function driveCar(e) {
        var driveLength = parseFloat(document.getElementById("myCar-miles").value);
        
        gas = gas - ( driveLength / mpg );
        driven = driven + driveLength;
        
        // build html mark-up in the middle of the code. THIS IS BAD! WE DON'T WANT THIS!
        var html = "<ul>";
        html = html + "<li>The car is a " + type + "</li>";
        html = html + "<li>There is " + gas + " gallons of gas in the tank</li>";
        html = html + "<li>The car gets " + mpg + " miles per gallon</li>";
        html = html + "<li>The car has driven " + driven + " miles</li>";
        html = html + "</ul>";
        document.getElementById("myCar").innerHTML = html;
        
        // prevent the browser from posting data anywhere
        e.preventDefault();
    }
    
    // override default form behavior by binding event handler
    document.getElementById("-submit").onclick = driveCar;
        
    </script>
</body>
</html>

Demo

There are a couple bad things about that piece of code. First off, the HTML mark-up for a Car is described in two places. It is hard-coded in with the rest of the HTML on load, and it is being generated in the driveCar function. Twice the code means twice the opportunities for bugs, and this code does the same thing! On top of that, our code looks like PHP. No, not because of the C-style syntax, because the presentation layer is mixed in with the programming logic. Gross! Let's try again.

<html>
<body>
    <form>
        Drive the car <input type="text" width="10" id="myCar-miles" /> miles. 
        <input type="submit" value="Drive!" id="myCar-submit" />
    </form>
    <div id="myCar">
    </div>
    <script>
    // car object constructor
    function Car(type, gas, id) {
        this.type = type;
        this.gas = gas;
        this.milesPerGallon = 20;
        this.milesDriven = 0;
        this.drive = function(miles) {
            this.gas = this.gas - ( miles / this.milesPerGallon );
            this.milesDriven = this.milesDriven + miles;
            this.render();
        };
        
        // The render method!
        this.render = function() {
            var html =  ["<ul>",
                         "<li>The car is a ", this.type, "</li>",
                         "<li>There is ", this.gas, " gallons of gas in the tank</li>",
                         "<li>The car gets ", this.milesPerGallon, " miles per gallon",
                         "<li>The car has driven ", this.milesDriven, " miles</li>",
                         "</ul>"].join("");
            document.getElementById("myCar").innerHTML = html;
        };
    }

    // make a Toyota Corolla with 10 gallons of gas in the tank
    var myCar = new Car("Toyota Corolla", 10, "myCar");
    
    // much cleaner event handler
    function driveCar(e) {
        myCar.drive(parseFloat(document.getElementById("myCar-miles").value));
        e.preventDefault();
    }
    document.getElementById("myCar-submit").onclick = driveCar;
    
    // add the initial rendering
    myCar.render();
        
    </script>
</body>
</html>

Demo

This is much better than the last time we tried. Our event handlers are clean and aren't littered with mark-up (or our one driveCar event handler, in this case, but the point stands). A designer could come in and edit the recognizable HTML in the render method without worrying about foreign JavaScript syntax. We can add more methods to the Car object and not worry about code to render the car again:

// a method for refueling
myCar.prototype.fillGas = function(gallons) {
    this.gas = this.gas + gallons;
    this.render();
};

// car gets older and needs service
myCar.prototype.age = function() {
    this.milesPerGallon = this.milesPerGallon - 1;
    this.render();
}

// etc...

What's up with join("")?

It is much faster to build strings with the join method than to concatenate strings with the + operator. That's because when you use the + operator, many intermediate strings are built in memory and the concatenation occurs many times. However, joining an array is one quick operation with no strings being created in the in-between stages. Read more.

Is this the best it gets? It doesn't really seem that much better...

No. We can make it much more modular now that we aren't repeating ourselves. Let's try one last time, and see if we can't let users create their own cars.

<html>
<body>
    <form>
        <input type="submit" value="Add a new car!" id="add-new-car" />
    </form>
    <script>
    function Car(type, gas, id) {
        this.type = type;
        this.gas = gas;
        this.id = id;
        this.milesPerGallon = 20;
        this.milesDriven = 0;
        
        this.drive = function(miles) {
            this.gas = this.gas - ( miles / this.milesPerGallon );
            this.milesDriven = this.milesDriven + miles;
        };
        
        this.bindHandlers = function() {
            (function(obj) {
                document.getElementById([obj.id, "-submit"].join("")).onclick = function(e) {
                    obj.drive(parseFloat(document.getElementById([obj.id,"-miles"].join("")).value));
                    obj.render();
                    return false;
                };
            })(this);
        };
        
        this.render = function() {
            var innerHtml =  ["<form>",
                              "    <input type='text' id='", this.id, "-miles' />",
                              "    <input type='submit' value='Drive!' id='", this.id, "-submit' />",
                              "</form>",
                              "<ul>",
                              "    <li>The car is a ", this.type, "</li>",
                              "    <li>There is ", this.gas, " gallons of gas in the tank</li>",
                              "    <li>The car gets ", this.milesPerGallon, " miles per gallon",
                              "    <li>The car has driven ", this.milesDriven, " miles</li>",
                              "</ul>"].join("");
            document.getElementById(this.id).innerHTML = innerHtml;
            this.bindHandlers();
        };
        
        // initial render
        var div = document.createElement("div");
        div.id = this.id;
        document.body.appendChild(div);   
        this.render();
    }
    
    document.getElementById("add-new-car").onclick = function(e) {
        e.preventDefault();
        var type = prompt("What type of car?");
        var gas = prompt("How much gas is in it?");
        var id = prompt("What unique id should it have?");
        new Car(type, gas, id);
    };
    </script>
</body>
</html>

Demo

What makes this piece of code stand out from the rest is that we are now manipulating objects that can render themselves to the DOM cleanly, rather than manipulating DOM elements directly. This is subtle but important. We can treat these objects the same we would in any other programming language, and when the time comes, render them to the DOM and not have to worry about the proper display. This is very useful when working with ajax and JSON, and you are receiving sets of data that have the same structure. For example, search results that need to be loaded as rows to a results table.

Last but not least, there are also a variety of JavaScript templating systems that attempt to provide a solution to the problem of HTML layout for JavaScript objects and data. I have found that most of these do not properly separate the presentation layer from the programming logic. That is why I am writing my own jQuery templating plug-in called Tempest (source) to solve the problem once and for all. Note that as for now this is just a first iteration, and should not be considered production ready. I will make a new post when I am ready to announce a beta release.

« Previous Entry

Next Entry »

View Comments


Recent Entries


TryParenScript.com

On July 23rd, 2010


In response to "A JavaScript Function Guard"

On July 19th, 2010


My Notes from John Resig's "jQuery Hack Day" Talk

On July 5th, 2010


Announcing Pocco

On June 29th, 2010


Yet Another Lisp

On June 25th, 2010


Recent Happenings: All Play and No Work

On June 21st, 2010


Arguments.callee considered extraneous

On June 2nd, 2010


Javascript, "bind", and "this"

On May 20th, 2010


Class-Based Views and Django

On May 19th, 2010


Introducing Zoolander

On May 2nd, 2010


Creative Commons License

Fork me on GitHub