I'm trying to figure out the "correct" way of accomplishing custom update
functions in Backbone.js Models. An example of what I'm trying to do is:
var Cat = Backbone.Model.extend({
defaults: {
name : 'Mr. Bigglesworth',
location : 'Living Room',
action : 'sleeping'
},
sleep: function () {
// POST /cats/{{ cat_id }}/action
// { action: "sleep" }
},
meow: function () {
// POST /cats/{{ cat_id }}/action
// { action: "meow" }
}
})
From what I can tell, the Backbone.Collection.save() method only performs the
following:
POST /cats/{{ cat_id }}
{ name: 'Mr. Bigglesworth', location: 'Living Room', action: '{{ value }} '}
But the API I'm working with won't let me change action that way, only by:
POST /cats/{{ cat_id }}/action
{ action: "{{ value }}" }
Hopefully that makes sense?
Any help would be appreciated.
You can pass the URL as a parameter when you call save. Maybe you can do something like this:
var Cat = Backbone.Model.extend({
urlRoot: '/cats/',
defaults: {
name : 'Mr. Bigglesworth',
location : 'Living Room',
action : 'sleeping'
},
sleep: function () {
var custom_url = this.urlRoot + this.id + "/action";
this.save({}, { url: custom_url});
// POST /cats/{{ cat_id }}/action
// { action: "sleep" }
},
});
See here: Posting form data using .save() to pass url parameters.
You can also implement the sync method to use another URL if you always want to use a custom URL on update. See for example here: backbone.js use different urls for model save and fetch.
There are different approaches you can take to solve this, but IMO the cleanest is to override Backbone.sync to act the way you want it to act if it's universal to the server backend you're connecting to.
For instance, if you want every one of your models/collections to interact with a particular backend implementation, this approach makes a lot of sense.
This way you can leave the rest of the Collection (or Model) code as the Backbone default but it will work the way you want it to work.
For example:
// Store the default Backbone.sync so it can be referenced later
Backbone.vanillaSync = Backbone.sync;
// Most of this is just copy-pasted from the original Backbone.sync
Backbone.sync = function(method, model, options) {
var type = methodMap[method];
// Default options, unless specified.
_.defaults(options || (options = {}), {
emulateHTTP: Backbone.emulateHTTP,
emulateJSON: Backbone.emulateJSON
});
// Default JSON-request options.
var params = {type: type, dataType: 'json'};
// Ensure that we have a URL.
if (!options.url) {
params.url = _.result(model, 'url') || urlError();
}
// START ADD YOUR LOGIC HERE TO ADD THE /action
// Add the action to the url
params.url = params.url + '/' + options.action;
// Remove the action from the options array so it isn't passed on
delete options.action;
// END ADD YOUR LOGIC HERE TO ADD THE /action
// Ensure that we have the appropriate request data.
if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) {
params.contentType = 'application/json';
params.data = JSON.stringify(options.attrs || model.toJSON(options));
}
// For older servers, emulate JSON by encoding the request into an HTML-form.
if (options.emulateJSON) {
params.contentType = 'application/x-www-form-urlencoded';
params.data = params.data ? {model: params.data} : {};
}
// For older servers, emulate HTTP by mimicking the HTTP method with `_method`
// And an `X-HTTP-Method-Override` header.
if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) {
params.type = 'POST';
if (options.emulateJSON) params.data._method = type;
var beforeSend = options.beforeSend;
options.beforeSend = function(xhr) {
xhr.setRequestHeader('X-HTTP-Method-Override', type);
if (beforeSend) return beforeSend.apply(this, arguments);
};
}
// Don't process data on a non-GET request.
if (params.type !== 'GET' && !options.emulateJSON) {
params.processData = false;
}
// If we're sending a `PATCH` request, and we're in an old Internet Explorer
// that still has ActiveX enabled by default, override jQuery to use that
// for XHR instead. Remove this line when jQuery supports `PATCH` on IE8.
if (params.type === 'PATCH' && window.ActiveXObject &&
!(window.external && window.external.msActiveXFilteringEnabled)) {
params.xhr = function() {
return new ActiveXObject("Microsoft.XMLHTTP");
};
}
// Make the request, allowing the user to override any Ajax options.
var xhr = options.xhr = Backbone.ajax(_.extend(params, options));
model.trigger('request', model, xhr, options);
return xhr;
};
In the above example I assumed you had sent the action via the options array, if you actually wanted the static word /action you could just replace that block with:
// Add the action to the url
params.url = params.url + '/action';
This should give you the cleanest implementation while still keeping the rest of your code clean.
Related
I am trying to set up a variant fetch method on my backbone model that will fetch the current model for a given user. This is available from the API on /api/mealplans/owner/{username}/current.
I have written the following model. I commented out the URL Root, as the prototype fetch call was simply using the urlRoot and I wanted to see if that was overriding the url parameter I passed in portions somehow.
var mealPlan = Backbone.Model.extend({
name: 'Meal Plan',
//urlRoot: '/api/mealplans',
defaults: {},
fetchCurrent: function (username, attributes, options) {
attributes = attributes || {};
options = options || {};
if (options.url === undefined) {
options.url = "/api/mealplans/owner/" + username + "/current";
}
return Backbone.Model.prototype.fetch.call(this, attributes, options);
},
validate: function (attributes) {
// To be done
return null;
}
});
I've seen this done, in some variations in other places, such as at backbone.js use different urls for model save and fetch - In that case the code is slightly different (I started with that and broke it down to make it easier for me to read.)
The options object has the url parameter in it fine when I pass it to fetch, but then it seems to ignore it!
I was assuming the same parameters to fetch as to save - This is not the case.
The method signature for fetch ONLY takes 'options' and not 'attributes', hence the url parameter wasn't found.
The model code should look a bit more like this..
var mealPlan = Ministry.Model.extend({
name: 'Meal Plan',
urlRoot: '/api/mealplans',
defaults: {
},
fetchCurrent: function (username, options) {
options = options || {};
if (options.url === undefined) {
options.url = this.urlRoot + "/owner/" + username + "/current";
}
return Backbone.Model.prototype.fetch.call(this, options);
},
validate: function (attributes) {
// To be done
return null;
}
});
I think it is better to override url() method, like below:
var mealPlan = Ministry.Model.extend({
name: 'Meal Plan',
urlRoot: '/api/mealplans',
//--> this gets called each time fetch() builds its url
url: function () {
//call the parent url()
var url=Backbone.Model.prototype.url.call(this);
//here you can transform the url the way you need
url += "?code=xxxx";
return url;
}
...
besides, in your example above I think there is a mistake and you should replace fetchCurrent by fetch
To be quite honest I'm stuck trying to override Backbone's sync() method for a Model, I have the signature for the function in place, and it gets triggered correctly, but I don't know what to put in the function body in order for it to make a default call to DELETE but with extra arguments.
ie.
class Master.Models.Member extends Backbone.Model
urlRoot: '/api/members/'
sync: (method, model, options) ->
params = _.clone options
Backbone.sync method, model, params
I call it like this:
......
remove: ->
#model.destroy
collective_id: the_id
My intention there, is to pass the collective_id param you see there to the server. But even though it's inside the options hash for sync() and I clone it, It won't make it to the server!
How can I send that extra param to the server?
(As it is, the only thing that reaches the server is the Model's id)
Thanks in advance!
When you call .destroy(), .fetch() or .save() they all call Model.sync which only calls Backbone.sync. It's a proxy function. This is intended to provide a nice hook for modifying the AJAX behavior of a single model or any models that extend from that model.
Solution 1: Override the Global Backbone.sync to JSON.stringify and modify the contentType when you've specified data to be sent with the delete request.
Pros: You can call model.destroy() and optionally pass in an options parameter
Solution 2 - Override the Model.sync method.
Pros: The override only affects individual models. Isolated changes.
Cons: All models that wish to delete with data need to extend from the correct 'base model'
Solution 3 - Don't override anything and explicitly call model.sync with all of the stringify and contentType stuff.
Pros: Very isolated changes, won't affect any other models. Useful if you're integrating with a large codebase.
[Solution 1] - Global Override of Backbone.sync (this will affect all models)
javacript version
var oldBackboneSync = Backbone.sync;
Backbone.sync = function( method, model, options ) {
// delete request WITH data
if ( method === 'delete' && options.data ) {
options.data = JSON.stringify(options.data);
options.contentType = 'application/json';
} // else, business as usual.
return oldBackboneSync.apply(this, [method, model, options]);
}
Usage:
var model, SomeModel = Backbone.Model.extend({ /* urlRoot, initialize, etc... */});
model = new SomeModel();
model.destroy({
data: {
/* data payload to send with delete request */
}
});
[Solution 2] - Override Backbone.destroy on a base model and extend other models from that.
override
// Create your own 'enhanced' model
Backbone.EnhancedModel = Backbone.Model.extend({
destroy: function( options ) {
if ( options.data ) {
// properly formats data for back-end to parse
options.data = JSON.stringify(options.data);
}
// transform all delete requests to application/json
options.contentType = 'application/json';
Backbone.Model.prototype.destroy.call(this, options);
}
});
usage
var model, SomeModel = Backbone.EnhancedModel.extend({ /* urlRoot, initialize, etc... */})
model = new SomeModel();
SomeModel.destroy({
data: {
/* additional data payload */
}
});
[Solution 3] - Call .destroy() with correct parameters.
If sending data with your destroy requests is an isolated thing, then this is an adequate solution.
When calling model.destroy() pass in the data and contentType options like so:
javascript version/usage
var additionalData = { collective_id: 14 };
model.destroy({
data: JSON.stringify(additionalData),
contentType: 'application/json'
});
The "Problem" (with Backbone, not solutions):
Backbone.js makes the assumption (view source) that delete requests do not have a data payload.
// delete methods are excluded from having their data processed and contentType altered.
if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) {
params.contentType = 'application/json';
params.data = JSON.stringify(options.attrs || model.toJSON(options));
}
In their assumed RESTful API call, the only data required is the ID which should be appended to a urlRoot property.
var BookModel = Backbone.Model.extend({
urlRoot: 'api/book'
});
var book1 = new BookModel({ id: 1 });
book1.destroy()
The delete request would be sent like
DELETE => api/book/1
contentType: Content-Type:application/x-www-form-urlencoded; charset=UTF-8
Params need to be sent in options.data, so try:
coffeescript
remove: () ->
#model.destroy
data: JSON.stringify
collective_id: the_id,
contentType: 'application/json'
javascript
remove: function() {
return this.model.destroy({
data: JSON.stringify({
collective_id: the_id
}),
contentType: 'application/json'
});
}
I have some trouble appending a token to the backbone url query string and hope you guys could help me out here. Three things to know,
There is a rest api that expects a token with each request
An nginx backend that does auth, serves the backbone app + proxy req to the api under /api
i'm a new to javascript + backbone :/
The backbone app actually reads the token from a cookie and I need to append this to the request url everytime backbone makes a call. I see this can be done by overriding backbone sync. but it troubles me in a few different things. like, this is what I do
console.log('overriding backbone sync');
var key ="token";
Backbone.old_sync = Backbone.sync
Backbone.sync = function(method, model, options) {
if (method === 'read') {
if (!(model.url.indexOf('?key=') != -1)) {
model.url = model.url + '?key=' + key;
}
} else {
old_url = model.url();
if (!(old_url.indexOf('?key=') != -1)) {
model.url = function() {
return old_url + '?key=' + key;
}
}
}
Backbone.old_sync(method, model, options);
};
model.url was returning a function when its not a "read" method and didn't know how to handle it well and the other trouble is when a consecutive request is made, the token is added twice. I tried to remove it with that indexOf stuff with no luck.
Is there a better way to do this ?
I don't think you need to override sync at all:
var globalKey = 'key123';
var urlWithKey = function(url, key) {
return function() {
return url + "?key=" + key;
};
};
var MyModel = Backbone.Model.extend({
url: urlWithKey('/my/url/', globalKey)
});
If you now create an object and save it, a POST request to my/url/?key=key123 is sent.
I guess you could also override the url method if this is the behavior you need for all of your Backbone models.
A general note: in Backbone most parameters, such as url can be a function or a value. I don't know why in your example it was a function once and a value in another case, but you always must be able to handle both ways if you override some of the internal functions. If you look at Backbone's sourcecode you will see that they use getValue to access these parameters:
var getValue = function(object, prop) {
if (!(object && object[prop])) return null;
return _.isFunction(object[prop]) ? object[prop]() : object[prop];
};
Update: Overloading the url method for all models could work like this:
var globalKey = 'key123';
(function() {
var baseUrl = Backbone.Model.prototype.url;
Backbone.Model.prototype.url = function() {
return this.baseUrl + "?key=" + globalKey;
};
})()
var MyModel = Backbone.Model.extend({
baseUrl: '/my/url/'
});
You could also leave the regular Backbone.Model as it is, and create your own base class. See http://documentcloud.github.com/backbone/#Model-extend for details.
Just set your URL like so:
url : function() {
return "/my/url" + this.key;
}
In your overridden .sync, you only need to set the key property.
I need to override Backbone.sync to allow PUT the problem is i don't know how and where to put it.
This is my Model:
define([
'underscore',
'backbone'
], function(_, Backbone) {
var Input = Backbone.Model.extend({
url: 'http://localhost/InterprisePOS/SOP/mobilecreateinvoice/',
initialize: function(){
},
toJSON : function() {
return _.clone({ input:this.attributes });
},
});
return Input;
});
This is my Save function in my view:
save: function(){
invoice = new Invoice();
input = new Input();
invoice.set({POSWorkstationID: "POS7"});
invoice.set({POSClerkID: "admin"});
invoice.set({CustomerName: "Alice in Wonderland Tours"});
invoice.set({IsFreightOverwrite: true});
invoice.set({BillToCode: "CUST-000009"});
InvoiceCollection.add( invoice );
//var invoices = JSON.stringify({Invoices: InvoiceCollection.toJSON()});
_.each(this.collection.models, function(cart){
InvoiceDetailCollection.add( cart );
});
input.set({ "Invoices": InvoiceCollection.toJSON() });
input.set({ "InvoiceDetails": InvoiceDetailCollection});
alert( JSON.stringify(input.toJSON()) );
input.save();
}
The default Backbone sync handler maps CRUD to REST like the following:
create → POST /collection
read → GET /collection[/id]
update → PUT /collection/id
delete → DELETE /collection/id
Sometimes older servers emulate HTTP by mimicking the HTTP method with _method and X-HTTP-Method-Override header. If that is the case, you should set Backbone.emulateHTTP to true
If you want custom mappings, then you would need to override Backbone.sync. An example of overriding could be like the following:
Backbone.sync = function(method, model, options, error) {
// Backwards compatibility with Backbone <= 0.3.3
if (typeof options == 'function') {
options = {
success: options,
error: error
};
}
var resp = function(resp) {
if (resp.status) {
options.success(method != 'read' ? model : resp.data);
}
else {
options.error('Record not found ' + resp.data);
}
};
var store = model.customStorage || model.collection.customStorage;
switch (method) {
case 'read': model.id ? store.read({id: model.id}, resp) : store.readAll(resp); break;
case 'create': store.create(model.attributes, resp); break;
case 'update': store.update(model.attributes, resp); break;
case 'delete': store.delete(model.id, resp); break;
}
};
Where customStorage is your implementation, it could be anything you want that respects the methods I created. Some time ago, I created a backbone sync override for HTML5 WebSQL Storage, it is open sourced located on GitHub https://github.com/mohamedmansour/backbone.webStorage
I hope this helps you get started! Good Luck!
I'm using the Blogger Protocol API and I'm having trouble deleting posts. I'm working on a webOS device and so I can't send DELETE directly; instead I use Google's workaround to use POST:
deletePostList: function(event)
{
var deletePostID = event.item.id.split('.').pop().split('-').pop();
var deleteRequest = new Ajax.Request("http://www.blogger.com/feeds/" + activeBlogID + "/posts/default/" + deletePostID,
{
method: 'post',
requestHeaders:
{
Authorization: 'GoogleLogin auth=' + authCode,
"X-HTTP-Method-Override": "DELETE",
"If-Match": "*"
},
onSuccess: this.deletePostRequestSuccess.bind(this),
onFailure: this.deletePostRequestFailure.bind(this)
});
},
This seems to work, i.e. deletePostRequestSuccess is called after this processes and all the headers and response text look like I think they should when deleting a post, but the reality is that the post remains in the feed. I tried adding the "If-Match" header to make sure it wasn't the GData conditional delete holding me up (even though I haven't changed anything in the post at this time), but that doesn't seem to help.
Any ideas on how to make this work? I'd like to stick with Protocol since it's native on webOS, whereas jQuery, etc. is not.
From what I can tell, your issue with the HTTP methods is not webOS, but in Prototype according to the source.
I would suggest creating a subclass:
<script type="text/javascript">
var MyAjaxRequest = Class.create(Ajax.Request, {
request: function(url) {
this.url = url;
this.method = this.options.method;
var params = Object.isString(this.options.parameters) ?
this.options.parameters :
Object.toQueryString(this.options.parameters);
/* comment out this stuff that prevents you from using the DELETE method
if (!['get', 'post'].include(this.method)) {
// simulate other verbs over post
params += (params ? '&' : '') + "_method=" + this.method;
this.method = 'post';
}
*/
if (params && this.method === 'get') {
// when GET, append parameters to URL
this.url += (this.url.include('?') ? '&' : '?') + params;
}
this.parameters = params.toQueryParams();
try {
var response = new Ajax.Response(this);
if (this.options.onCreate) this.options.onCreate(response);
Ajax.Responders.dispatch('onCreate', this, response);
this.transport.open(this.method.toUpperCase(), this.url,
this.options.asynchronous);
if (this.options.asynchronous) this.respondToReadyState.bind(this).defer(1);
this.transport.onreadystatechange = this.onStateChange.bind(this);
this.setRequestHeaders();
this.body = this.method == 'post' ? (this.options.postBody || params) : null;
this.transport.send(this.body);
/* Force Firefox to handle ready state 4 for synchronous requests */
if (!this.options.asynchronous && this.transport.overrideMimeType)
this.onStateChange();
}
catch (e) {
this.dispatchException(e);
}
});
</script>
That way you can use method: 'DELETE'