Server-side actions

Once we've filled a Model class with data, we might not want to lose that data, and that's where the AJAX features of Model come into play. Every Model has three methods to interact with the server, which can be used to generate four types of HTTP requests, as shown in the following table:

Method

RESTful URL

HTTP method

Server action

fetch

/books/id

GET

retrieves data

save (for a new Model)

/books

PUT

sends data

save (for an existing Model)

/books/id

POST

sends data

destroy

/books/id

DELETE

deletes data

The sample URLs in the preceding table are what Backbone will generate by default when it tries to perform any of the three AJAX methods. Backbone works best with a set of server-side APIs that are organized using this RESTful architecture. The basic idea of a RESTful server-side API is that it is made up of URL endpoints that expose various resources for the client to consume.

The different HTTP methods are used in such an architecture to control which action the server should take with that resource. Thus, to delete a book with an ID of 7, a RESTful API will expect a DELETE request to /books/7.

Of course, Backbone won't know that the server-side endpoint for a Book Model will be /books unless you tell it, which you can do by specifying a urlRoot property:

var Book = new Backbone.Model.extend({
    urlRoot: '/books'
});
new Book().save(); // will initiate a PUT request to "/books"

If, for some reason, your server-side API is on a different domain, you can also use urlRoot method to specify an entirely different domain. For instance, to indicate that our Book class should use the papers endpoint of http://www.example.com/, we can set urlRoot of http://www.example.com/papers. This approach will not work on most sites, however, because of cross-site scripting limitations imposed by browsers; so, unless you employ special techniques (for instance, a server-side proxy), you will most likely want your server-side API to be on the same domain that serves your JavaScript files.

If your server-side API isn't RESTful, you can still use Backbone's AJAX capabilities by overwriting its built-in url method. While the default version of this method simply combines the Model's urlRoot method with its ID (if any), you can redefine it to specify any URL that you'd prefer Backbone to use.

For instance, let's say our book Models have a fiction attribute and we want to use two different endpoints to save our books: /fiction for fiction books and /nonfiction for nonfiction books. We can override our url method to tell Backbone as much, as shown here:

var Book = Backbone.extend({
    url: function() {
        if (this.get('fiction')) {
            return '/fiction;
        } else {
            return  '/nonfiction';
        }
    }
});
var theHobbit = new Book({fiction: true});
alert(theHobbit.url()); // alerts "/fiction"

Storing URLs on the client

As we just covered, Backbone allows you to specify any URLs you want for your Models. If your site is fairly complex, however, it's a good idea to store the actual URL strings, or even URL-generating functions, in a separate urls object. Take an example of the following code snippet:

var urls = {
    books: function() {
        return this.get('fiction') ? '/fiction' : '/nonfiction';
    },
    magazines: '/magazines'
};
var Book = Backbone.Model.extend({url:  urls.books});
var Magazine = Backbone.Model.extend({urlRoot:  urls.magazines});

While this is not strictly necessary (and is may be overkill on smaller sites), this approach has several benefits on a larger site:

  • You can easily share URLs between any of your Models
  • You easily share URLs between your Models and non-Backbone AJAX calls
  • Finding existing URLs is quick and easy
  • If any URL changes, you only have to edit one file

Identification

Up until now, we've avoided the question of how exactly Backbone determines what a Model's ID is. As it turns out, Backbone has a very simple mechanism: it uses whatever attribute you specify as the idAttribute property of the Model class. The default idAttribute property is simply id; so, if the JSON returned by your server includes an id attribute, you don't even need to specify an idAttribute property. However, since many APIs don't use id (for example, some use _id instead), Backbone watches for changes to its attributes, and when it sees an attribute that matches the idAttribute property, it will set the Model's special id property to that attribute's value, as shown here:

var Book = Backbone.Model.extend({idAttribute: 'deweyDecimalNumber'})
var warAndPeace = new Book({{deweyDecimalNumber: '082 s 891.73/3'});
warAndPeace.get('deweyDecimalNumber'), // '082 s 891.73/3'
warAndPeace.id; // also '082 s 891.73/3'

In addition to telling Backbone what URL to use when saving the Model, the ID attribute also has another function: its absence tells Backbone that the Model is new, which you can see if you use the isNew method:

var warAndPeace = new Book({deweyDecimalNumber: '082 s 891.73/3'});
var fiftyShades = new Book();
warAndPeace.isNew(); // false
fiftyShades.isNew(); // true

This method can also be overridden if you need to use some other mechanism to determine whether a Model class is new or not. There's one other issue with isNew though: if new Models don't have IDs, how will you identify them? For instance, if you were storing a set of Models by ID, what will you do with the new ones?

var warAndPeace = new Backbone.Model({{id: 55});
var shades = new Backbone.Model();
var bookGroup = {};
bookGroup[warAndPeace.id] =  warAndPeace; // bookGroup = {55: warAndPeace}
bookGroup[shades.id] = shades; // doesn't work because shades.id is undefined

To get around this problem, Backbone provides a special client-side only ID, or cid property, on every Model. This cid property is guaranteed to be unique, but has no connection whatsoever with the Model's actual ID (if it even has one). It is also not guaranteed to be consistent; if you refresh the page, your Models' might have entirely different cid properties generated for them.

The presence of a cid property allows you to use a Model's ID for all server-related tasks and its cid for all client-side tasks, without the need to get a new ID from the server every time you create a new Model. By using the cid property, we can solve our previous problem and successfully index new books:

var bookGroup = {};
bookGroup[warAndPeace.cid] = warAndPeace; // bookGroup = {c1: warAndPeace}
bookGroup[fiftyShades .cid] = fiftyShades;
// bookGroup = {c1: warAndPeace, c2: fiftyShades};

Fetching data from the server

The first of Backbone's three AJAX methods, fetch, is used to retrieve the data of a Model from the server. The fetch method doesn't require any arguments, but it takes a single optional options argument. This argument can take Backbone-specific options, such as silent (which will prevent the Model from triggering an event as a result of fetch), or any options that you will normally pass to $.ajax, such as async.

You can specify what to do when the fetch request is finished in one of the two ways. First, you can specify a success or error callback in the options passed to fetch:

var book = new Book({id: 55});
book.fetch({
    success: function() {
        alert('the fetch completed successfully'),
    },
    error: function() {
        alert('an error occurred during the fetch'),
    }
});

The fetch method also returns a jQuery promise, which is an object that lets you say when the fetch is done, do ___ or if the fetch fails, do ___. We can use promises instead of a success callback to trigger logic after the AJAX operation finishes, and this approach even lets us chain multiple callbacks together:

var promise = book.fetch().done(function() {
        alert('the fetch completed successfully'),
}).fail(function() {
        alert('an error occurred during the fetch'),
});

While both approaches work, the promise style is slightly more powerful because it allows you to easily trigger multiple callbacks from a single fetch or trigger a callback only after multiple fetch calls complete. For instance, let's say we wanted to fetch two different Models and display an alert only after they've both been returned from the server. Using the success/error approach, this can be awkward, but with the promise style (combined with jQuery's when function), the problem is simple to solve:

var warAndPeace = new Backbone.Model({{id: 55});
var fiftyShades = new Backbone.Model({id: 56});
var warAndPeacePromise = warAndPeace.fetch();
var fiftyShadesPromise = fiftyShades.fetch();
$.when( warAndPeacePromise, fiftyShadesPromise).then(function() {
    alert('Both books have now been successfully fetched!'),
});

Once a fetch method is complete, Backbone will take the server's response, assume that it's a JSON object representing the Model's attributes, and call set on that object. In other words, fetch is really just a GET request followed by a set of whatever comes back in the response. The fetch method is designed to work with a RESTful-style API that returns just the Model's JSON, so if your server returns something else, such as that same JSON wrapped inside an envelope object, you'll need to override a special Model method called parse.

When a fetch method finishes, Backbone passes the server's response through parse before it calls set, and the default implementation of the parse method simply returns the object given to it without modification. By overriding the parse method with your own logic, however, you can tell Backbone how to convert this response from whatever format the server sent it in into a Backbone attributes object.

For instance, let's say that instead of returning the data for a book directly, your server returned an object with the book's attributes contained inside a book key, as shown here:

{
    book: {
        pages: 300,
        name: 'The Hobbit'
    },
    otherInfo: 'stuff we don't care about'
}

By overriding the parse method of our Book class, we can tell Backbone to throw out the otherInfo and just use the Book property as our book's attribute:

var Book = Backbone.Model.extend({
    parse: function(response) {
        return response.pages; // Backbone will call this.set(response.pages);
    }
});

Saving data to the server

Once you've started creating Models, you'll no doubt find yourself wanting to save their data to your remote server, and that's where Backbone's save method comes in. As its name suggests, save allows you to send your Model's attributes to your server. It does so in the JSON format, at the URL specified by the url method of your Model, via an AJAX POST or PUT request. Like fetch, save allows you to provide success and error callback options and (also like fetch) it returns a promise, which can be used to trigger code once the save method is completed.

Here's an example of save used with a promise-based callback:

var book = new Book({
    pages: 20,
    title: 'Ideas for Great Book Titles'
});
book.save().done(function(response) {
    alert(response); // alerts the the response's JSON
});

The preceding code reveals another problem: it will only work if our server is set up to receive all of the Model's attributes in the JSON format. If we want to save only some of those attributes or if we want to send other JSON, such as a wrapping envelope object, Backbone's save method won't work out of the box. Fortunately, Backbone provides a solution in the form of its toJSON method. When you save, Backbone passes your Models attributes through toJSON, which (like parse) normally does nothing, because by default toJSON simply returns whatever is passed in to it.

However, by overriding toJSON, you can gain complete control over what Backbone sends to your server. For instance, if we wanted to wrap our book JSON inside another object as a book property and then add some further information, we can override toJSON as follows:

var Book = Backbone.Model.extend({
    toJSON: function(originalJson) {
        return {
            data:  originalJson,
            otherInfo: 'stuff'
        };
    }
});
var book = new Book({pages: 100);
book.save(); // will send: {book: {pages: 100}, otherInfo: 'stuff'}

Validation

Before you send anything to the server, it's a good idea to make sure that the data you're sending is actually valid. To solve this problem, Backbone provides another method for you to optionally overwrite: validate. The validate method is called every time you save, but its default implementation simply returns true. If you overwrite it, however, you can add whatever validation logic you want, and if that logic returns false, then the whole save operation will fail and also return false. For instance, let's say we wanted to ensure that every new book has a minimum of 10 pages:

var Book = Backbone.Model.extend({
    validate: function(attributes) {
        var isValid = this.get('pages') >= 10;
        return isValid;
    }
});
var tooShort = new Book({pages: 5});
var wasAbleToSave = tooShort.save(); // == false

Note

Note that if the validation fails, Backbone will not even return a promise; it will just return false.

As a result, if you add validation logic to your Model class, you will need to test for a failed validation case separately every time you save; you can't simply rely on the fail method of the returned promise since no such promise will be returned. In other words, the following won't work (and will cause an error since false has no fail method):

tooShort.save().fail(function() {
    // this code will never be reached
});

Instead, you should use the following code snippet:

var savePromise = tooShort.save();
if (savePromise) {
    savePromise.done(function() {
        // this code will be reached if both the validation and AJAX call succeed
    }).fail(function() {
        // this code will be reached if the validation passes but the AJAX fails
    });
} else {
     // this code will be reached if the validation fails
}

Note

Note that you can also check the validity of your Models at any time by using the isValid method, which will only return the validation result (and not save).

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

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