Handling persisted data

To do any interesting work, a web application usually needs to interact with a web server. Historically, this would have been managed by using jQuery's $.ajax function, and storing the result in a JavaScript object. As our applications grow in size, this starts to become more and more difficult to manage unless we introduce a structure.

Another drawback of this method is that updating our UI in response to data changes gets very complex very quickly. Take an application like Evernote - there are usually two versions of a note: the one in the list and the one you're currently editing. How do you easily keep both versions in sync? If we want to get an updated version of the note from our server, how can we easily make sure both versions are updated simultaneously?

Backbone gives us the Model and Collection classes for just this purpose. In short, the Model represents a single object/resource/record with attributes that can be synchronized with a server. A Collection is simply a list of Models, with extra helper methods, that can also be synchronized with our server. With these two classes, it becomes very easy to simply list a Collection, change the individual Model instances, and have those changes simultaneously propagated across different sections of your application.

Sound too easy? Let's look at how to make it all happen.

Models

We'll start by looking at the Backbone.Model and how to use it. Let's create a simple note with a timestamp, content, and title.

var Note = Backbone.Model.extend({
  defaults: {
    timestamp: '',
    content: '',
    title: ''
  }
});

var note = new Note({
  timestamp: '2015-09-02 11:00:00',
  content: "I'm writing a book!",
  title: 'Doing something'
});

Now we have our note, we can read its fields:

console.log(note.get('content'));
// Doing something

If we have some new data to put in this note, we can update it like so:

note.set('content', 'New content');

Or, if we wanted to update multiple fields at a time:

note.set({
  content: 'New content',
  title: 'Updated title too'
});

Besides brevity, there's a very good reason we'd want to update multiple fields at once, as we'll get to later.

There's no reason we have to stick to the fields defined by defaults (we don't even need to define them), so we can add some ad-hoc data:

note.set('reminder', '2015-09-04 11:01:00');

Server synchronization

This is all well and good but it still takes us no closer to pushing and pulling data to a server. First, we'll need to define a pretend server. Let's give it the URL http://example.com/note/1 which, when we GET it, returns:

{
  "id": 1,
  "title": "My note",
  "content": "Some content saved online",
  "timestamp": "2015-09-02 11:01:02",
  "reminder": null
}

First, we'll define a new type of Note:

var Note = Backbone.Model.extend({
  urlRoot: 'http://example.com/note/'
});

var note = new Note({
  id: 1
});
note.fetch();

The Model.fetch method knows how to construct a URL from its urlRoot and id properties - namely appending id to urlRoot. Like most web calls in JavaScript, fetch is asynchronous - execution will continue before the web request completes.

If we wanted to perform an action on the data once the fetch method returns, we can attach a success callback, as in jQuery:

note.fetch({
  success: function(response, model) {
    // Do something
  }
});

When we're done modifying our data and want to save it, we'll call Model.save() and Backbone will save the data back to the server:

note.set('title', 'New title');
note.save();

We could also modify data and save in one call:

note.save({
  title: 'New title'
});

Again, like fetch, we can use the success callback to execute based on the result of the call:

note.save(
  {
    title: 'New title'
  },
  {
    success: function(response, model) {
    // Do something
  }
);

This dependency on the success and error callbacks doesn't help us when our model is attached to multiple views - what happens if one view updates the model but another one needs to be changed? Do both views need to know about each other? Let's find out.

Events

Models use events to signal that something has happened that another object may be interested in. For example, a model can signal that fields have changed, it has successfully saved its data (or failed), or that it has fetched a new set of data from the server. These events can then be listened to by views, or any other object that knows about the model. Using this, our models can affect multiple parts of an application without needing to be explicitly told about them. Let's look at some examples:

note.set('title', 'New title');
// Fires the 'change' and 'change:title' events

note.set('content', 'New content');
// Fires another 'change' event and 'change:content' event

note.set({
  content: 'Newer content',
  title: 'Newer title'
});
// Fires only one 'change' event, 'change:content', and 'change:title'

note.save();
// Fires the 'request' event, then either 'sync' or 'error' depending on the
// server response

note.fetch();
// Like save, fires `request`, then `sync` or `error` depending on the response

A full, up-to-date, list of events can always be found on the Backbone documentation, with a description of when each event fires. As we can see in the example, change is fired every time we successfully set a field - if we want to only fire a single change event, we must pass an object into set with all the fields we want to update.

Listening to Events

For these events to be useful, we need to attach listeners that act when the event is fired. When the title is updated, let's listen for it like so:

var titleUpdated = function(model, value) {
  console.log('title is now ' + value);
};

note.on('change:title', titleUpdated);
note.set('title', 'Changed Title');
// Outputs 'title is now Changed Title'

We can also listen to events from our views by using the modelEvents attribute. When defining our view:

var NoteView = Marionette.LayoutView.extend({
  modelEvents: {
    'change:title': 'updateTitle'
  },

  updateTitle: function(model, value, options) {
    console.log('title for NoteView is now ' + value);
  }
});

This will trigger the updateTitle method on our NoteView whenever title changes.

Custom Events

When you start building your apps, you'll notice that Backbone doesn't always give you the events you need. As a common example, there's no way to distinguish between a successful save and a successful data pull - sync covers both cases.

Luckily, we can fire custom events on models, and Marionette views are capable of binding to them. Let's see an example of this now:

var MyView = Marionette.LayoutView.extend({
  modelEvents: {
    saved: 'saveComplete'
  },

  triggers: {
    'click .save-button': 'save:note'
  },

  onSaveNote: function() {
    this.model.save(
      {
        content: 'New content',
        title: 'New title'
      },
      {
        success: function() {
          note.trigger('save', note);
        }
      }
    );
  },

  saveComplete: function(model) {
    console.log('Note saved');
  }
});

Now, when save succeeds, our saveComplete method gets called and Note saved makes it to the log. Whilst useful, this example has a flaw in that only NoteView will trigger the saved event, and so it's not much better than just executing the code in success directly. This could be acceptable if our save is only called in this view - other views can still happily listen to the saved event, even though they don't fire it.

Collections

Managing a single model is good, and we can do a lot of interesting things with just this knowledge. When it comes to building applications, we will normally operate on collections of data to render lists, draw charts, and otherwise aggregate data.

The Backbone.Collection class is used to model and act on multiple models at the same time. Let's take our note example and see how we could build up a list of notes that we'd like to draw later:

var NoteCollection = Backbone.Collection.extend({
});

var noteList = new NoteCollection([
  new Note({title: 'Note1', content: 'Content1'}),
  new Note({title: 'Note2', content: 'Content2'})
]);

This will store the list of notes. After creating our collection, we can add new notes using the add method, like so:

noteList.add(new Note({title: 'Note3', content: 'Content3'}));

Since we only have a single type in our list, let's set this constraint in the definition:

var NoteCollection = Backbone.Collection.extend({
  model: Note
});

var noteList = new NoteCollection([
  {title: 'Note1', content: 'Content1'},
  {title: 'Note2', content: 'Content2'}
]);

noteList.add({title: 'Note3', content: 'Content3'});

Now Backbone will convert our raw JavaScript objects into Note objects, both on creation and when adding new objects. It will also enforce this:

var NotANote = Backbone.Model.extend();

noteList.add(new NotANote({something: 'Some Value'}));
// ERROR - This is not a Note object

Synchronizing data

Collections are used to pull lists of data from our server and build an abstraction we can operate on. Assuming that our note-taking app has a server endpoint /note/ that returns a JSON list in the form:

[
  {
    "content": "Note Content",
    "title": "Note title",
    "reminder": null,
    "timestamp": "2015-09-01 12:01:00",
    "id": 1
  },
  {
    "content": "Another note with some content",
    "title": "Note title2",
    "reminder": "2015-09-02 09:00:00",
    "timestamp": "2015-09-01 12:51:00",
    "id": 2
  },
  {
    "content": "Yet another note",
    "title": "Note title 3",
    "reminder": null,
    "timestamp": "2015-02-01 12:01:00",
    "id": 3
  }
]

Now let's define our collection that references the URL endpoint:

var NoteCollection = Backbone.Collection.extend({
  url: '/note/',
  model: Note
});

var noteList = new NoteCollection();
noteList.fetch();

Like in Model, we use the fetch method to pull our data from the server and insert it into our collection.