Search code examples
backbone.jsdryencapsulationmustache

Backbone.js: how can I most effectively apply my shared Mustache template to a View's el?


Backbone JS Views are very "helpful" in that they always creates a DOM node for you, with a configurable tag, id and class. This is very nice and accommodating, but I'm finding that it creates an unfortunate situation: the DOM node created by the View is a hidden template.

This became obvious to me on our current project, where we are sharing Mustache templates between the front and back ends. With Backbone, when you want a DOM that looks like this:

<section class="note">
  <textarea>
  ...
  </textarea>

  <input type="button" class="save-button" value="Save">
  <input type="button" class="cancel-button" value="Cancel">
</section>

you end up creating templates that look like this:

<textarea>
{{& content}}
</textarea>

<input type="button" class="save-button" value="Save">
<input type="button" class="cancel-button" value="Cancel">

But now your template is tied to the secret root-node template on your Backbone view, which will need to be duplicated on the server side. So much for DRY encapsulation!

I don't see an immediately obvious way to address this, except by using setElement() with the rendered template at render time, but this brings other problems with it, like having to replace the newly rendered subtree in the DOM after every render().

How have you addressed this issue?


Solution

  • It's an interesting question. I've never had to solve this particular problem before, but I tried out a few options and I think I found one that I like.

    First, here's the code:

    //A base view which assumes the root element of its
    //template (string, DOM node or $) as its el.
    var RootlessView = Backbone.View.extend({
    
        //override the view constructor
        constructor: function(options) {
    
            //template can be provided by constructor argument or
            //as a subclass prototype property
            var template = options.template || this.template;
    
            //create a detached DOM node out of the template HTML
            var $el = Backbone.$(template).clone()
    
            //set the view's template to the inner html of the element
            this.template = $el.html(); 
    
            //set the element to the template root node and empty it
            this.el = $el.empty()[0];
    
            //call the superclass constructor
            Backbone.View.prototype.constructor.apply(this, arguments);
        }
    });
    

    Essentially you define a base view that expects every derived view to have a template property, or to take a template as an argument in the options hash. The template can be a string, a DOM node or a jQuery/Zepto -wrapped DOM node. The view assumes the root node of the template as its el, and redefines the template property as the contents of the root node.

    You would use it as a normal view:

    var View = RootlessView.extend({
        template: templateHtml,
        render: function() {
            this.$el.html(Mustache.render(this.template, this.model));
            return this;
        }
    }); 
    

    The el property is available from the get-go, and it's not detached and reattached on re-render. The only exception to the normal Backbone.View behavior is that if you've defined the id, cssClass or tagName properties or arguments, they will be ignored, because the template provides the root element.

    This is not extensively tested, but seems to pass most simple test cases. The only drawback that I can think of is that the template html string is stored on every view instance (instead of the prototype) and wastes precious bytes of memory, but that shouldn't be hard to solve either.

    Here is a working demo on JSFiddle.