Skip to content

jQuery - The Little Things - 2

It seems my world has become doing everything but writing Javascript. One of my rather frequent tasks these days is reviewing code. While this post is targeted at a specific project, it's the perfect little reminder everyone needs from time to time (including lazy ol' me…).

The Problematic Source

To many people this looks like valid code - in its current form »it just works«™:

// Let there be some data
var image = {
  width: "240",
  height: "230",
  title: "dancing unicorns - by Someone Special",
  alt: "dancing unicorns" ,
  img: "images/dancing-unicorns.jpg"
};
jQuery('<img alt="' + image.alt + '" title="' + image.title + '" width="' + image.width + '" height="' + image.height + '" src="' + image.img + '" />')
  .appendTo('#some-selector');

Escaping Strings For Use In HTML

The most important, yet not very obvious, issue is escaping special characters. You know that <, >, &, " and ' have a special meaning in HTML. <> delimit tags, " and ' delimit attribute values, & is the lead-in of entities.

What do you think would happen to '<img title="' + image.title + '">' were some content author to set image.title = '"dancing unicorns" by Someone Special';? exactly, the resulting HTML string would look like <img title=""dancing unicorns" by Someone Special"> - obviously invalid markup. Things break, they break fast. The problem gets worse if content authors and developers aren't the same person - and they never are!

Aside from the risk some random team member might venture outside the scope of alpha-numeric characters, this also presents a security vulnerability. Someone with malicious intent might be able to inject some funky code into your page - something you really want to prevent by all means.

Javascript - the feature-poorest language since brainfuck - does not have a function to escape these special characters (not yet at least). Javascript developers are used to doing things themselves, over and over again. The following snippet is such a thing:

function escapeHtml(text) {
  var characters = {
    '&': '&amp;',
    '"': '&quot;',
    "'": '&#039;',
    '<': '&lt;',
    '>': '&gt;'
  };
  return (text + "").replace(/[<>&"']/g, function(m){
    return characters[m];
  });
};

Introducing this homebew html-escaper to our image-creator, we end up with:

jQuery('<img alt="' + escapeHtml(image.alt) + '" title="' + escapeHtml(image.title) + '" width="' + escapeHtml(image.width) + '" height="' + escapeHtml(image.height) + '" src="' + escapeHtml(image.img) + '" />')
  .appendTo('#some-selector');

Not very nice, but at least we've managed to kill the stability and security concerns.

Using jQuery

What we can do manually with escapeHtml(), we can also let jQuery (the DOM, actually) do for us. jQuery.attr() wraps element.setAttribute. This is a DOM method, not a string wrangler. This method knows that the output is meant for the DOM and thus handles escaping special characters internally (figuratively speaking). jQuery, being the spectacular API it is, allows us to rewrite our image-creator to something safer and more readable:

jQuery('<img />').attr({
  src: image.img,
  alt: image.alt,
  title: image.title,
  width: image.width,
  height: image.height
}).appendTo('#some-selector');

Paying close attention to the code you've probably seen some optimization potential. image already is a map, why not simply pass that?

image.src = image.img; // did you see that coming?
jQuery('<img />').attr(image).appendTo('#some-selector');

But, but, now there's an img attribute on the <img> element. Could that mean that our content authors could add arbitrary attributes like image.whosDaBoss = "me me me!";? Well, yes, sadly this can happen - and as you know Murphy's Law: whatever can happen, will happen. So how can we prevent undesired attribtues from being set?

Underscore, a functional library you really should have a closer look at, provides the function _.pick. The function walks maps (objects, dictionaries, hashes, whatever the cool kids call it today) and only returns the properties you wanted:

image.src = image.img; // yeah, still here
var attributes = _.pick(image, 'src', 'alt', 'title', 'width', 'height');
jQuery('<img />').attr(attributes).appendTo('#some-selector');

If you're not already using Underscore and have no need for any of its (quite helpful) functions, you can implement pick() yourself:

function pick(map /* ..rest */) {
  var properties = Array.prototype.slice.call(arguments, 1);
  var result = {};
  for (var i = 0, length = properties.length; i < length; i++) {
    if (map[properties[i]] !== undefined) {
      result[properties[i]] = map[properties[i]];
    }
  }
 
  return result;
}

Learning By Teaching

Writing this article opened the door for others to comment. @bassistance pointed out that jQuery takes a second argument - a map of attributes - that eliminates the need for the .attr() call. Putting this into action:

image.src = image.img; // yeah, still here
var attributes = _.pick(image, 'src', 'alt', 'title', 'width', 'height');
jQuery('<img />', attributes).appendTo('#some-selector');

Conclusion

  • Be wary of context switches like text -> html, always escape everything
  • Remember that the data you're handling is not your domain, it might change without you being informed
  • Use jQuery's powerful API
  • Use Underscore, lot's of helping stuff in there

Next to neglected string escaping, unfamiliarity with jQuery (any library, actually) is something I see (and experience myself) every day. If this is happening to you, too, try setting up a »read the docs day« for your team. The gist is that every developer has to look up (and actually read) the docs of every single function he uses that day. If you're a fun bunch, count the (numerous) »oooh, I didn't know XYZ could do that?!« developers mumble into their beards that day. The one who's learnt the most gets a free beer (or ten).

Comments

Display comments as Linear | Threaded

Jörn Zaefferer on :

Jörn ZaeffererInstead of calling .attr(), you can also pass that map to the second argument of jQuery. "As of jQuery 1.4, the second argument to jQuery() can accept a map consisting of a superset of the properties that can be passed to the .attr() method." See http://api.jquery.com/jQuery/#jQuery2

The author does not allow comments to this entry