Search code examples
backbone.jsbackbone-viewsbackbone-routing

Backbone view organization and partials


I have a rather complex Backbone application and I'm not sure how to organize the views/templates. The application is a web-based email client. I'm really having trouble understanding how to make this sidebar.

The application sidebar is very similar to what you see in Apple Mail/Outlook. It's basically a folder browser. This view exists on each page.

I have two main issues:

How do I get the collection data into the sidebar view?

There are technically three "collections" that get rendered on the sidebar - accounts => mailboxes, and labels. So some sample code would look like this:

<div class="sidebar">
  <div class="sidebar-title">Mailboxes</div>

  <div class="sidebar-subtitle">Gmail</div>
  <div class="mailboxes" data-account-id="1">
    <ul>
      <li><a href="...">Inbox</a></li>
      <li><a href="...">Sent</a></li>
      ...
    </ul>
  </div>

  <div class="sidebar-subtitle">Yahoo</div>
  <div class="mailboxes" data-account-id="2">
    <ul>
      <li><a href="...">Inbox</a></li>
      <li><a href="...">Sent</a></li>
      ...
    </ul>
  </div>

  <div class="sidebar-title">Labels</div>
  <div class="sidebar-labels">
    <ul>
      <li>Home</li>
      <li>Todo</li>
    </ul>
  </div>
</div>

So theoretically I need to do something like this, no?

<div class="sidebar">
  <div class="sidebar-title">Mailboxes</div>

  <% for account in @accounts.models: %>
    <div class="sidebar-subtitle"><%= account.get('name') %></div>
    <div class="mailboxes" data-account-id="<%= account.get('id') %>">
      <ul>
        <% for mailbox in account.mailboxes.models: %>
          <li><a href="..."><%= mailbox.get('name') %></a></li>
        <% end %>
      </ul>
    </div>
  <% end %>

  <div class="sidebar-title">Labels</div>
  <div class="sidebar-labels">
    <ul>
      <% for label in @labels: %>
        <li><%= label.get('name') %></li>
      <% end %>
    </ul>
  </div>
</div>

The problem is that I can't pass both @accounts and @labels from the router. I realize I could use @options, but that seems messy and unbackbone. This sidebar needs to exist on each page. What should that look like on the Backbone stack? I don't think it should have its own router, since it's not really a "page".

Do I break things up into smaller views?

Should each mailbox be its own view? Should each mailbox be its own view? Each label its own view? I want to listen for events, such as a new message, and update a mailboxes unread count (for example). But how do I do this? How do I nest the views and handle all the object creation/fetching efficiently? If I am going to break things into smaller views, what should my sidebar view look like?

Sorry if I'm rambling. I've been looking at this for days and can't seem to find a good solution.


Solution

  • Using options is totally legit and Backbone-y. So I wouldn't fear utilizing it although there are plenty of other ways to go with this. Yeah. I think the sidebar should be initialized inside your bigger page-view which gets called by the router.

    I also think your mailboxes should be its own sub-view. Sounds like regardless of whether the mail is yahoo or google, each mailbox will have the same functionality (like minimizing) so it makes a lot of sense to create that view class and reuse it multiple times.

    So what should this mailbox view take? (with respect to collection)

    Again, you have choices. Throw in 1 large collection that has everything, or throw in each collection that represents the mail from a type of account...

    If your collections are already separate (e.g. 1 for Google mail, 1 for Yahoo mail) then it would be easy to create a subview and pass the appropriate collection into each one. Even if it was not separate, I'd probably filter the collection down to only the models it needs before passing it in as a collection. This is what I would lean toward. I'm assuming that in each mailbox view there is never a situation where that mailbox needs access to any mail model not associated with a particular account. So it doesn't make sense to pass in anything you don't need.

    As for labels, I'm not sure. It depends on what and how your labels come to be I suppose. For example, do you have your own label model that is basically a label tag associated with a mail model.id? Or are labels an attribute of a mail models that you're plucking?

    UPDATE with sample code - Focus on passing data through view hierarchy

    So I basically outline here a very common type of way of making child views inside of parent views and how to pass different data (read: in your case collections) through the hierarchy of views.

    You can do this for even more child views (like maybe each mail model has it's own representative child view as a list item inside the mailbox view etc.) by simply repeating the pattern, utilizing the [options] argument of the View constructor and passing stuff through and accepting it on the inside.

    In many parts of this code, I take a long winded approach so as to make it more transparent exactly which data is getting passed where. I slightly altered your mailbox template just to illustrate a point as to how you might add more child-views but you can certainly change it back. What you do with your MailboxView is up to you and your code (depending on your goals) should reflect that.

    So without further ado, here is some sample code to chew on and think about. I whipped this together so there might be errors. I didn't actually see if it would execute.

    Normally in a view, we'd probably define the view.el as the most outer element in the template and only include the inner parts but to keep with the templates you provided and to decrease the number of possible confusing extras in the code, I've left the templates as they are for the most part.

    // Your templates
    <script id="sidebar" type="text/template">
    
        <div class="sidebar">
        <div class="sidebar-title">Mailboxes</div>
            // Mailbox here
            // Mailbox here
            // Labels  here
        </div>
    
    </script>
    
    // I altered this template slightly just to illustrate a point on appending the model
    // view data into your mailbox view. You might not want this but it's just for
    // demonstration purposes
    <script id="mailbox" type="text/template">
        <div class="sidebar-subtitle">Gmail</div>
        <div class="mailboxes" data-account-id="1">
            <ul class="inbox"></ul>  // Changed this
            <ul class="sent"></ul>
        </div>
    </script>
    
    <script id="labelBox" type="text/template">
        <div class="sidebar-title">Labels</div>
            <div class="sidebar-labels">
                <ul>
                    <li>Home</li>
                    <li>Todo</li>
                </ul>
            </div>
        </div>
    </script>
    

    Your application BIG view, main view that encompasses all.

    AppView = Backbone.View.extend({
        initialize: function() {
            // You might not instantiate your collections here but it is a good starting
            // point for this demo. We'll be passing these through children view and 
            // utilizing them in the appropriate views.
    
            // Let's assume these collections we instantiate already have models inside them.
    
            // Your 3 Collections
            this.gmail = new MailCollection();
            this.yahoo = new MailCollection();
            this.labels = new LabelCollection();
    
        },
        render: function() {
            this.$el.html();
    
            // We pass each collection into the SidebarView [options] hash. We can "pick-up"
            // these collections in the sidebar view via this.options.xxx
    
            // Render the sidebar
            var sidebar = new SidebarView({
                'gmail':this.gmail,
                'yahoo':this.yahoo,
                'labels':this.labels
            });
    
            // Render the sidebar and place it in the AppView... anywhere really
            // but for simplicity I just append it to the AppView $el
            this.$el.append(sidebar.render().el);
    
            return this;
        }
    });
    

    Your sidebar view:

    SidebarView = Backbone.View.extend({
        template: _.template($('#sidebar').html()),
        initialize: function() {
    
            // We passed the 3 collections into Sidebar view and can access
            // them through the options. We could have instantiated them here
            // but I passed them into the side to illustrate that using
            // options for this kind of thing is a-okay.
    
            this.gmail = this.options.gmail,
            this.yahoo = this.options.yahoo,
            this.labels = this.options.labels
    
            // This is an array of sub-view mailboxes so when we close this view,
            // it's easy to loop through the subviews and close both mailboxes
            this.mailboxes = [];
        },
        render: function() {
            // We render the sidebarView using the template
            this.$el.html(this.template());
    
            // We generate the sub-view mailbox views (gmail and yahoo)
            var gmailView = new MailboxView({
                'collection': this.gmail // We pass in only the gmail collection
            });
            var yahooView = new MailboxView({
                'collection': this.yahoo // Pass in the yahoo mail collection
            });
            var labelView = new LabelboxView({
                'collection': this.labels // Pass in the labels collection
            });
    
            // We push the views into our array
            this.mailboxes.push(gmailView);
            this.mailboxes.push(yahooView);
            this.mailboxes.push(labelView);
    
            // Render each view and attach it to this sidebar view
            this.$el.append(gmailView.render().el);
            this.$el.append(yahooView.render().el);
            this.$el.append(labelView.render().el);
    
            return this;
        },
        onClose: function() {
            // Sample code of how we close out child views. When this parent view closes,
            // it automatically cleans up the child views.
    
            _.each(this.mailboxes, function(view) {
                view.close(); // A special function that I use to close views
            });
        }
    });
    

    See Zombie View Cleanup for more details on the onClose() and close() methods I use. Particularly helpful once you start creating lots of views / sub-view relationships in your apps.

    Your Mailbox View:

    MailboxView = Backbone.View.extend({
        template: _.template($('#mailbox').html()),
        initialize: function() {
            // When we pass something in as 'collection' backbone automatically
            // attaches it as a property of the view so you can use the collection
            // as this.collection rather than this.options.collection for
            // convenience
        },
        render: function() {
            this.$el.html(this.template());
    
            this.loadMail();
    
            return this;
        },
        loadMail: function() {
            // Some code that loops through your collection and adds the mail to an
            // appropriate DOM element. Would make sense to make Inbox a ul and each
            // mail an li so I altered your template to demonstrate this
    
            // Let's assume a mail model has the attr 'author' and 'body' in this
            // simple example
    
            // This will loop through your collection and use a mail template to
            // populate your list with the appropriate data.
            this.collection.each(function(mail) {
                this.$('ul').append(_.template('<li><%=author%><%=body%></li>', mail.toJSON()))
            });
    
            // Alternatively, you could (and probably should) make each mail model
            // represented by ANOTHER sub-view, a mail subview that has all the
            // functionality that mail views usually have.
            // To accomplish this you just repeat the prior demonstrated cycle
            // of creating child view inside child view, passing in the appropriate
            // data that you need, creating the view, and attaching it to the parent
            // where you would like.
        }
    });
    

    Your LabelsView:

    LabelboxView = Backbone.View.extend({
        template: _.template($('#labelBox').html()),
        initialize: function() {
            // I think you get the gist from above, you'd do the same thing as each
            // mailbox view except deal with the unique aspects of labels, whatever
            // these may be (and do)
        },
        render: function() {
            this.$el.html(this.template());
            return this;
        }
    });