From 302c0d5ceeeafc6db75fde2e95bb236c0f2fc05e Mon Sep 17 00:00:00 2001 From: John Wright Date: Thu, 28 Oct 2010 20:21:59 -0600 Subject: [PATCH 1/2] Added Backbone.emulateJSON to enable the current behavior of syncing and made sending the body as application/json without a wrapping model param, the default --- backbone.js | 43 +++++++++++++++++++++++++------------ index.html | 25 ++++++++++++++++------ test/sync.js | 60 ++++++++++++++++++++++++++++++++++++++++++++++------ 3 files changed, 102 insertions(+), 26 deletions(-) diff --git a/backbone.js b/backbone.js index 61127598..fe72b4b8 100644 --- a/backbone.js +++ b/backbone.js @@ -27,9 +27,14 @@ // For Backbone's purposes, jQuery owns the `$` variable. var $ = this.jQuery; - // Turn on `emulateHttp` to fake `"PUT"` and `"DELETE"` requests via + // Turn on `emulateHTTP` to fake `"PUT"` and `"DELETE"` requests via // the `_method` parameter. - Backbone.emulateHttp = false; + Backbone.emulateHTTP = false; + + // Turn on `emulateJSON` to encode the body as + // `application/x-www-form-urlencoded` instead of `application/json` + // with the model as a JSON string in the param `model` + Backbone.emulateJSON = false; // Backbone.Events // ----------------- @@ -654,25 +659,37 @@ // * Send up the models as XML instead of JSON. // * Persist models via WebSockets instead of Ajax. // - // Turn on `Backbone.emulateHttp` in order to send `PUT` and `DELETE` requests + // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests // as `POST`, with an `_method` parameter containing the true HTTP method. // Useful when interfacing with server-side languages like **PHP** that make - // it difficult to read the body of `PUT` requests. + // it difficult to read the body of `PUT` requests. Also, turn on Backbone.emulateJSON + // to send the body as `application/x-www-form-urlencoded` instead of + // `application/json`, with the model in a JSON string param named `model`, Backbone.sync = function(method, model, success, error) { var sendModel = method === 'create' || method === 'update'; - var data = sendModel ? {model : JSON.stringify(model)} : {}; + var data = sendModel ? model : {}; var type = methodMap[method]; - if (Backbone.emulateHttp && (type === 'PUT' || type === 'DELETE')) { + if (Backbone.emulateHTTP && (type === 'PUT' || type === 'DELETE')) { data._method = type; type = 'POST'; - } + } + + + if (sendModel && Backbone.emulateJSON) data.model = JSON.stringify(model); + if (sendModel && !(Backbone.emulateHTTP || Backbone.emulateJSON)) data = JSON.stringify(data); + + processData = Backbone.emulateJSON ? true : false; + contentType = Backbone.emulateJSON ? "application/x-www-form-urlencoded" : "application/json"; + $.ajax({ - url : getUrl(model), - type : type, - data : data, - dataType : 'json', - success : success, - error : error + url : getUrl(model), + type : type, + data : data, + processData : processData, + contentType : contentType, + dataType : 'json', + success : success, + error : error }); }; diff --git a/index.html b/index.html index a5a592de..36b2955d 100644 --- a/index.html +++ b/index.html @@ -1232,10 +1232,10 @@ var othello = NYPL.create({

With the default implementation, when Backbone.sync sends up a request to save - a model, its attributes will be passed, serialized as JSON, under the model - parameter. When returning a JSON response, send down the attributes of the - model that have been changed by the server, and need to be updated on the - client. When responding to a "read" request from a collection + a model, its attributes will be passed, serialized as JSON, and sent in the HTTP body + with content-type application/json. When returning a JSON response, + send down the attributes of the model that have been changed by the server, and need + to be updated on the client. When responding to a "read" request from a collection (Collection#fetch), send down an array of model attribute objects.

@@ -1255,15 +1255,26 @@ var othello = NYPL.create({ If your web server makes it difficult to work with real PUT and DELETE requests, you may choose to emulate them instead, using HTTP POST, and passing them under the _method parameter - instead, by turning on Backbone.emulateHttp: + instead, by turning on Backbone.emulateHTTP:

-Backbone.emulateHttp = true;
+Backbone.emulateHTTP = true;
 
 model.save();  // Sends a POST to "/collection/id", with "_method=PUT"
 
+

+ Also you can choose to send the body with content-type + application/x-www-form-urlencoded and the model in a JSON + string param called model by turning on Backbone.emulateJSON: +

+ +
+Backbone.emulateJSON = true;
+
+model.save();  //sends data as application/x-www-form-urlencoded
+

As an example, a Rails handler responding to an "update" call from Backbone.sync might look like this: (In real code, never use @@ -1557,7 +1568,7 @@ var DocumentView = Backbone.View.extend({ as an option, which will be invoked if validation fails, overriding the "error" event. You can now tell backbone to use the _method hack instead of HTTP - methods by setting Backbone.emulateHttp = true. + methods by setting Backbone.emulateHTTP = true. Existing Model and Collection data is no longer sent up unnecessarily with GET and DELETE requests. Added a rake lint task. Backbone is now published as an NPM module. diff --git a/test/sync.js b/test/sync.js index 36888b19..bf9dc5a5 100644 --- a/test/sync.js +++ b/test/sync.js @@ -36,10 +36,23 @@ $(document).ready(function() { equals(lastRequest.url, '/library'); equals(lastRequest.type, 'POST'); equals(lastRequest.dataType, 'json'); + var data = JSON.parse(lastRequest.data); + equals(data.title, 'The Tempest'); + equals(data.author, 'Bill Shakespeare'); + equals(data.length, 123); + }); + + test("sync: create with emulateJSON", function() { + Backbone.emulateJSON = true; + library.add(library.create(attrs)); + equals(lastRequest.url, '/library'); + equals(lastRequest.type, 'POST'); + equals(lastRequest.dataType, 'json'); var data = JSON.parse(lastRequest.data.model); equals(data.title, 'The Tempest'); equals(data.author, 'Bill Shakespeare'); equals(data.length, 123); + Backbone.emulateJSON = false; }); test("sync: update", function() { @@ -47,25 +60,53 @@ $(document).ready(function() { equals(lastRequest.url, '/library/1-the-tempest'); equals(lastRequest.type, 'PUT'); equals(lastRequest.dataType, 'json'); - var data = JSON.parse(lastRequest.data.model); + var data = JSON.parse(lastRequest.data); equals(data.id, '1-the-tempest'); equals(data.title, 'The Tempest'); equals(data.author, 'William Shakespeare'); equals(data.length, 123); }); - test("sync: update with emulateHttp", function() { - Backbone.emulateHttp = true; + test("sync: update with emulateHTTP", function() { + Backbone.emulateHTTP = true; + library.first().save({id: '2-the-tempest', author: 'Tim Shakespeare'}); + equals(lastRequest.url, '/library/2-the-tempest'); + equals(lastRequest.type, 'POST'); + equals(lastRequest.dataType, 'json'); + equals(lastRequest.data._method, 'PUT'); + Backbone.emulateHTTP = false; + }); + + test("sync: update with emulateJSON", function() { + Backbone.emulateJSON = true; + library.first().save({id: '2-the-tempest', author: 'Tim Shakespeare'}); + equals(lastRequest.url, '/library/2-the-tempest'); + equals(lastRequest.type, 'PUT'); + var data = JSON.parse(lastRequest.data.model); + equals(lastRequest.dataType, 'json'); + equals(data.id, '2-the-tempest'); + equals(data.title, 'The Tempest'); + equals(data.author, 'Tim Shakespeare'); + equals(data.length, 123); + Backbone.emulateJSON = false; + }); + + test("sync: update with emulateHTTP and emulateJSON", function() { + Backbone.emulateHTTP = true; + Backbone.emulateJSON = true; library.first().save({id: '2-the-tempest', author: 'Tim Shakespeare'}); equals(lastRequest.url, '/library/2-the-tempest'); equals(lastRequest.type, 'POST'); equals(lastRequest.dataType, 'json'); equals(lastRequest.data._method, 'PUT'); var data = JSON.parse(lastRequest.data.model); + equals(lastRequest.dataType, 'json'); equals(data.id, '2-the-tempest'); equals(data.title, 'The Tempest'); equals(data.author, 'Tim Shakespeare'); equals(data.length, 123); + Backbone.emulateHTTP = false; + Backbone.emulateJSON = false; }); test("sync: read model", function() { @@ -76,19 +117,26 @@ $(document).ready(function() { }); test("sync: destroy", function() { - Backbone.emulateHttp = false; library.first().destroy(); equals(lastRequest.url, '/library/2-the-tempest'); equals(lastRequest.type, 'DELETE'); ok(_.isEmpty(lastRequest.data)); }); - test("sync: destroy with emulateHttp", function() { - Backbone.emulateHttp = true; + test("sync: destroy with emulateJSON", function() { + library.first().destroy(); + equals(lastRequest.url, '/library/2-the-tempest'); + equals(lastRequest.type, 'DELETE'); + ok(_.isEmpty(lastRequest.data)); + }); + + test("sync: destroy with emulateHTTP", function() { + Backbone.emulateHTTP = true; library.first().destroy(); equals(lastRequest.url, '/library/2-the-tempest'); equals(lastRequest.type, 'POST'); equals(JSON.stringify(lastRequest.data), '{"_method":"DELETE"}'); + Backbone.emulateHTTP = false; }); }); From 6c972695db10b1021fd21452fc68884fd8f16eb7 Mon Sep 17 00:00:00 2001 From: John Wright Date: Fri, 29 Oct 2010 10:47:59 -0600 Subject: [PATCH 2/2] Default behavior for Backbone.sync is now to send everything as application/json. Emulating http and sending data as form url-encoded can be turned on with Backbone.emulateHttp --- backbone.js | 69 ++++++++++++++++++++++++++-------------------------- index.html | 23 ++++++------------ test/sync.js | 61 ++++++---------------------------------------- 3 files changed, 49 insertions(+), 104 deletions(-) diff --git a/backbone.js b/backbone.js index fe72b4b8..082733d9 100644 --- a/backbone.js +++ b/backbone.js @@ -27,14 +27,11 @@ // For Backbone's purposes, jQuery owns the `$` variable. var $ = this.jQuery; - // Turn on `emulateHTTP` to fake `"PUT"` and `"DELETE"` requests via - // the `_method` parameter. - Backbone.emulateHTTP = false; - - // Turn on `emulateJSON` to encode the body as - // `application/x-www-form-urlencoded` instead of `application/json` - // with the model as a JSON string in the param `model` - Backbone.emulateJSON = false; + // Turn on `emulateHttp` to use support legacy HTTP servers. Setting this option will + // fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and set a + // `X-Http-Method-Override` header, will encode the body as`application/x-www-form-urlencoded` + // instead of `application/json` and will send the model in a param named `model`. + Backbone.emulateHttp = false; // Backbone.Events // ----------------- @@ -659,38 +656,42 @@ // * Send up the models as XML instead of JSON. // * Persist models via WebSockets instead of Ajax. // - // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests - // as `POST`, with an `_method` parameter containing the true HTTP method. + // Turn on `Backbone.emulateHttp` in order to send `PUT` and `DELETE` requests + // as `POST`, with a `_method` parameter containing the true HTTP method, + // as well as all requests with the body as `application/x-www-form-urlencoded` instead of + // `application/json` with the model in a param named `model`. // Useful when interfacing with server-side languages like **PHP** that make - // it difficult to read the body of `PUT` requests. Also, turn on Backbone.emulateJSON - // to send the body as `application/x-www-form-urlencoded` instead of - // `application/json`, with the model in a JSON string param named `model`, + // it difficult to read the body of `PUT` requests. Backbone.sync = function(method, model, success, error) { var sendModel = method === 'create' || method === 'update'; - var data = sendModel ? model : {}; var type = methodMap[method]; - if (Backbone.emulateHTTP && (type === 'PUT' || type === 'DELETE')) { - data._method = type; - type = 'POST'; - } + var ajaxParams = {}; + if (Backbone.emulateHttp) { + ajaxParams.contentType = "application/x-www-form-urlencoded"; + ajaxParams.processData = true; + ajaxParams.data = {}; + if (sendModel) ajaxParams.data.model = model.toJSON(); + if (type === 'PUT' || type === 'DELETE') { + ajaxParams.data._method = type; + type = 'POST'; + ajaxParams.beforeSend = function(xhr) { + xhr.setRequestHeader("X-Http-Method-Override", "PUT"); + } + } + } else { + ajaxParams.contentType = "application/json"; + ajaxParams.processData = false; + ajaxParams.data = sendModel ? JSON.stringify(model.toJSON()) : {}; + } + + ajaxParams.url = getUrl(model); + ajaxParams.type = type; + ajaxParams.dataType = "json"; + ajaxParams.success = success; + ajaxParams.error = error; - if (sendModel && Backbone.emulateJSON) data.model = JSON.stringify(model); - if (sendModel && !(Backbone.emulateHTTP || Backbone.emulateJSON)) data = JSON.stringify(data); - - processData = Backbone.emulateJSON ? true : false; - contentType = Backbone.emulateJSON ? "application/x-www-form-urlencoded" : "application/json"; - - $.ajax({ - url : getUrl(model), - type : type, - data : data, - processData : processData, - contentType : contentType, - dataType : 'json', - success : success, - error : error - }); + $.ajax(ajaxParams); }; // Helpers diff --git a/index.html b/index.html index 36b2955d..18419aac 100644 --- a/index.html +++ b/index.html @@ -1252,10 +1252,12 @@ var othello = NYPL.create({

- If your web server makes it difficult to work with real PUT and - DELETE requests, you may choose to emulate them instead, using - HTTP POST, and passing them under the _method parameter - instead, by turning on Backbone.emulateHTTP: + If you want to work with a legacy web server that doesn't support Backbones's + default JSON centric approach, you may choose to turn on Backbone.emulateHttp. + Setting this option will emulate PUT and DELETE requests with + a HTTP POST, and passing them under the _method parameter. Setting this option + will also send the model under a model param and use application/x-www-form-urlencoded + instead of the default application/json as the content-type for data sent.

@@ -1264,20 +1266,9 @@ Backbone.emulateHTTP = true;
 model.save();  // Sends a POST to "/collection/id", with "_method=PUT"
 
-

- Also you can choose to send the body with content-type - application/x-www-form-urlencoded and the model in a JSON - string param called model by turning on Backbone.emulateJSON: -

- -
-Backbone.emulateJSON = true;
-
-model.save();  //sends data as application/x-www-form-urlencoded
-

As an example, a Rails handler responding to an "update" call from - Backbone.sync might look like this: (In real code, never use + Backbone might look like this: (In real code, never use update_attributes blindly, and always whitelist the attributes you allow to be changed.)

diff --git a/test/sync.js b/test/sync.js index bf9dc5a5..971c25ed 100644 --- a/test/sync.js +++ b/test/sync.js @@ -42,18 +42,6 @@ $(document).ready(function() { equals(data.length, 123); }); - test("sync: create with emulateJSON", function() { - Backbone.emulateJSON = true; - library.add(library.create(attrs)); - equals(lastRequest.url, '/library'); - equals(lastRequest.type, 'POST'); - equals(lastRequest.dataType, 'json'); - var data = JSON.parse(lastRequest.data.model); - equals(data.title, 'The Tempest'); - equals(data.author, 'Bill Shakespeare'); - equals(data.length, 123); - Backbone.emulateJSON = false; - }); test("sync: update", function() { library.first().save({id: '1-the-tempest', author: 'William Shakespeare'}); @@ -67,46 +55,18 @@ $(document).ready(function() { equals(data.length, 123); }); - test("sync: update with emulateHTTP", function() { - Backbone.emulateHTTP = true; + test("sync: update with emulateHttp", function() { + Backbone.emulateHttp = true; library.first().save({id: '2-the-tempest', author: 'Tim Shakespeare'}); equals(lastRequest.url, '/library/2-the-tempest'); equals(lastRequest.type, 'POST'); equals(lastRequest.dataType, 'json'); equals(lastRequest.data._method, 'PUT'); - Backbone.emulateHTTP = false; - }); - - test("sync: update with emulateJSON", function() { - Backbone.emulateJSON = true; - library.first().save({id: '2-the-tempest', author: 'Tim Shakespeare'}); - equals(lastRequest.url, '/library/2-the-tempest'); - equals(lastRequest.type, 'PUT'); - var data = JSON.parse(lastRequest.data.model); - equals(lastRequest.dataType, 'json'); + var data = lastRequest.data.model; equals(data.id, '2-the-tempest'); - equals(data.title, 'The Tempest'); equals(data.author, 'Tim Shakespeare'); equals(data.length, 123); - Backbone.emulateJSON = false; - }); - - test("sync: update with emulateHTTP and emulateJSON", function() { - Backbone.emulateHTTP = true; - Backbone.emulateJSON = true; - library.first().save({id: '2-the-tempest', author: 'Tim Shakespeare'}); - equals(lastRequest.url, '/library/2-the-tempest'); - equals(lastRequest.type, 'POST'); - equals(lastRequest.dataType, 'json'); - equals(lastRequest.data._method, 'PUT'); - var data = JSON.parse(lastRequest.data.model); - equals(lastRequest.dataType, 'json'); - equals(data.id, '2-the-tempest'); - equals(data.title, 'The Tempest'); - equals(data.author, 'Tim Shakespeare'); - equals(data.length, 123); - Backbone.emulateHTTP = false; - Backbone.emulateJSON = false; + Backbone.emulateHttp = false; }); test("sync: read model", function() { @@ -123,20 +83,13 @@ $(document).ready(function() { ok(_.isEmpty(lastRequest.data)); }); - test("sync: destroy with emulateJSON", function() { - library.first().destroy(); - equals(lastRequest.url, '/library/2-the-tempest'); - equals(lastRequest.type, 'DELETE'); - ok(_.isEmpty(lastRequest.data)); - }); - - test("sync: destroy with emulateHTTP", function() { - Backbone.emulateHTTP = true; + test("sync: destroy with emulateHttp", function() { + Backbone.emulateHttp = true; library.first().destroy(); equals(lastRequest.url, '/library/2-the-tempest'); equals(lastRequest.type, 'POST'); equals(JSON.stringify(lastRequest.data), '{"_method":"DELETE"}'); - Backbone.emulateHTTP = false; + Backbone.emulateHttp = false; }); });