Models, collections & data

Although models may reflect logic that’s on the server, they’re not meant to mirror all of it. Replicating every relation and validation that exists on the other side is time wasted. It may come down to that, but there’ll be a use case on the client app to justify it, or it’s useless.

★ ★ ★

Don’t clutter models with a lot of methods for rendering attributes in different ways, human-friendly, different date formats, and so on. Write a presenter object that's separate from the model, which you can pass the model to it's methods and it’ll handle that for you. Then pass that presenter to the view (more on that in the views chapter).

★ ★ ★

Views will listen to attribute changes that happen on your models, so plan accordingly as you'll be listening to changes in specific attributes. For example, it’s fine having an attribute in Book called paid that’s stale, as in it exists only on the client side (the server knows it’s paid because there's an associated payment entry). So just setting paid to true re-renders whatever view associated itself with this model and renders an appropriate template that shows that this thing is paid for.

Before you cry that Backbone will always send every model attribute to the server when you save(), the solution is simple: declare a list of stale attributes (they're likely gonna be lesser in number than the others), and omit them from what gets sent by overriding toJSON():

class Book extends Backbone.Model
  stale: ['paid']

  toJSON: ->
    _.omit @attributes, @stale
var Book = Backbone.Model.extend({
  stale: ['paid'],

  toJSON: function() {
    return _.omit(this.attributes, this.stale);
  }
});
★ ★ ★

Most apps communicate with a server using HTTP with a RESTful API. So the API URLs are of summary importance. Don't scatter them around the app. Write a module named apiUrl which exports a function with the same name. That module acts like a catalog of URLs, and model or collection that talks with the API has to require it. It looks like this:

# List of API URLs.
URLs =
  books: ->
    "/api/books"
  book: (id) ->
    "/api/books/#{id}"
  subscriptions: (userId, id) ->
    "/api/users/#{userId}/subscriptions/#{id}"

# Helper for accessing the URL list. Think of it as something similar
# to Rails' URL helpers.
apiUrl = (type, args...) ->
  URLs[type]? args...
// List of API URLs.
var URLs = {
  books: function() {
    return "/api/books";
  },
  book: function(id) {
    return "/api/books/"+ id;
  },
  subscriptions: function(userId, id) {
    return "/api/users/"+ userId +"/subscriptions/" + id;
  }
}

// Helper for accessing the URL list. Think of it as something similar
// to Rails' URL helpers.
var apiUrl = function(type) {
  return URLs[type] ?
    URLs[type].apply(this, [].slice.call(arguments, 1)) :
    undefined;
}

Then forever (no exceptions) refer to URLs by calling this module. In a Book model, it’d look like this:

class Book extends Backbone.Model
  url: ->
    apiUrl 'book', @id
var Book = Backbone.Model.extend({
  url: function() {
    return apiUrl('book', this.id);
  }
});
★ ★ ★

Data associations are easy to solve without plugins if the server API is sane, so it’s worth making it so otherwise your data layer will absorb your API’s ugliness and amplify it by one hundred. Assume we have authors for those books I spoke of, and when fetching an author, I want to grab their books along. Say the API looks like this:

GET   /api/authors/1?with=books
GET   /api/authors/1?with=books

And by calling this URL I get the author object along with a books attribute which is an array of book objects, here’s how you’d do it:

class Author extends Backbone.Model
  initialize: ->
    @books = new Books @get('books'), url: apiUrl('author books', @id)
var Author = Backbone.Model.extend({
  initialize: function() {
    this.books = new Books(
      this.get('books'), { url: apiUrl('author books', this.id) }
    );
  }
});

Now make sure from now on you refer to the collection (author.books) to access the values and not the container model attribute (author.get 'books'). The collection will contain the representation of what the author’s books are, and will ensure the book model instances are in sync with the server as you add, remove, or edit them.

★ ★ ★

Loading associated data (one to many) is trivial once you think for a second about what makes sense. Assume you have this URL in your app:

/authors/:id/books
/authors/:id/books

You need to grab the author first and then his/her books. This could go one of two ways: you have loaded the author before, and thus it’s in memory, or you need to grab it from the server and then grab the books.

Before you OCD about performance and think you should do this in one request, consider you’re potentially creating an endpoint that will send down a huge amount of information. Correct use of caching, specifically ETags, will mitigate these problems by a lot.

In the method that’s called for that route, here’s how you’ll do it.

showBooks: (authorId) ->
  author = App.Authors.get authorId
  if author?
    author.books.fetch success: =>
      view = new BooksList collection: author.books
        # Render the view.
  else
    author = new Author id: authorId
    author.fetch success: =>
      App.Authors.add author
      author.books.fetch success: =>
        # Render the view.
showBooks: function(authorId) {
  var author = App.Authors.get(authorId);
  if (author) {
    author.books.fetch({
      success: function() {
        var view = new BooksList({collection: author.books});
        // Render the view.
      }
    );
  } else {
    author = new Author({id: authorId});
    author.fetch({
      success: function() {
        App.Authors.add(author);
        author.books.fetch({
          success: function() {
            // Render the view.
          }
        });
      }
    });
  }
}

I can hear you having a heart attack over code repetition. But for starters, having the whole code that describes what happens when you visit each URL in the router itself is a terrible idea. I’ll explain this refactor further ahead in the Routing & controllers chapter.

★ ★ ★

The best way to handle pagination is by using link headers (GitHub’s API uses it). To be clear: pagination doesn’t rely on your necessarily having pages of information. You could use this for infinite scrolling, or that. How you present it goes beyond what your collection needs to worry about.

Integrating this with a Backbone collection is trivial (I’m using this to parse it):

class PaginatedCollection extends Backbone.Collection
  link: {}

  parse: (response, options) ->
    link = options.xhr.getResponseHeader 'Link'
    @link = parseLinks link
    super
var PaginatedCollection = Backbone.Collection.extend({
  link: {},

  parse: function(response, options) {
    var link = options.xhr.getResponseHeader('Link');
    this.link = parseLinks(link);
    return Backbone.Collection.prototype.parse.call(this, arguments);
  }
});

Obviously, collections that need pagination should inherit from PaginatedCollection. From here on, you can check for link.last?, link.next? and so on in collection instances to determine whether there’s more data to be retrieved or not.

And since this header is emitted in every request, the collection object is kept up to date everytime it interacts with the server.

★ ★ ★

The concept of a full/partial model is handy for cases where the server API emits “light” representations of models. Like in the case above, getting a list of books for an author, it’s quite likely that you’ll want to render a list and then when a user clicks an entry, you fetch and show the full book.

Which means you don’t necessarily want a request to return the complete book objects if they’ll be large and numerous (e.g.: containing a summary, comments, etc).

# Partial model
# =============
#
# An implementation for partial models. A partial model is a model
# that's considered incomplete until it has all of it's attributes set
# to some value. You can alternatively specify which attributes need
# to be set for it to be complete.

class PartialModel extends Backbone.Model
  @completeIfHas: []

  isPartial: ->
    not _.every(
      _.map(@completeIfHas, (attr) => @attributes[attr]), _.isBlank
    )
// Partial model
// =============
//
// An implementation for partial models. A partial model is a model
// that's considered incomplete until it has all of it's attributes set
// to some value. You can alternatively specify which attributes need
// to be set for it to be complete.

var PartialModel = Backbone.Model.extend({
  @completeIfHas: [],

  isPartial: function() {
    return !_.every(
      _.map(
        this.completeIfHas,
        function(attr) { return this.attributes[attr]; }
      ),
      _.isBlank
    );
  }
});

In the router, when fetching the model for rendering, you'd do:

showBook: (bookId) ->
  book = App.Books.get bookId
  if book? and not book.isPartial()
    # Just render the book.
  else
    # Fetch and then render it.
showBook: function(bookId) {
  var book = App.Books.get(bookId);
  if (book && !book.isPartial()) {
    // Just render the book.
  } else {
    // Fetch and then render it.
  }
}
★ ★ ★

Where and when you instantiate your collections has to be consistent with the data strategy you conceive. It is popular to instantiate a collection, fetch data for it, pass it to a view and render it for each route. But this pattern leaves out something you'll want 90% of the time, which is not discarding data you already have.

In the context of the previous example: the user visits the URL for a book, goes back to the list, visits a different book. Going back to the first book (say the user wants to double check something) needs to happen instantly. This doesn't mean you can't have the book model fetch data from the server everytime the URL is visited anyway, in case what's in the collection has gone stale, but it's worth rendering something right away. You can then re-render the book page as soon as fetch() succeeds.

So say this app is about browsing authors and their books. Just instantiate and keep a “global” collection of authors around. Data always gets added to it as the user browses the app, similar to the previous examples where I referred to App.Authors. Pass this collection around either via a global or by requiring it in each subsequent module.

Because every URL will always query whether the global collection contains an author and attempt to fetch it if not, you can keep the concern of flushing it to avoid reaching a point where you'll have thousands of authors in memory within the collection itself, which is ideal. You could do this maybe by ensuring the collection API endpoint supplies an ETag header that when changed, the next time the collection calls fetch() you pass { remove: true, reset: true }. Or really query how many objects the collection has.

★ ★ ★

Backbone Collections are really smart in how they handle data. By default, they won't allow duplicate IDs, and if you re-add an existing model, you can pass an option to add() which will get the collection to merge the attributes with the current model living in the collection.

If you're wondering why that matters, think of this: you can keep models synchronous using HTTP and WebSockets/Server-side events simultaneously. So long as you make it so that every message coming from the server uses the collection methods (add(), remove()), you can literally add real-time to an app on top of an existing HTTP/REST API without interfering with it at all, and enable or disable the functionality without the app breaking.

C J