How to prevent Backbone.Marionette from rendering a view if it's model hasn't been fetched?
Asked Answered
B

4

12

In my backbone.Marionette application I have a Model that requires an Id attribute to construct it's url. I therefore create the model by passing it an Id, add it to a view and then fetch the model:

   model = new Model({_id:id})               
   view = new View({model:model})                               
   app.content.show(view)                                                    
   model.fetch()

I would expect the view to only start rendering once the model has been fetched, but Marionette renders the model immediately causing my template rendering to fail as the expected attributes don't exist. Any workarounds?

I'm trying to do something similar to the accepted answer here: Binding a Backbone Model to a Marionette ItemView - blocking .fetch()?

But while that works with backbone, as stated in the answer, Marionette automatically renders the view.

Also see: Backbone Marionette Displaying before fetch complete

Bromidic answered 26/11, 2012 at 14:53 Comment(2)
Wouldn't it be easier to instead delay the app.content.show() till after the fetch? That would for all purposes delay rendering the view till the model is fetched.Slobbery
If you instead wish to show some kind of loading message, have a look at github.com/marionettejs/backbone.marionette/wiki/…Slobbery
G
7

If you truly want to prevent rendering until the model has been fetched you should reorder your calls like this:

model = new Model({_id:id});
view = new View({model:model});
model.fetch({success: function () {
  app.content.show(view);
});

Alternatively, you should think about rendering immediately and taking advantage of Marionette's blank state support.

Glee answered 26/11, 2012 at 21:55 Comment(2)
I ended up using something similar, but I was hoping there was something built in to marionette to handle such cases. Thanks.Bromidic
Can you explain more about Marionette's blank state support?Galilee
D
9

Both answers above work.
Here's yet another way which I prefer to use (feels more backboney).

Bind the view directly to the model
When the model is done being fetched, the function you specify will run. This can be done with models or collections.

var Model = Backbone.Model.extend({});
var model = new Model();
model.fetch();

var View = Marionette.ItemView.extend({
    model:model,
    initialize: function(){
        this.model.bind("sync", this.render, this);// this.render can be replaced
    }                                              // with your own function
});                               

app.region.show(new View);                                           

This also enables you to fetch whenever you want.

Dagnah answered 27/2, 2013 at 9:6 Comment(1)
.show() calls .render() on the view that its passed so this approach renders once when you call .show(new View) and again on model:sync.Defunct
G
7

If you truly want to prevent rendering until the model has been fetched you should reorder your calls like this:

model = new Model({_id:id});
view = new View({model:model});
model.fetch({success: function () {
  app.content.show(view);
});

Alternatively, you should think about rendering immediately and taking advantage of Marionette's blank state support.

Glee answered 26/11, 2012 at 21:55 Comment(2)
I ended up using something similar, but I was hoping there was something built in to marionette to handle such cases. Thanks.Bromidic
Can you explain more about Marionette's blank state support?Galilee
K
2

based on one of Derick Bailey's examples:

 model = new Model({_id:id})               
   var fetched = model.fetch();

  // wait for the model to be fetched
  $.when(fetched).then(function(){

     view = new View({model:model})                               
     app.content.show(view)     
  });
Kape answered 28/11, 2012 at 8:46 Comment(4)
Thanks, this is basically the same as handling it in the success handler of fetch, although I prefer that way above this.Bromidic
Hi, not exactly, some times fetch can return before the dom is rendered a deferred object can help hereKape
Where deferreds are really interesting is when you need to render a view after multiple data sources have been fecthed (see davidsulc.com/blog/2013/04/02/…). So using a deferred even when only one source is fetched allows you to keep your code consistent.Ladykiller
Thanks David, that's indeed a good use case to prefer deferred's.Bromidic
U
1

I've recenlty run into the same issue and settled on a pattern of using Marionette's event system to let the views communicate their status before firing show(). I'm also using requireJS which adds some additional complexity - but this pattern helps!

I'll have one view with a UI element that when clicked, fires an event to load a new view. This could be a subview or a navview - doesn't matter.

App.execute('loadmodule:start', { module: 'home', transition: 'left', someOption: 'foo' });

That event is caught by the Wreqr 'setHandlers' and triggers a view loading sequence (in my case I'm doing a bunch of logic to handle state transitions). The 'start' sequence inits the new view and passes in necessary options.

var self = this;
App.commands.setHandlers({'loadmodule:start': function( options ) { self.start( options );

Then the view getting loading handles the collections / models and fetching on initalize. The view listens for model / collection changes and then fires a new "ready" event using wreqr's executre command.

this.listenTo( this.model, 'change', function(){
    App.execute('loadmodule:ready', { view: this, options: this.options });
}); 

I have a different handler that catches that ready event (and options, including the view opject reference) and triggers the show().

ready: function( options ) {

// catch a 'loadmodule:ready' event before proceeding with the render / transition - add loader here as well

var self = this;
var module = options.options.module;
var view = options.view;
var transition = options.options.transition;
var type = options.options.type;

if (type === 'replace') {
    self.pushView(view, module, transition, true);
} else if (type === 'add') {
    self.pushView(view, module, transition);
} else if (type === 'reveal') {
    self.pushView(view, module, transition, true);
}   

},


pushView: function(view, module, transition, clearStack) {

var currentView = _.last(this.subViews);
var nextView = App.main.currentView[module];
var self = this;

var windowheight = $(window).height()+'px';
var windowwidth = $(window).width()+'px';
var speed = 400;

switch(transition) {
case 'left':

    nextView.show( view );

    nextView.$el.css({width:windowwidth,left:windowwidth});
    currentView.$el.css({position:'absolute',width:windowwidth,left:'0px'});

    nextView.$el.animate({translate: '-'+windowwidth+',0px'}, speed, RF.Easing.quickO);
    currentView.$el.animate({translate: '-'+windowwidth+',0px'}, speed, RF.Easing.quickO, function(){

        if (clearStack) {
            _.each(_.initial(self.subViews), function( view ){
                 view.close();
            });
            self.subViews.length = 0;
            self.subViews.push(nextView);
        }

    });

break;

I'll try to write up a decent gist of the whole system and post here in the next week or so.

Unilingual answered 15/5, 2013 at 1:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.