Representing Data in Backbone Models and Collections

A model, in the Backbone sense, is an entity that serves as a key-value store where changes can be observed via event listeners. Additionally, Backbone models inherit methods for syncing their data with a remote server. That’s all there is to it, and yet models are the heart of Backbone. Being able to “listen” for changes to a set of data is incredibly powerful: change events can tell us when we need to re-render a view, display a message, or fetch additional data. Before Backbone, most JavaScript applications had no model layer. They performed actions in direct response to user input events, making it very difficult to maintain a consistent application state. Separating out the view and model layers was an enormous leap forward for client-side application development, and it’s fair to say that Backbone was pivotal in popularizing this approach.

Backbone also defines collections, ordered sets of models that can also be observed via event listeners. When we load our data from the server (or, for this chapter, from localStorage), we’re going to load each of the three arrays into a corresponding collection: one for boards, one for columns, and one for cards. We’ll start with the cards, because they contain no references to other types. Here is our card model class and the corresponding collection class, both of which we’re attaching to window as we define them so that they’re visible to other modules:

 class​ window.Card ​extends​ Backbone.Model
 class​ window.CardCollection ​extends​ Backbone.Collection
  model: Card

Simple? Very. We haven’t added any functionality to the underlying Backbone.Model and Backbone.Collection classes. In a normal application, we’d at least add a url property to point to a RESTful API endpoint. But that’s a feat we’ll save for the next chapter. Instead, we’re going to modify Backbone to persist data with localStorage. To do that, we’ll override Backbone.sync, which provides the persistence functionality underlying every model and collection’s save and fetch methods. More on that in a bit.

Before we can load our columns, we need to load our cards so that each column’s cardIds have cards to point to. So we’ll define a global collection and load all of the cards in localStorage into it:

 cardData = JSON.parse(localStorage.cards)
 window.allCards = ​new​ CardCollection(cardData, {parse: true})

Now on to the Column model. Because of cardIds, this is going to be slightly more complicated than the Card model. We’re going to implement parse and toJSON methods, which Backbone uses to convert raw JSON data into the model’s attributes and vice versa:

 class​ window.Column ​extends​ Backbone.Model
  defaults:
  name: ​'New Column'
 
  parse: (data) ->
  attrs = _.omit data, ​'cardIds'
 
 # Convert the raw cardIds array into a collection
  attrs.cards = @get(​'cards'​) ? ​new​ window.CardCollection
  attrs.cards.reset(
 for​ cardId ​in​ data.cardIds or []
  window.allCards.get(cardId)
  )
 
  attrs
 
  toJSON: ->
  data = _.omit @attributes, ​'cards'
 
 # Convert the cards collection into a cardIds array
  data.cardIds = @get(​'cards'​).pluck ​'id'
 
  data

Let’s break down the parse method:

First, we’re using Underscore’s _.omit method to make a shallow copy of the raw JSON data that exclude cardIds. When this copy is returned by parse, it’s going to be used as the model’s attributes. We’re excluding cardIds because we don’t want that array to become a part of the model; we want the cards referenced by that array instead. Having both would cause unnecessary data duplication and possible inconsistencies.

Second, we’re creating a CardCollection that contains the models returned by a list comprehension. The list comprehension goes through cardIds and, for each unique ID, gets the card with that id from allCards.

The toJSON method simply does the opposite of what parse did, extracting the IDs from the column’s CardCollection. The data we return from toJSON will be persisted to localStorage, perhaps to be passed into some new column’s parse method one day.

We don’t have to do any work for ColumnCollection. That’s because when we instantiate a Backbone collection with a bunch of raw data, it automatically creates new models and calls the parse method on each one:

 class​ window.ColumnCollection ​extends​ Backbone.Collection
  model: Column

Now we can load all our columns from localStorage, just as we did with allCards:

 columnData = JSON.parse(localStorage.columns)
 window.allColumns = ​new​ ColumnCollection(columnData, {parse: true})

The only model left to define is a board, which we define in much the same way that we defined a column:

 class​ window.Board ​extends​ Backbone.Model
  defaults:
  name: ​'New Board'
 
  parse: (data) ->
  attrs = _.omit data, ​'columnIds'
 
 # Convert the raw columnIds array into a collection
  attrs.columns = @get(​'columns'​) ? ​new​ window.ColumnCollection
  attrs.columns.reset(
 for​ columnId ​in​ data.columnIds or []
  window.allColumns.get(columnId)
  )
 
  attrs
 
  toJSON: ->
  data = _.omit @attributes, ​'columns'
 
 # Convert the columns collection into a columnIds array
  data.columnIds = @get(​'columns'​).pluck ​'id'
 
  data
 class​ window.BoardCollection ​extends​ Backbone.Collection
  model: Board
 boardData = JSON.parse(localStorage.boards)
 window.allBoards = ​new​ BoardCollection(boardData, {parse: true})

Now we have all of our models and collections in place. However, we still need to write the logic needed to sync them with localStorage. That logic is very specific to this application, but I’ll include it here for completeness’ sake:

 Backbone.sync = (method, model, options) ->
 # We only have to handle model syncs (not collection syncs)
 if​ model ​instanceof​ window.Card
  collection = window.allCards
  localStorageKey = ​'cards'
 else​ ​if​ model ​instanceof​ window.Column
  collection = window.allColumns
  localStorageKey =​'columns'
 else​ ​if​ model ​instanceof​ window.Board
  collection = window.allBoards
  localStorageKey = ​'boards'
 
 switch​ method
 when​ ​'get'​ ​# 'get' corresponds to a model.fetch() call
  model.reset collection.get(model.id), {silent: true}
 when​ ​'create'​ ​# 'create' is a model.save() call on a new model
  model.set ​'id'​, collection.length + 1
  collection.add(model)
  localStorage[localStorageKey] = JSON.stringify collection.toJSON()
 when​ ​'update'​ ​# 'update' is a model.save() call on an old model
  localStorage[localStorageKey] = JSON.stringify collection.toJSON()
 
 # Simulate a successful jqXHR
  xhr = options.xhr = jQuery.Deferred().resolve(model.toJSON()).promise()
  options.success(model.toJSON())
  xhr

Don’t worry if you’re not sure how this sync logic works. Normal, server-driven applications don’t usually have to muck with Backbone’s internals this way. We’ll get rid of this code in the next chapter, when we build a server for our application to sync to.

And that’s it! The model layer of our application is complete. We’ve created three models (Card, Column, and Board) with corresponding collections and implemented the logic needed to persist them in the browser’s localStorage. Now we just need to add a view layer that ties our models together with our templates, and we’ll be ready to fire up our application.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.133.122.68