Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

backbone.js view renders before model fetch

Tags:

backbone.js

I'm trying to make a small backbone.js app, but struggle with the order of things.

In my html file, I have two script blocks in the header:

<script type="text/template" id="model-template">
  <a href="#"><%= title %></a>
</script>

<script type="text/javascript">
  jQuery(function(){
    window.model.fetch();
  }); 
</script>

In my app.js, I have defined a simple model, view and a router.

(function($) {

window.MyModel = Backbone.Model.extend({
    url: '/mymodel'
});

window.mymodel = new MyModel();

$(document).ready(function() {

    window.MyModelView = Backbone.View.extend({
        template: _.template($('#mymodel-template').html()), 

        initialize: function () {
            _.bindAll(this, 'render');
        },

        render: function () {
            var renderedContent = this.template(this.model.toJSON());
            $(this.el).html(renderedContent);
            return this;
        }
    });
});

window.MyApp = Backbone.Router.extend({
    routes: {
        '': 'home'
    },

    initialize: function () {
        this.myModelView = new MyModelView({
            model: window.mymodel
        });
    },

    home: function() {
        var $body = $('body');
        $body.empty();
        $body.append(this.myModelView.render().el);
    }
 });

 $(function() {
    window.App = new MyApp();
    Backbone.history.start({pushState: true});
 });

})(jQuery);

The application is served by a simple sinatra application. The url /mymodel serves a static json file:

{
    "title": "My Model",
}

When loading the application, I get an error in the javascript console:

Uncaught ReferenceError: title is not defined

The problem seems to be, that the view renders itself before the model is fetched from the server. Why is that?

Yesterday, I followed the first two backbone.js screencasts from PeepCode. I have tried to compare my application with the one that came out of the screencasts, but cant't see a reason for why my application want work.

Any suggestions?

like image 283
Vegar Avatar asked Feb 09 '12 22:02

Vegar


4 Answers

In this case you should bootstrap your model data so that it's available on page load.

Instead of

window.model.fetch();

put something like this in (if using a .erb)

<script>
    window.model = new MyModel(<%= @model.to_json %>);
</script>

Otherwise you need to render the view once the model is fetched e.g.

bind the view to render when the model changes

initialize: function () {
    _.bindAll(this, 'render');

    this.model.on("change", this.render);
},

or handle the success of the model.fetch and render the view

window.model.fetch({
   success: function (model, response) { 
      window.MyApp.myModelView.render();
   }
});
like image 58
Chris Herring Avatar answered Nov 02 '22 08:11

Chris Herring


You can also take advantage of the deferred object http://api.jquery.com/category/deferred-object/ that Backbone.Model.fetch() returns, like this:

window.MyModel = Backbone.Model.extend({
   url: '/mymodel'
});

//when the model is fetched, store a reference to the jQuery deferred object
window.MyModelFetch = window.MyModel.fetch();

window.MyModelView = Backbone.View.extend({
    template: _.template($('#mymodel-template').html()), 

    initialize: function () {
        _.bindAll(this, 'render');
    },

    render: function () {
        //reference the deferred object and use 'done' method 
        //to register a callback after complete
        window.MyModelFetch.done(function(){
             var renderedContent = this.template(this.model.toJSON());
             $(this.el).html(renderedContent);
             return this;
        }
    }
});

You may want to create an extension of the backbone model that stores a deferred object on your model that you can reference, like this:

 Backbone.DeferrableModel = Backbone.Model.extend({
      fetch: function(){
           this.fetching = Backbone.Model.prototype.fetch.apply(this, arguments);
           return this.fetching;
      }
 });

Then in your view render method, you can just say this:

  render: function () {
        //the deferred reference is now directly referenced from your model
        this.model.fetching.done(function(){
             var renderedContent = this.template(this.model.toJSON());
             $(this.el).html(renderedContent);
             return this;
        }
    }    

It can be very handy to use an extended model and follow this pattern throughout your backbone application.

like image 5
brappleye3 Avatar answered Nov 02 '22 07:11

brappleye3


Resets the model's state from the server. Useful if the model has never been populated with data, or if you'd like to ensure that you have the latest server state. A "change" event will be triggered if the server's state differs from the current attributes. Accepts success and error callbacks in the options hash, which are passed (model, response) as arguments.

In this case, you'll want to render the view in the success callback.

model.fetch({
   error: function () {
   },
   success: function (model, response) { // model is ready now
      // do view stuff here
   }
});
like image 1
Trevor Avatar answered Nov 02 '22 06:11

Trevor


Clearly from the previous answers, you know that you need to render on the fetch success callback, however I think you problem is a bit more than that. Namely, your home route is used to build myModelView immediately rather than when its data loads. This is happening because you are calling render() in order to append to body. Instead, try and initialize the view with an existing element. This way you can delay the call for render until fetch has completed:

window.MyApp = Backbone.Router.extend({
    routes: {
        '': 'home'
    },

    initialize: function () {

    },

    home: function() {
        var $body = $(document.body).empty();
        var myModelEl = $("<div></div>").appendTo($body);

        this.myModelView = new MyModelView({
            model: window.mymodel,
            el: myModelEl
        });
    }
 });

Now, you haven't called render() yet, but your view is successfully attached to the DOM as you intended.

like image 1
Skylar Anderson Avatar answered Nov 02 '22 06:11

Skylar Anderson