Binding a Backbone Model to a Marionette ItemView - blocking .fetch()?

21,435

Solution 1

From a very basic standpoint, throwing aside the specific example that you've provided, here is how I would approach the problem and solution.

A Generic Problem / Solution

Here's a generic version of the problem:

  • You need to fetch a model by its id.
  • You need a view to render after the model has been fetched.

This is fairly simple. Attach the model to the view before fetching the data, then use the "sync" event of the model to render the view:

MyView = Backbone.View.extend({
  initialize: function(){
    this.model.on("sync", this.render, this);
  },

  render: function(){ ... }
});


myModel = new MyModel({id: someId});
new MyView({
  model: myModel
});

myModel.fetch();

Things to note:

I'm setting up the model with its id, and the view with the model before calling fetch on the model. This is needed in order to prevent a race condition between loading the data and rendering the view.

I've specified generic Backbone stuff here. Marionette will generally work the same, but do the rendering for you.

Your Specific Needs

Blocking Fetch

Bad idea, all around. Don't try it.

A blocking fetch will make your application completely unresponsive until the data has returned from the server. This will manifest itself as an application that performs poorly and freezes any time the user tries to do anything.

The key to not doing this is taking advantage of events and ensuring that your events are configured before you actually make the asynchronous call, as shown in my generic example.

And don't call the fetch from within the model's initializer. That's asking for trouble as you won't be able to set up any views or events before the fetch happens. I'm pretty sure this will solve the majority of the problems you're having with the asynchronous call.

Events Between View And Model

First, I would avoid using MyApp.vent to communicate between the model and the view instance. The view already has a reference to the model, so they should communicate directly with each other.

In other words, the model should directly trigger the event and the view should listen to the event on the model. This works in the same way as my simple example, but you can have your model trigger any event you want at any time.

I would also be sure to the use bindTo feature of Marionette's views, to assist in cleaning up the events when the view is closed.

MyView = Backbone.Marionette.ItemView.extend({
  initialize: function(){
    this.bindTo(this.model, "do:something", this.render, this);
  }
});

MyModel = Backbone.Model.extend({
  doSomething: function(){
    this.trigger('do:something');
  }
});

myModel = new MyModel();
new MyView({
  model: myModel
});

myModel.doSomething();

Other Items

There are some other items that I think are causing some problems, or leading toward odd situations that will cause problems.

For example, you have too much happening in the DOMReady event: $ ->

It's not that you have too much code being executed from this event, but you have too much code defined within this event. You should not have to do anything more than this:

$ -> 
  App.MyApp.start(data)

Don't define your Marionette.Application object in this event callback, either. This should be defined on its own, so that you can set up your initializers outside of the DOMReady callback, and then trigger them with the app.start() call.

Take a look at the BBCloneMail sample application for an example on rendering a layout and then populating its regions after loading data and external templates:

source: https://github.com/derickbailey/bbclonemail

live app: http://bbclonemail.heroku.com/


I don't think I'm directly answering your questions the way you might want, but the ideas that I'm presenting should lead you to the answer that you need. I hope it helps at least. :)

Solution 2

See Derick's new suggestion to tackle this common problem at: https://github.com/marionettejs/backbone.marionette/blob/master/upgradeGuide.md#marionetteasync-is-no-longer-supported

In short, move the asynchronous code away from your views, which means you need to provide them with models whose data has already been fetched. From the example in Marionette's upgrade guide:

 Marionette.Controller.extend({
  showById: function(id){
    var model = new MyModel({
      id: id
    });

    var promise = model.fetch();

    $.when(promise).then(_.bind(this.showIt, this));
  },

  showIt: function(model){
    var view = new MyView({
      model: model
    });

    MyApp.myRegion.show(view);
  }
});
Share:
21,435
Admin
Author by

Admin

Updated on July 09, 2022

Comments

  • Admin
    Admin almost 2 years

    This is a 2 part question. 1) Is there a better way to render a model to a view asynchronously? I'm currently making the ajax request using the fetch method in the model (though I'm calling it explicitly upon initilization), then rendering the templated view using an application event, vent, which gets published from inside the model after the parse method is called. Cool but wonky? 2) Would a blocking fetch method be of use, and is it possible?

    The application renders this to the page:

    layout
    navbar
    index
    

    Then it fetches the model and renders this:

    layout
    navbar
    thing
    1
    something
    somethingelse
    

    But, if I don't use the vent trigger, it (expectedly) renders:

    layout
    navbar
    thing
    1
    null
    null
    

    The html templates:

    <!-- Region: NavBar -->
    <script type="text/template" id="template-navbar">
       <div id="navbar">
          navbar
       </div>
    </script>
    
    <!-- View: IndexView -->
    <script type="text/template" id="template-index">
       <div id="index">
          index
       </div>
    </script>
    
    <!-- View: ThingView -->
    <script type="text/template" id="template-thing">
       <div id="thing">
          thing<br/>
          <%= id %><br/>
          <%= valOne %><br/>
          <%= valTwo %><br/>
       </div>
    </script>
    
    <!-- Region -->
    <div id="default-region">
      <!-- Layout -->
      <script type="text/template" id="template-default">
         layout
         <div id="region-navbar">
         </div>
         <div id="region-content">
         </div>
      </script>
    </div>
    

    app.js:

    window.App = { }
    
    # Region
    class RegionContainer extends Backbone.Marionette.Region
      el: '#default-region'   
      # Called on the region when the view has been rendered
      onShow: (view) ->
        console.log 'onShow RegionContainer'
    
    App.RegionContainer = RegionContainer
    
    # Layout
    class DefaultLayout extends Backbone.Marionette.Layout
      template: '#template-default'  
      regions:
        navbarRegion: '#region-navbar'
        contentRegion: '#region-content'
      onShow: (view) ->
        console.log 'onShow DefaultLayout'
    
    App.DefaultLayout = DefaultLayout
    
    # NavBar (View)
    class NavBar extends Backbone.Marionette.ItemView
      template: '#template-navbar'    
      initialize: () ->
        console.log 'init App.NavBar'
    
    App.NavBar = NavBar
    
    # Index View
    class IndexView extends Backbone.Marionette.ItemView
      template: '#template-index'  
      initialize: () ->
        console.log 'init App.IndexView'
    
    App.IndexView = IndexView
    
    # Thing View
    class ThingView extends Backbone.Marionette.ItemView
      template: '#template-thing'  
      model: null
      initialize: () ->
        console.log 'init App.ThingView'
      events:
        'click .test_button button': 'doSomething'
      doSomething: () ->
        console.log 'ItemView event -> doSomething()'
    
    App.ThingView = ThingView
    
    # Thing Model
    class Thing extends Backbone.Model
      defaults:
        id: null
        valOne: null
        valTwo: null
      url: () ->
        '/thing/' + @attributes.id
      initialize: (item) ->
        console.log 'init App.Thing'
        @fetch()
      parse: (resp, xhr) ->
        console.log 'parse response: ' + JSON.stringify resp 
        # resp: {"id":"1","valOne":"something","valTwo":"somethingelse"}
        @attributes.id = resp.id
        @attributes.valOne = resp.valOne
        @attributes.valTwo = resp.valTwo
        console.log 'Thing: ' + JSON.stringify @
        @
        App.MyApp.vent.trigger 'thingisdone' 
    
    App.Thing = Thing
    
    # App
    $ ->
    
      # Create application, allow for global access
      MyApp = new Backbone.Marionette.Application()
      App.MyApp = MyApp
    
      # RegionContainer
      regionContainer = new App.RegionContainer
    
      # DefaultLayout
      defaultLayout = new App.DefaultLayout
      regionContainer.show defaultLayout
    
      # Views
      navBarView = new App.NavBar
      indexView = new App.IndexView
    
      # Show defaults
      defaultLayout.navbarRegion.show navBarView
      defaultLayout.contentRegion.show indexView
    
       # Allow for global access
      App.defaultRegion = regionContainer
      App.defaultLayout = defaultLayout
    
      # Set default data for MyQpp (can't be empty?)
      data = 
        that: 'this'
    
      # On application init...
      App.MyApp.addInitializer (data) ->
        console.log 'init App.MyApp'
    
        # Test
        App.modelViewTrigger = ->
          console.log 'trigger ajax request via model, render view'
          App.MyApp.vent.trigger 'show:thing' 
    
        App.timeoutInit = ->
          console.log 'init timeout'
          setTimeout 'App.modelViewTrigger()', 2000
    
        App.timeoutInit()
    
        # Event pub/sub handling
        App.MyApp.vent.on 'show:thing', ->
          console.log 'received message -> show:thing'
          thing = new App.Thing(id: '1')
          App.thingView = new App.ThingView(model: thing)
          # I should be able to do this, but it renders null
          # App.defaultLayout.contentRegion.show App.thingView
    
        # Testing to see if I could pub from inside model..yes!
        App.MyApp.vent.on 'thingisdone', ->
          console.log 'received message -> thingisdone'
          App.defaultLayout.contentRegion.show App.thingView
    
      MyApp.start data
    
  • Admin
    Admin almost 12 years
    I think I'm beginning to see what you mean after messing with some code. This one is going to need to sit a while :) Thanks!
  • Rudolf Meijering
    Rudolf Meijering over 11 years
    @Derick-bailey, how do you prevent Marionette from rendering the view before the model is fetched? Since the model already contains an id, Marionette tries to render it immediately?
  • MBHNYC
    MBHNYC almost 11 years
    Hey Derick — would you still consider this an up-to-date best practice on structuring fetch + renders? Curious as I've tried this a few different ways in my app and am curious which is Derick Approved®.. ;)
  • Duu82
    Duu82 almost 8 years
    I would make note of the date of this response. In particular, in v1.0.0-rc3 of Marionette, bindTo was replaced with the listenTo method. github.com/marionettejs/backbone.marionette/blob/master/…