1
\$\begingroup\$

The setup of Backbone:

1 Collection, 3 Models, 1 View for the collection, 1 View per Model.

HTML:

<div id="showit"></div>

Template:

<script id="item-template" type="text/x-handlebars-template">
    <a class="item" href="#">{{book.name}}</a>
    <br/>
    <span>Liked :{{book.likes}}</span><br/>
</script> 

Backbone JS:

Book    =   Backbone.Model.extend({
            defaults:{
                name    :   "No Name",
                author  :   "No Name",
                likes   :   0
            }

BookListView    =   Backbone.View.extend({
                        collection  :   BookList,
                        initialize  :   function(collection){
                            this.collection = collection;
                            this.render();
                        },
                        render      :   function(){
                            this.collection.each(function(model){
                            var pustakView = new BookView({model: model});
                            pustakView.render();
                            });                                
                        }                              
});

BookView    =   Backbone.View.extend({
    model       : Book,                
    initialize  : function(){},       
    events      : {"click a.item": "clicked"},
    template    : Handlebars.compile($('#item-template').html()),

    clicked     : function(e){
                    e.preventDefault();
                    var likes_count =    this.model.get("likes");
                    this.model.set({likes:++likes_count});
                    this.renderOne();
    },

    renderOne   : function(){
                    var output=this.template({'book':this.model.toJSON()});
                    $(this.el).html(output);                    
    },
    render      : function(){
                    var output = this.template({'book':this.model.toJSON()});
                    $(this.el).append(output);
                    $('#showit').append($(this.el));
     }  
});  

var book1   =   new Book({name:'Awesome Book1',author:"John",likes:10});
var book2   =   new Book({name:'Awesome Book2',author:"Dave",likes:20});
var book3   =   new Book({name:'Awesome Book3',author:"Bill",likes:30});

BookList    =   Backbone.Collection.extend({model:Book});
var myBookCollection    =   new BookList([book1,book2,book3]);
myBookList  =   new BookListView(myBookCollection);

So here is a snippet of what it generates on page load:

<div id="showit">
    <div>
       <a class="item" href="#">Awesome Book1</a><br>
       <span>Liked:10</span><br>
    </div>
...
...
</div>

I am a newbie into world of backbone and trying to implement the basics to apply it into my site. I have referred this site for the above implementation. (Well, not exactly as I have customized it for my needs).

The feature I have been trying to implement is that multiple models with same template will load in 1 Collection, clicking on any element in the rendered view of that model has to give me access to that model so that I can re-render the view of the clicked model only.

The above example does the same where in clicking a hyperlink updates the counter called 'likes' and updates the corresponding model view only. As it can be noticed I have not specified an 'el' attribute for the view of each model and so it will be a 'div'.

My Question is whether this method is an efficient method of updating individual views (when model attributes change due to events) or are there any better methods than this? Also I am not sure if having a view for each loaded model will not cause any memory issues (asuuming I have an infinite scroll implemented which keeps on loading the list of Books)

If the above implementation is good enough, I hope it can help other backbone newbies to get up and running with re-rendering model view when model attributes change.

\$\endgroup\$
7
\$\begingroup\$

First of all, Backbone views already have a cached version of $(this.el) in this.$el so you should use this.$el instead of $(this.el).

I find that things work better when views only touch their own el and leave it up to someone else to put that el into the DOM. In your case, that would mean that you shouldn't do this:

$('#showit').append($(this.el));

in the sub-view, it would be better to say:

render: function() {
    var output = this.template({'book':this.model.toJSON()});
    this.$el.html(output);
    return this; // <----------------- this is common behavior, you'll see why in a moment
}

and then in the parent view, you say this:

el: '#showit',
render: function() {
    this.collection.each(function(model) {
        var pustakView = new BookView({model: model});
        this.$el.append(pustakView.render().el);
    }, this);
    return this;
}

The parent adds the child using the child's new return value from render:

this.$el.append(pustakView.render().el);
// ---------------------------------^^

Also note the second argument to this.collection.each to make this inside the iterator function the parent view.

Now we don't need renderOne in BookView at all, you can bind render to the appropriate event. You can also leave the re-rendering up to Backbone. When you say this:

this.model.set({likes: ++likes_count});

you will trigger a 'change' event on this.model. If you bind to that in initialize:

initialize: function() {
    this.listenTo(this.model, 'change', this.render);
}

then Backbone will call render for you when the model changes.

Back to BookListView. A view's initialize will copy the collection property out of its options just like it will copy model:

constructor / initialize new View([options])

There are several special options that, if passed, will be attached directly to the view: model, collection, el, id, className, tagName, attributes and events.

so your BookListView should look more like this:

var BookListView = Backbone.View.extend({
    initialize: function() {
        this.render();
    },
    //...

and you should instantiate it like this:

var myBookList = new BookListView({ collection: myBookCollection });

Notice that I have removed collection: ... from the view declaration. The collection and model properties should be instances and they should be bound per-view-instance so you almost never want collection or model to appear when defining a view, you only want to see those in the view's argument list when you instantiate a view.

Furthermore, I notice that you're not using var to declare your variables. That means that you are creating accidental globals all over the place and that's never a good idea. Always use var to declare variables so that you can control the scope.

\$\endgroup\$
  • \$\begingroup\$ Implemented all the suggestions offered and was able to see the effect in firebug profiler. Results: v1 (21.222ms, 2076 calls), v2 (18.671ms, 1614 calls) v1 (profiler for my original script) v2 (profiler for script with suggested changes) Firebug Profiler was done for initialization of 'BookListView' with a collection consisting of 10 'Book' Objects. I see that the performance has increased. Could you also please shed some light on memory issues if there is 1 view per model with an infinite scroll implementation \$\endgroup\$ – learner May 12 '14 at 6:33
  • \$\begingroup\$ One view per model vs one big view shouldn't really have much of a memory impact on its own. If they scroll and scroll and scroll then you'll chew up pretty much the same memory either way. \$\endgroup\$ – mu is too short May 12 '14 at 17:16
1
\$\begingroup\$

Changing model attributes within an click handler is okay and very common, but I would suggest keeping your rendering code away from event handlers. Backbone supports event mechanism that can help implementing Observer pattern, so it's unnecessary to explicitly write renderOne() everywhere after you change the model.

In your case, this.model.set({likes:++likes_count}) causes the model sending a "change" message to all views that concerned with it. You can register BookView's renderOne() function to this message. Then, if the model changes, re-rendering function will be automatically called.

BookView    =   Backbone.View.extend({
  ...
  initialize  : function(){
    // if "change" on this model is triggered, call renderOne.
    this.listenTo(this.model, "change", this.renderOne);
    // if the model has been removed from the collection, this
    // view is not needed, call remove.
    this.listenToOnce(this.model, "remove", this.remove);
  },       
  events      : {"click a.item": "clicked"},
  ...    
  clicked     : function(e){
    ...
    this.model.set({likes:++likes_count});
    // this.renderOne(); this line is commented out 
  },
  renderOne   : function(){
    ...                 
  },
  render      : function(){
    ...
  }  
});

Besides, View provides remove() function to reclaim itself (remove HTML snippet from its parent node, stop listening to the model, etc), but it is the developer's duty to decide when to reclaim. A common time to reclaim View is after the corresponding model removed from the container, or destroyed. You can refer to Backbone's documentation for these special events.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy