Skip to content

View helpers for underscore templates

cw edited this page Jun 7, 2012 · 2 revisions

Underscore’s template engine lets you run any arbitrary JavaScript code that you want, within a template. You could write an entire JavaScript application within an underscore template if you want. But this is a really bad idea. Templates should be clean and simple. Some people go so far as to say “logic-less templates”, but I honestly don’t think that’s reasonable. I also don’t think it’s reasonable or responsible to have a bunch of JavaScript code littered throughout the template, though.

Fortunately, it’s easy to build helper methods for Underscore templates (and they end up looking a lot like Rails view helpers).

Logic In Templates: A Bad Idea

On a client project recently, I had a JavaScript object that needed to be bound to my template. The object looked somewhat like this:

var data = {
  type: "something",
  name: "whatever",
  addresses: [
    {
      type: "read_address",
      address: "1/2/3",
      value: "10"
    },
    {
      type: "write_address",
      address: "1/2/4"
    }
  ]
}

I needed to take all of the “addresses” and find each specific address within the array, by name, so that I could get the right information in place at the right time in my template.

The first pass of code that I wrote to do this, looked like this:

<script id="my-template" type="text/template">
  <p>type: <%= type %></p>
  <p>name: <%= name %></p>
  <p>read address: <%= _.find(this.addresses, function(addr){ return addr.type == "read_address" }).address %> </p>
  <p>read address value: <%= _.find(this.addresses, function(addr){ return addr.type == "read_address" }).value %> </p>
  <p>write address: <%= _.find(this.addresses, function(addr){ return addr.type == "write_address" }).address %> </p>
</script>

And this worked, but it’s a bad idea. I have a lot of duplication in this template and I have a lot of logic and processing of data in the template, too. This is going to make for a very painful setup when it comes to maintenance – especially if / when I have to change how the address is looked up.

View Helper Methods: A Better Idea

In asking around on twitter for some suggestions on how to clean this up, I received several responses. Out of everything that everyone said, though, I liked the response I got from DaveTheNinja because of it’s simplicity.

The basic idea is to recognize that the data I send to my template is a JavaScript object. Since this is an object, it can have methods. If I can have methods on my object, why not put methods on it as view helper methods?

var viewHelpers = {
  getAddress: function(type){
    var address = _.find(this.addresses, function(addr) { 
      return addr.type == type 
    });
    return address;
  }
}

_.extend(data, viewHelpers);

_.template(myTemplate, data);

The helper method – “getAddress” – is pretty much the same code. But now it’s consolidated in to a single location and I’m able to give better formatting for it which makes it easier to read and understand.

You can also see that I’ve kept the data separate from the objects until just before rendering the template. I’m using Underscore’s “extend” method to copy all of the methods from my “viewHelpers” object on to my data object.

Then when I pass the data object to my template, I can call my “getAddress” method like this:

<script id="my-template" type="text/template">
  <p>type: <%= type %></p>
  <p>name: <%= name %></p>
  <p>read address: <%= getAddress("read_address").address %> </p>
  <p>read address value: <%= getAddress("read_address").value %> </p>
  <p>write address: <%= getAddress("write_address").address %> </p>
</script>

And we’re done! I now have view helper methods that I can call from within my templates.

Using This With Backbone

I use this technique with Backbone a lot. The code that I showed above is really no different than what I do with my Backbone views, either. When you get to the point where you need to render your template, just extend the view helper methods on to your data before doing the actual rendering:

Backbone.View.extend({
  template: "#my-template",

  render: function(){

    var data = this.model.toJSON();
    _.extend(data, viewHelpers);

    var html = _.template($(this.template), data);
    this.$el.html(html);

  }
});

Using This With Backbone.Marionette

I’ve added this functionality in to Backbone.Marionette as well. ItemView, CompositeView and Layout will all look for a templateHelpers attribute on a view definition. If it finds it, it will mix that object in to the data that is used to render the template:

Backbone.Marionette.ItemView.extend({

  template: "#my-template",
  templateHelpers: viewHelpers

});

You can specify a reference to the viewHelpers object as shown in this example, provide an object literal as the templateHelpers directly, or specify a function that returns an object with the helper methods that you want. For more information see the documentation for Marionette.

Other Template Engines

This basic technique will probably work with other template engines, too. Some engines like Handlebars, though, have this idea baked in to them directly. But if you’re using Underscore’s templates, then this is a good way to keep your templates clean while still providing the logic you need.


Originally Posted At LosTechies.com: http://lostechies.com/derickbailey/2012/04/26/view-helpers-for-underscore-templates/