Writing reusable JavaScript

One of the biggest hurdles in going from JavaScript as a trivial gimmick for making divs change color to a powerful language for building real applications is its single-namespace weakness. It’s easy to accidentally cause variable names to collide when you start combining scripts, and there’s no native library or module system to alleviate this. For example, jQuery, prototype.js, and MooTools all want to use “$” as a shortcut variable, which has caused me problems before when combining software written by other people.

It’s easy to write a bunch of JavaScript that drops variables all over the global namespace, but often, you want to reuse some JavaScript component in multiple pages. Let’s look at a hypothetical example:

We want to make a JS widget called PinpointLocation that augments latitude and longitude input fields with a live map showing where in the world that particular coordinate is.

Amazing drawing skills aside, let’s look at the most naive way of implementing it:

Most naive way

<!-- Somewhere in the middle of map.html ... -->
<input type="text" id="latitudeInput" />
<input type="text" id="longitudeInput" />
<div id="pinpointMap"></div>
<script>
myMap = ThirdPartyMap.initialize($("#pinpointMap").get(0))
myMarker = new ThirdPartyMapMarker()
myMap.add(myMarker)
updateMap = function() {
  myMarker.setLocation(
    $("#latitudeInput").val(),
    $("#longitudeInput").val())
}
$("#latitudeInput").change(updateMap)
$("#longitudeInput").change(updateMap)
</script>

The most obvious problem is that this is not easily reusable. We’d have to copy and paste this code on every page that we want this widget to appear, violating DRY (Don’t Repeat Yourself) and definitely causing problems later on. What if we needed to fix a bug?

Factoring the JavaScript out of the page

If you’re ever going to reuse JavaScript, the first step is to put the code in a different file. Let’s try that:

Our web page:

<script src="pinpointlocation.js"></script>
<!-- Somewhere in the middle of map.html ... -->
<input type="text" id="latitudeInput" />
<input type="text" id="longitudeInput" />
<div id="pinpointMap"></div>

Our JavaScript file “pinpointlocation.js”:

myMap = ThirdPartyMap.initialize($("#pinpointMap").get(0))
myMarker = new ThirdPartyMapMarker()
myMap.add(myMarker)
updateMap = function() {
  myMarker.setLocation(
    $("#latitudeInput").val(),
    $("#longitudeInput").val())
}
$("#latitudeInput").change(updateMap)
$("#longitudeInput").change(updateMap)

Well, it’s one big step but we’re making assumptions about the page that we’re embedding this in:

  1. jQuery is called $

    What if we’re embedding on a page that has prototype.js? It’s enough to document that jQuery is a requirement, but the $ variable isn’t necessarily bound to jQuery. We could get around this by replacing all instances of “$” with “jQuery” in our code.

  2. There are no other uses of variables named “myMap”, “updateMap”, etc.

    We polluted the global namespace with variables like “updateMap” and “myMap”. If any other script happens to use those names, something will break.

  3. The input fields are called “latitudeInput” and “longitudeInput”.

    This could become a huge pain for whoever needs to reuse this code.

Scope!

We can cure the jQuery/$ problem and the global namespaces problem easily:

function($) {
  var myMap = ThirdPartyMap.initialize($("#pinpointMap").get(0))
  var myMarker = new ThirdPartyMapMarker()
  myMap.add(myMarker)
  var updateMap = function() {
    myMarker.setLocation(
      $("#latitudeInput").val(),
      $("#longitudeInput").val())
  }
  $("#latitudeInput").change(updateMap)
  $("#longitudeInput").change(updateMap)
}(jQuery)

Now, $ gets bound to jQuery only within the scope of our code here, because the wrapping function is immediately run with jQuery as an argument. In addition, we’ve added the “var” keyword to our inner variable declarations, making them inaccessible outside the function.

Still, we haven’t fixed assumption #3: that “latitudeInput” and “longitudeInput” are really the inputs we want to bind to.

Have the embedding page call our script instead

Instead of enabling the widget just by embedding it, let’s have the embedding page explicitly invoke our script. This way, the embedding page chooses which input fields to bind, and when to do so.

Our modified map.html:

<script src="pinpointlocation.js"></script>
<!-- Somewhere in the middle of map.html ... -->
<input type="text" id="latitudeInput" />
<input type="text" id="longitudeInput" />
<div id="pinpointMap"></div>
<script>
  PinpointLocation(
    $("#latitudeInput").get(0),
    $("#longitudeInput").get(0),
    $("#pinpointMap").get(0))
</script>

Our new, reusable JavaScript, with documentation!:

/*
 * PinpointLocation takes a latitude <input> field,
 * a longitude <input>, and a <div>. It turns the
 * <div> into a map marking the location of the
 * coordinate specified by the latitude and longitude
 * fields whenever they are changed.
 *
 * PinpointLocation *requires* jQuery and ThirdPartyMap.
 */
PinpointLocation = function($) {
  return function(latInput, lonInput, mapDiv) {
    var lat = $(latInput)
    var lon = $(lonInput)
    var myMap = ThirdPartyMap.initialize(mapDiv)
    var myMarker = new ThirdPartyMapMarker()
    myMap.add(myMarker)
    var updateMap = function() {
      myMarker.setLocation(
        lat.val(),
        lon.val())
    }
    lat.change(updateMap)
    lon.change(updateMap)
  }
}(jQuery)

Note that instead of passing in jQuery objects, the web page is passing in plain DOM elements. We don’t want to count on the implementor to pass us jQuery objects, because they might not use jQuery. We wrap those plain DOM elements (“latInput” and “lonInput”) into jQuery objects anyways so that we can continue to use the jQuery functions.

As a final tally, we’ve cut the number of globals we introduce to 1 (PinpointLocation), minimizing pollution. Additionally, because all of the variables we used are alive only within the function body, a JavaScript minifier is free to rename the variables “latInput”, “mapDiv”, “myMap”, etc., cutting down on the filesize. If these variables were accessible from outside the code, they couldn’t be safely minified.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>