Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I render a Backbone Collection in a List and Item View?

I am working on a contact bar which renders all contacts of a user in a html list.

What I have:

  1. UserModel - This is a simple Backbone.Model with username and email
  2. UserCollection - This is used as the contact list
  3. ContactsView - This is the ul contact list
  4. ContactView - This is a single contact model rendered as li

I am currently breaking my head about a solution how (and where) I can fetch my UserCollection and how I pass the single models down to a single ContactView item.

Specific hurdles are:

  1. Where should I fetch, store the UserCollection
  2. How do I render the contact list
  3. How do I render the contact items
  4. How do I prevent fetch({ success: callback }) from breaking my code structure

My current code is this:

entrance point:

// create a new instance of the contact list view
var view = new ContactsView();
// insert the rendered element of the contact list view in to the dom
$('div.contacts-body').html(view.render().el);
view.fetch({ success: view.loadContacts });

ContactsView:

define(
    ['jquery', 'underscore', 'backbone', 'text!templates/conversations/contacts.html', 'collections/users', 'views/conversations/contact'],
    function($, _, Backbone, ContactsTemplate, UserCollection, ContactView) {

        var ContactsView = Backbone.View.extend({

            tagName: "ul",

            className: "contacts unstyled",

            attributes: "",

            // I am feeling uneasy hardcoding the collection into the view
            initialize: function() {
                this.collection = new UserCollection();
            },

            // this renders our contact list
            // we don't need any template because we just have <ul class="contacts"></ul>
            render: function() {
                this.$el.html();
                return this;
            },

            // this should render the contact list
            // really crappy and unflexible
            loadContacts: function() {
                this.collection.each(function(contact) {
                    // create a new contact item, insert the model
                    var view = new ContactView({ model: contact });
                    // append it to our list
                    this.$el.append(view.render().el);
                });
            }

        });

        return ContactsView;

});

ContactView

define(
    ['jquery', 'underscore', 'backbone', 'text!templates/conversations/contact.html'],
    function($, _, Backbone, ContactTemplate) {

        var ContactView = Backbone.View.extend({

            tagName: "li",

            className: "contact",

            attributes: "",

            template:_.template(ContactTemplate),

            initialize: function() {
                this.model.bind('change', this.render, this);
                this.model.bind('destroy', this.remove, this);
            },

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

        });

        return ContactView;

});

Could somebody help me about my four hurdles.

Good example links are welcome. I oriented my code style at the todos list unfortunatly the todos list isn't that advanced...

UPDATED CODE:

define(
    ['jquery', 'underscore', 'backbone', 'text!templates/conversations/contacts.html', 'collections/users', 'views/conversations/contact'],
    function($, _, Backbone, ContactsTemplate, UserCollection, ContactView) {

        var ContactsView = Backbone.View.extend({

            tagName: "ul",

            className: "contacts unstyled",

            attributes: "",

            events: {

            },

            initialize: function() {
                this.collection = new UserCollection();
                this.collection.on('reset', this.render);
                this.collection.fetch();
            },

            render: function() {
                // in chromium console
                console.log(this.el); // first: html, second: undefined
                console.log(this.$el); // first: html in array, second: undefined
                this.$el.empty(); // error on the called that this.$el is undefined

                this.collection.each(function(contact) {
                    var view = new ContactView({ model: contact });
                    this.$el.append(view.el);
                }.bind(this));

                return this;
            }

        });

        return ContactsView;

Can it be that reset is triggering this.render twice?

like image 618
bodokaiser Avatar asked Jul 25 '12 09:07

bodokaiser


2 Answers

First of all: why do you fetch the view? Backbone views do not have a fetch method..

1 The correct place to fetch your UserCollection would be inside the view's initialize method:

initialize: function() { // ContactsView
  _.bindAll(this, 'render', 'otherMethodName', ...); // Bind this to all view functions
  ...
  this.collection.on('reset', this.render); // bind the collection reset event to render this view
  this.collection.fetch();
  ...
}

Now you fetch the contacts exactly when you need them. Next step is to render the collection.

2 Binding to the reset event makes your loadContacts method obsolete and we can do that in the render function:

render: function() {
  this.$el.empty(); // clear the element to make sure you don't double your contact view
  var self = this; // so you can use this inside the each function

  this.collection.each(function(contact) { // iterate through the collection
    var contactView = new ContactView({model: contact}); 
    self.$el.append(contactView.el);
  });

  return this;
}

Now you render your contactlist inside the render method, where it should be done.

3 The ContactView actually looks good.

Just make the item to render itself in the initialize method, so you don't have to make useless calls in the ContactsView's render method and clutter up your code. Also bindAll here as well.

initialize: function() { // ContactView
  _.bindAll(this, 'render', 'otherMethodName', ...);
  ...
  this.render(); // Render in the end of initialize
}

I have no idea what you are asking in here, but I think the best way is not to use success callbacks. The collections and models trigger events whenever something is done to them, so tapping onto them is much more robust and reliable than success callbacks. Check out the catalog of events to learn more. The Wine Cellar tutorial by Christophe Coenraets is has an excellent example of this kind of listview-listitemview arrangement.

Hope this helps!

UPDATE: Added _.bindAlls to fix the problem with this in a event bound render call. Some info on binding this.

like image 172
jakee Avatar answered Nov 15 '22 14:11

jakee


NOTE: all the code is simplified and no tested

When I have all the elements structure defined, as you have, with all the Models, Collections and Views implemented then I implement a Loader which is in charge of trigger the fetching and rendering actions.

First of all I need to expose the classes definition from the outside something like this:

// App.js
var App = {}

// ContactsCollection.js
$(function(){
  var App.ContactsCollection = Backbone.Collection.extend({ ... });
});

// ContactsView.js
$(function(){
  var App.ContactsView = Backbone.View.extend({ ... });
});

// and so on...

And then I implement what I call the Loader:

// AppLoad.js
$(function(){

  // instantiate the collection
  var App.contactsCollection = new App.ContactsCollection();

  // instantiate the CollectionView and assign the collection to it
  var App.contactsView = new App.ContactsView({
    el: "div.contacts-body ul",
    collection: App.contactsCollection
  });

  // fetch the collection the contactsView will
  // render the content authomatically
  App.contactsCollection.fetch();
});

Another changes you have to do is configure the ContactsView in a way that respond to the changes in the App.contactsCollection because as the fetch() is asynchronous you can call render() when the collection is still not loaded, so you have to tell to the CollectionView to render it self when the Collection is ready:

var ContactsView = Backbone.View.extend({
  initialize: function( opts ){
    this.collection.on( 'reset', this.addAll, this );
    this.collection.on( 'add', this.addOne, this );
    // ... same with 'remove'
  },

  addOne: function( model ){
    var view = new App.ContactView({ model: contact });
    this.$el.append( view.render().el );
  },

  addAll: function(){
    this.collection.each( $.proxy( this.addOne, this ) );
  }
});

You have to require your js files in the proper order:

  1. App.js
  2. Your Models, Collections, Views
  3. AppLoad.js

With this system you obtain:

  • External access to your collection in case you need to access it from another place.
  • External control of the CollectionView.el with is better for decoupling and testing.
  • The CollectionView will respond to changes in the Collection authomatically

Note: If you use Router you can move the AppLoad.js logic to there.

like image 38
fguillen Avatar answered Nov 15 '22 13:11

fguillen