Introduction of Maintainable Javascript 4

Programming Practices

1. Loose Coupling of UI Layers

Loose coupling is achieved when you’re able to make changes to a single component without making changes to other components.

1.1. Keep JavaScript Out of CSS

/* Bad */
.box {
  width: expression(document.body.offsetWidth + "px");
}

The CSS expression is enclosed in the special expression() function, which accepts any JavaScript code. CSS expressions are reevaluated frequently by the browser and were considered to be bad for performance and maintenance nightmare.

1.2. Keep CSS Out of JavaScript

// Bad
element.style.color = "red";

// Bad
element.style.color = "red";
element.style.left = "10px";
element.style.top = "100px";
element.style.visibility = "visible";

// Bad
element.style.cssText = "color: red; left: 10px; top: 100px; visibility: hidden";
//Good
//CSS
.reveal {
  color: red;
  left: 10px;
  top: 100px;
  visibility: visible;
}

//Javascript
// Good - Native
element.className += " reveal";

// Good - HTML5
element.classList.add("reveal");

// Good - YUI
Y.one(element).addClass("reveal");

// Good - jQuery
$(element).addClass("reveal");

// Good - Dojo
dojo.addClass(element, "reveal");

1.3. Keep JavaScript Out of HTML

<!-- Bad -->
<button onclick="doSomething()" id="action-btn">Click Me</button>

First, the doSomething() function must be available when the button is clicked. The code for

doSomething() may be loaded from an external file or may occur later in the HTML file. So that, it’s possible for a user to click the button before the function is available and cause a JavaScript error. The second problem is a maintenance issue. What happens if you want to change the name of doSomething()? What happens if the button should now call a different function when clicked? In both cases, you’re making changes to both the JavaScript and the HTML; this is the very essence of tightly coupled code.

Most—if not all—of your JavaScript should be contained in external files and included on the page via a element. The on attributes should not be used for attaching event handlers in HTML. Instead, use JavaScript methods for adding event handlers once the external script has been loaded. For DOM Level 2–compliant browsers, you can achieve the same behavior in the previous example by using this code:

//Good
function doSomething() {
  // code
}
var btn = document.getElementById("action-btn");
btn.addEventListener("click", doSomething, false);

The advantage of this approach is that the function doSomething() is defined in the same file as the code that attatches the event handler. If the function name needs to change, there is just one file that needs editing; if the button should do something else when clicked, there is still just one place to go to make that change. Another way of embedding JavaScript in HTML is to use the element with inline code:

<!-- Bad -->
  doSomething();

It’s best to keep all JavaScript in external files and to keep inline JavaScript code out of your HTML. Part of the reason for this approach is to aid in debugging. When a Java-Script error occurs, your first inclination is to start digging through your JavaScript files to find the issue. If the JavaScript is located in the HTML, that’s a workflow interruption. You first have to determine whether the JavaScript is in the JavaScript files (which it should be) or in the HTML. Only then can you start debugging.

1.4. Keep HTML Out of JavaScript

HTML frequently ends up in JavaScript as a consequence of using the innerHTML property, as in:

// Bad
var div = document.getElementById("my-div");
div.innerHTML = "<h3>Error</h3><p>Invalid e-mail address.</p>";

Embedding HTML strings inside your JavaScript is a bad practice for a number of reasons. First, it complicates tracking down text and structural issues. The second problem with this approach is maintainability. If you need to change text or markup, you want to be able to go to one place: the place where you manage HTML.

1.4.1. Alternative #1: Load from the Server

The first is to keep the templates remote and use an XMLHttpRequest object to retrieve additional markup. This approach is more convenient for single-page applications than for multiple-page applications. For instance, clicking on a link that should bring up a new dialog box might look like this:

function loadDialog(name, oncomplete) {

  var xhr = new XMLHttpRequest();
  xhr.open("get", "/js/dialog/" + name, true);

  xhr.onreadystatechange = function() {

    if (xhr.readyState == 4 && xhr.status == 200) {

      var div = document.getElementById("dlg-holder");
      div.innerHTML = xhr.responseText;
      oncomplete();

    } else {
      // handle error
    }
  };

  xhr.send(null);
}

So instead of embedding the HTML string in the JavaScript, JavaScript is used to request the string from the server, which allows the markup to be rendered in whatever way is most appropriate before being injected into the page. JavaScript libraries make this process a bit easier by allowing you to load remote markup directly into a DOM element. jQuery has simple APIs for accomplishing this:

// jQuery
function loadDialog(name, oncomplete) {
  $("#dlg-holder").load("/js/dialog/" + name, oncomplete);
}

1.4.2. Alternative #2: Simple Client-Side Templates

Client-side templates are markup pieces with slots that must be filled by JavaScript in order to be complete. For example, a template to add an item to a list might look like this:

<li><a href="%s">%s</a></li>

This template has

%s placeholders for the area in which text should be inserted (this is the same format as sprintf() from C). The intent is for JavaScript to replace these placeholders with real data before injecting the result into the DOM. Here’s the function to use with it:


function sprintf(text) {
  var i=1, args=arguments;
  return text.replace(/%s/g, function() {
    return (i < args.length) ? args[i++] : "";
  });
}

// usage
var result = sprintf(templateText, "/item/4", "Fourth item");


function addItem(url, text) {
  var mylist        = document.getElementById("mylist"),
      templateText      = mylist.firstChild.nodeValue,
      result            = sprintf(template, url, text);

  div.innerHTML = result;
  mylist.insertAdjacentHTML("beforeend", result);
}

// usage
addItem("/item/4", "Fourth item");

The second way of embedding templates into an HTML page is by using a element with a custom type property. Browsers assume that code in elements are JavaScript by default, but you can tell the browser that it is not JavaScript be specifying a type that it won’t understand. For example:


  <li><a href="%s">%s</a></li>

You can then retrieve the template text by using the text property of the element:


var script      = document.getElementById("list-item"),
    templateText  = script.text;

The addItem() function would then change to:


function addItem(url, text) {
  var mylist        = document.getElementById("mylist"),
      script        = document.getElementById("list-item"),
      templateText  = script.text,
      result        = sprintf(template, url, text),
      div           = document.createElement("div");
  div.innerHTML = result.replace(/^\s*/, "");
  list.appendChild(div.firstChild);
}

// usage
addItem("/item/4", "Fourth item");

1.4.3. Alternative #3: Complex Client-Side Templates

The templating format used in the previous section is quite simplistic and doesn’t do any escaping. For more robust templating, you may want to consider a solution such as Handlebars. Handlebars is a complete client-side templating system designed to work with JavaScript in the browser. Handlebars templates use double braces to indicate placeholders. Here’s a Handlebars version of the template from the previous section:


<li><a href="{{url}}">{{text}}</a></li>

The placeholders in Handlebars templates are named so that they correspond to named values in JavaScript. Handlebars suggests embedding the template in an HTML page using a element with a type of text/x-handlebars-template:

  <li><a href="{{url}}">{{text}}</a></li>

To use the template, you first must include the Handlebars JavaScript library on your page, which creates a global variable called Handlebars that is used to compile the template text into a function:

var script        = document.getElementById("list-item"),
    templateText  = script.text,
    template      = Handlebars.compile(script.text);

The variable template now contains a function that, when executed, returns a formatted string. All you need to do is pass in an object containing the properties name and url:

var result = template({
    text: "Fourth item",
    url: "/item/4"
});
function addItem(url, text) {
  var mylist      = document.getElementById("mylist"),
      script        = document.getElementById("list-item"),
      templateText  = script.text,
      template      = Handlebars.compile(script.text),
      div           = document.createElement("div"),
      result;

  result = template({
    text: text,
    url: url
  });

  div.innerHTML = result;
  list.appendChild(div.firstChild);
}

// usage
addItem("/item/4", "Fourth item");

Suppose you want to render an entire list rather than an item, but you want to do that only if there are actually items to render. You can create a Handlebars template that looks like this:

{{#if items}}
<ul>
  {{#each items}}
  <li><a href="{{url}}">{{text}}</a></li>
  {{/each}}
</ul>
{{/if}

The

{{#if}} block helper prevents the enclose markup from being rendered unless the items array has at least one item. The {{#each}} block helper then iterates over each item in the array. So you compile the template into a function and then pass in an object with an items property, as in the following example:


// return an empty string
var result = template({
  items: []
});
// return HTML for a list with two items
var result = template({
  items: [
    {
      text: "First item",
      url: "/item/1"
    },
    {
      text: "Second item",
      url: "/item/2"
    }
  ]
});

Handlebars has other block helpers as well—all designed to bring powerful templating to JavaScript.