Search code examples
javascriptjquerybackbone.jsunderscore.jsunderscore.js-templating

Multiple underscore templates in a single file with common values


I am having a template.html file like this:

<script type="text/template" id="form-template">
<input type="text" class="form-control input-sm" style="" id="company"
               value="<% if(typeof company!=='undefined') {%><%=company%><% } %>" placeholder="Company name">

</script>

<script type="text/template" id="person-template">
<input type="text" class="form-control input-sm" style="" id="company"
               value="<% if(typeof company!=='undefined') {%><%=company%><% } %>" placeholder="Company name">

</script>

Now in the backbone view.js I have two views like this

var FormView = Backbone.View.extend({

 template: _.template($(formTemplate).filter("#form-template").html()),
 render:
 function(){this.$el.html(this.template(this.model.toJSON())}
});

var personView = Backbone.View.extend({

 template: _.template($(formTemplate).filter("#person-template").html()),
 render:
 function(){this.$el.html(this.template()}
});

When i render the form view, it works fine but when I render the person view, i get the fields pre-populated with value as "[object HTMLInputElement]" even if i pass nothing as the data.

The obvious guess is that both the templates are using <%=company%> but why that should be a problem if the templates are completely separate from each other?


Solution

  • You have two problems:

    1. Both of your templates contain id="company" so you should expect odd things to happen when you put both templates on the same page. id attributes must be unique or you don't really have HTML anymore, you have something that sort of looks like HTML.
    2. After you put <input id="company"> in the page, you have a window.company property whose value is the DOM node for <input id="company">.

    The second problem is what you're seeing but you should fix 1 anyway.

    Underscore templates use JavaScript's with to allow an object to act like a scope. If with doesn't find the name (company in this case) in the supplied object (which will be {} in the second template), then it goes up the scope chain trying to find a variable called company. Since you have <input id="company"> already in the DOM, with will find window.company as company. That leaves you with a DOM node in company and typeof company won't be 'undefined' so your template throws a stringified DOM node on the page.

    Consider this simplified example:

    <script type="text/template" id="form-template">
        <input type="text" id="company"><br>
    </script>
    <script type="text/template" id="person-template">
        <%= typeof company %><br>
        <%= typeof company !== 'undefined' ? company : '' %>
    </script>
    

    and then a little JavaScript to mirror what your Backbone code is doing:

    var f = _.template($('#form-template').html());
    var p = _.template($('#person-template').html());
    
    $('body').append(f());
    $('body').append(p());
    console.log(window.company);
    

    You should see something like this on the page:

    <input ...>
    object
    [object HTMLInputElement]
    

    and a DOM node in the console.

    Demo: http://jsfiddle.net/ambiguous/sRnuw/

    This is a rather insidious problem with no clean solutions that I can think of. Possible options:

    1. Make sure the ids on your form elements don't match any properties in your templates.

    2. Make sure you supply all the properties to your templates, even empty ones. This will let you short-circuit the with before it goes looking for things in window.

    3. Use the variable option to _.template:

      By default, template places the values from your data in the local scope via the with statement. However, you can specify a single variable name with the variable setting.

      See the _.template docs for examples that use the variable option.

    4. Switch to a better template system that doesn't use with trickery. I tend to use Handlebars.