3

I am rendering a single Backbone model in a view. I am using the default underscore template to render the model. How do I handle the "undefined" attribute errors when I am rendering the view (though the model has not loaded)? To clarify, here is an example.

// Using Mustache style markers
_.templateSettings = {
    interpolate : /\{\{(.+?)\}\}/g
};

App.Model = Backbone.Model.extend({});

App.Collection = Backbone.Collection.extend({
    model: App.Model
});

App.View = Backbone.View.extend({
    initialize: function() {
        _.bindAll(this, 'render');
        this.template = _.template($('#model_template').html());
        this.model.bind('reset', this.render);
    },
    render: function() {
        var renderedContent = this.template(this.model.toJSON());
        $(this.el).html(renderedContent);
        return this;
    }
});

// HTML
<div id="container"></div>
<script id="model_template" type="text/template">
    <strong>Name:</strong> {{ name }} <br/>
    <strong>Email:</strong> {{ email }} <br/>
    <strong>Phone:</strong> {{ phone }}
</script>

// Run code
var collection = new App.Collection;
var view = new App.View(model: collection.at(0));
$('#container').html(view.render().el);
collection.fetch();

When this code is run, the view is rendered twice, first with an empty model and second when the AJAX query is complete (and 'reset' is triggered). But the issue I am facing is JS stops at the first instance when the model is empty. It gives an error saying the model attribute is undefined.

What am I doing wrong here? Can I suppress the 'undefined' error when the view is rendered in the first instance?

Nilesh C
  • 697
  • 2
  • 7
  • 17
  • 1
    If you need to render a view with an empty model, I probably tend toward defaults as abraham has mentioned. Otherwise your templates could accommodate conditionals. See http://stackoverflow.com/questions/7230470/how-to-use-if-statements-in-underscore-js-templates/9321127#9321127 – SunnyRed Feb 19 '12 at 17:07
  • 2
    Thanks, SunnyRed. This looks like a good idea if the number of attributes are less. I have 20+ attributes in my template! Would be a tough thing to manage. – Nilesh C Feb 23 '12 at 03:45

4 Answers4

4

One option is to define default empty values for your model.

App.Model = Backbone.Model.extend({
  defaults: {
    name: '',
    email: '',
    phone: ''
  }
});
abraham
  • 46,583
  • 10
  • 100
  • 152
  • 1
    Yes, using defaults is what came to my mind earlier. However, there is a little bit of complexity in my case. I have a dynamic set of attributes for my model. My server is not REST based and does not send the same set of attributes for different data. So I ruled out this method. – Nilesh C Feb 23 '12 at 03:38
3

There are a few things going wrong with what you do:

When you do:

// Run code
var collection = new App.Collection;
var view = new App.View({model: collection.at(0)});
$('#container').html(view.render().el);
collection.fetch();

First, you create the collection. It does not yet have any models associated with it. So when next you initialize the view with the collection's first model that does not yet exist. The correct way to do that is to fetch() the collection and when that has completed create the view. Something like:

var p = collection.fetch();
p.done(function () {
  var view = new App.View({model: collection.at(0)});
  ...
}

It is also better to give the container element to the view. You can do that by simply

var view = new App.View({model: collection.at(0), el: '#container'});
mu is too short
  • 426,620
  • 70
  • 833
  • 800
ggozad
  • 13,105
  • 3
  • 40
  • 49
  • $.done doesn't seem to work for me with Backbone. I am using jQuery 1.7 already. Does `.fetch()` return an ajax object? I get `undefined` as the value of p when I run your code. – Nilesh C Feb 23 '12 at 03:39
  • `fetch()` returns the result of a `sync()` which is a ajax promise. If you have customized fetch or sync it might not work. In that case just don't use a promise but instead pass a success callback. – ggozad Feb 23 '12 at 09:46
  • Thanks for the tip, ggozad. I had a custom `sync()` which did not return the promise. Fixed it and your solution works perfectly. – Nilesh C Feb 25 '12 at 20:16
2

To avoid the undefined variable error while rendering underscore template, pass the template data inside a wrapper object. Missing property access won't throw an error.

var template = _.template($('#template').html());
var data = {name:'Jeo'}; // phone not available
template({data:data});
// Output: Jeo 

<div id="template">
    {{data.name}} {{data.phone}}
</div>
Venkat Kotra
  • 10,413
  • 3
  • 49
  • 53
  • But missing nested (deep) attributes will still throw errors. – regretoverflow Sep 19 '13 at 17:24
  • 1
    `
    {{data.name}} {{data.phone}} {{data.demographics.foo.bar}}
    ` will still throw an undefined error
    – regretoverflow Sep 20 '13 at 05:29
  • That gives an error in javascript too and its no different in template rendering. In the above example `{{data.phone}}` does not give error because `data` is defined. For `{{data.demographics.foo.bar}}` `data.demographics.foo` has to be defined and a check for that is a must before using `data.demographics.foo.bar` – Venkat Kotra Sep 20 '13 at 05:54
  • 1
    That was my point - that simply assigning the object to another object only handles a single case. you have to test every property to see if it exists – regretoverflow Sep 20 '13 at 19:31
0

I'd also suggest using Distal templates.

Kernel James
  • 3,752
  • 25
  • 32