Using Backbone Fetch Success Callback to Change Data Before Initializing View - javascript

I'm looking for a way to intercept the value returned from a server when I fetch a backbone model (a collection, strictly speaking) from the server, then modify it before continuing. I would think that I could do something like this
SessionController.prototype._initPages = function() {
return App.pages.fetch({
reset: true,
success: function(model, response, options) {
//modify the contents of response
}
};
And my modifications would be reflected in the model that's used to initialize the view.
However I was looking at the backbone source and I think I may have misunderstood something.
fetch: function(options) {
options = options ? _.clone(options) : {};
if (options.parse === void 0) options.parse = true;
var success = options.success;
var collection = this;
options.success = function(resp) {
var method = options.reset ? 'reset' : 'set';
collection[method](resp, options); //this line updates the model
if (success) success(collection, resp, options); // my success callback
collection.trigger('sync', collection, resp, options);
};
wrapError(this, options);
return this.sync('read', this, options);
}
For my needs, it seems the two commented lines need to be switched, though I assume I'm just misunderstanding how to use this feature.
How can I modify the server response before it becomes my model?

I think you could just override the parse function to modify your data as needed
http://backbonejs.org/#Model-parse

Related

Backbone modularized pattern with xhr on fetch

I'm trying to use alter the xhr object on an ajax request. I'm doing this on a fetch call for a collection. But when I alter the xhr I get no data. The purpose of this is to show the loaders percentage but the xhr isn't even working when I return the new xhr object. I did checkout the xhr that is returned and the url points to /admin/categories
require(['views/categories', 'models/categories', 'helpers/helper'], function(CategoriesView, model, helper) {
var categories = new model.CategoriesCollection;
categories.fetch({ url: "/admin/categories/getcategories", xhr: helper.xhr('#main-loader') }).then(function(response) {
console.log(response);
});
});
and here is my helper file
define(['helpers/helper', 'require'], function(Helper, require) {
'use strict';
var $ = require('jquery');
var Backbone = require('backbone');
var xhr = function(loaderId) {
var _xhr = Backbone.$.ajaxSettings.xhr();
_xhr.addEventListener("progress", function(e){
if (e.lengthComputable) {
console.log(e);
}
}, false);
return _xhr;
}
return {
xhr: xhr
}
});
An easy way to pass options to the xhr object (XMLHttpRequest object) is to use the xhrFields option of the jQuery ajax function. Since Backbone.sync uses jQuery.ajax by default in the background, any options passed to a Backbone syncing function is then passed as the ajax options.
Simple one-off solution
The simplest example of checking progress:
myCollection.fetch({
url: root + '/photos/',
xhrFields: {
onprogress: function() {
console.log("options onprogress");
}
}
});
Permanent solution
But a more convinient way would be to override the global Backbone.sync function to add our own progress callback option and a custom progress event.
Overriding Backbone.sync
Warning: don't override Backbone core if you're writing a library or code that will be shared.
Backbone.sync = (function(syncFn) {
return function(method, model, options) {
options = options || {};
var context = options.context,
progress = options.progress,
xhrFields = options.xhrFields || {},
onprogress = xhrFields.onprogress;
xhrFields.onprogress = function(e) {
var params = [model, e.loaded, _.extend({}, options, { event: e })];
if (progress) progress.apply(context, params)
if (onprogress) onprogress.apply(this, arguments);
model.trigger(['progress'].concat(params));
};
options.xhrFields = xhrFields;
return syncFn.apply(this, arguments);
};
})(Backbone.sync);
How to use
It's really straight-forward to use:
var myCollection = new Backbone.Collection(),
root = 'https://jsonplaceholder.typicode.com';
It provides a custom progress event.
myCollection.listenTo(myCollection, 'progess', function(collection, value, options) {
console.log("collection progress event");
});
It also provides a custom progress callback that can be passed to any Backbone functions that calls Backbone.sync in the background, like fetch, save, destroy. Also, passing xhrFields still works as expected.
myCollection.fetch({
url: root + '/photos/',
success: function() {
console.log(myCollection.models);
},
// custom options callback
progress: function(collection, value, options) {
console.log("collection onprogress callback");
},
// this still works
xhrFields: {
onprogress: function() {
console.log("options onprogress");
}
}
});
You receive the collection or model, the loaded data count, and the options object, which contains all the options of the sync, in addition to the native progress event (options.event).
Note that it's not that useful as the total doesn't always work. As an example, in Chrome the total is always zero, but in firefox, the total is correct. You should check the lengthComputable property.

Load Model From More Than One URL

How can I load different attributes for a model using different URLs?
E.g. I have 3 different and independent URLs that will return some attributes and I want to add all of those to a single model, suppose that I have the promise returned by all of them like this:
var model = //...
$.when(nameAttributes, addressAttributes, metaAttributes).then(
function(nameData, addressData, metaData) {
return _.extend({}, nameData, addressData, metaData);
})
.done(function(allData) {
model.set(allData);
doStuffWith(model);
});
Is there a way to turn that into this:
model.fetch().done(function(){ doStuffWith(model); });
Well.. Ok, this should do what you want. I STRONGLY suggest you do NOT take this route. Keep the data separately in individual Model()s that way you can update it and pull it when ever you need to. If you take this approach saving data will be a disaster.
http://jsfiddle.net/kjhvwxg4/
PS. I tried to not add any more code then i had to, keep in mind this will probably not handle error handling very well since xhr will never throw an error, you need to catch it yourself -- i didnt want to spend the time coding that since this is just a proof of concept.
var Model1 = Backbone.Model.extend({
fetch: function(options) {
options = _.extend({
parse: true
}, options);
var model = this;
var success = options.success;
var error = options.error;
options.success = function(resp) {
var serverAttrs = options.parse ? model.parse(resp, options) : resp;
if (!model.set(serverAttrs, options)) return false;
if (success) success.call(options.context, model, resp, options);
model.trigger('sync', model, resp, options);
};
options.error = function(resp) {
if (error) error.call(options.context, model, resp, options);
model.trigger('error', model, resp, options);
};
// custom code starts here
var call1 = $.getJSON('http://jsonplaceholder.typicode.com/posts/1');
var call2 = $.getJSON('http://jsonplaceholder.typicode.com/comments/2');
var call3 = $.getJSON('http://jsonplaceholder.typicode.com/albums/3');
var xhr = $.when(call1, call2, call3);
// mimics the same triggers the normal backbone does
model.trigger('request', model, xhr, options);
xhr.done(function(one, two, three){
var resp = _.extend(one[0], _.extend(two[0], _.extend(three[0], {})));
options.success(resp);
});
// we still need to send back an event handler
return xhr;
}
});
var model = new Model1();
model.fetch();
model.on('sync', function(model) {
alert(JSON.stringify(model.toJSON()));
});

override Backbone's Collection-fetch

I want to get my collection in a NON-RESTful way, so I decide to override the Collection.fetch with
App.carClc = Backbone.Collection.extend({
model : App.cardModel,
url : 'http://localhost/bbtest/data.php',
fetch : function() {
$.ajax({
type : 'GET',
url : this.url,
success : function(data) {
console.log(data);
}
});
}
});
I don't know how to set my collection to the response. I'm new to BackboneJS, thanks all of you!
If you want to add a custom "decorator" to fetch, but not override it completely, try:
var MyCollection = Backbone.Collection.extend({
//custom methods
fetch: function(options) {
//do specific pre-processing
//Call Backbone's fetch
return Backbone.Collection.prototype.fetch.call(this, options);
}
});
Here, you don't have to roll out your own $.ajax
Also, don't forget the return in the last line if you want to use the jQuery promise returned by Backbone's fetch method.
See http://japhr.blogspot.in/2011/10/overriding-url-and-fetch-in-backbonejs.html for more details.
Backbone collection has two methods to set new data add and reset. Let's say you want to replace all collection data with the incoming data and therefor use the reset:
App.carClc = Backbone.Collection.extend({
model : App.cardModel,
url : 'http://localhost/bbtest/data.php',
fetch : function() {
// store reference for this collection
var collection = this;
$.ajax({
type : 'GET',
url : this.url,
dataType : 'json',
success : function(data) {
console.log(data);
// set collection data (assuming you have retrieved a json object)
collection.reset(data)
}
});
}
})
I am using something like this:
$.when( $.ajax( URL, { dataType: "json" } ) )
.then( $.proxy( function( response ) {
ctx.view.collection.reset( response );
},ctx ) );
The main point beeing I use collection.reset(data) to reinitialize the collection
If you would like to keep fetch "thenable" for promises then you could also do something like this:
fetch: function() {
var self = this,
deferred = new $.Deferred();
$.get(this.url).done(function(data) {
// parse data
self.reset({parsed data});
deferred.resolve(); //pass in anything you want in promise
});
return deferred.promise();
}
If you need to do this for every model and/or collection, override Backbone.ajax.
Overriding Backbone.ajax gives you the request options that would be normally passed to $.ajax. You only need to return the response of $.ajax (or some other Promise) and don't need to worry about setting stuff in the collection/model.

Exclude model properties when syncing (Backbone.js)

Is there a way to exclude certain property from my model when I sync?
For example, I keep in my model information about some view state. Let's say I have a picker module and this module just toggle a selected attributes on my model. Later, when I call .save() on my collection, I'd want to ignore the value of selected and exclude it from the sync to the server.
Is there a clean way of doing so?
(Let me know if you'd like more details)
This seems like the best solution (based on #nikoshr referenced question)
Backbone.Model.extend({
// Overwrite save function
save: function(attrs, options) {
options || (options = {});
attrs || (attrs = _.clone(this.attributes));
// Filter the data to send to the server
delete attrs.selected;
delete attrs.dontSync;
options.data = JSON.stringify(attrs);
// Proxy the call to the original save function
return Backbone.Model.prototype.save.call(this, attrs, options);
}
});
So we overwrite save function on the model instance, but we just filter out the data we don't need, and then we proxy that to the parent prototype function.
In Underscore 1.3.3 they added pick and in 1.4.0 they added omit which can be used very simply to override your model's toJSON function to whitelist attributes with _.pick or blacklist attributes with _.omit.
And since toJSON is used by the sync command for passing the data to the server I think this is a good solution as long as you do not want these fields wherever else you use toJSON.
Backbone.Model.extend({
blacklist: ['selected',],
toJSON: function(options) {
return _.omit(this.attributes, this.blacklist);
},
});
my solution combine all the above. just use white list instead of black one .. this is good rule in general
define
attrWhiteList:['id','biography','status'],
and then overwrite the save
save: function(attrs, options) {
options || (options = {});
//here is whitelist or all
if (this.attrWhiteList != null )
// Filter the data to send to the server
whitelisted = _.pick(this.attributes, this.attrWhiteList);
else
whitelisted =this.attributes;
/* it seems that if you override save you lose some headers and the ajax call changes*/
// get data
options.data = JSON.stringify(whitelisted);
if ((this.get('id') == 0) || (this.get('id') == null))
options.type = "POST"
else
options.type = "PUT";
options.contentType = "application/json";
// options.headers = {
// 'Accept': 'application/json',
// 'Content-Type': 'application/json'
// },
// Proxy the call to the original save function
return Backbone.Model.prototype.save.call(this, attrs, options);
},
In fact there is a much simpler way of achieving this without messing with backbone save or sync function since you would no be expecting this behaviour to be permanent
if you look at backbone.js line 1145 you will see that
// 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));
}
Which means that you may override the data part of the xhr by putting data in your options
Since backbone save requires model.save([attributes], [options])
But remember that attributes like id might be essential to proper saving
Example
model.save( {}, { data: JSON.stringify(data) } ) ;
So you should be doing something like this
var data = { id : model.id , otherAttributes : 'value' } ;
model.save( {}, { data : JSON.stringify(data) } );
This do the trick quite well for me and could be used with any backbone with xhr such as fetch, save, delete, ...
Based on several of the answers, this accounts for cases of null objects and a conditional in Backbone that doesn't sent the contentType if options.data is already set:
EDITABLE_ATTRIBUTES = ["name", "birthdate", "favoriteFood"];
...
save: function(attrs, options) {
// `options` is an optional argument but is always needed here
options || (options = {});
var allAttrs = _.extend({}, this.attributes, attrs);
var allowedAttrs = _.pick(allAttrs, EDITABLE_ATTRIBUTES);
// If `options.data` is set, Backbone does not attempt to infer the content
// type and leaves it null. Set it explicitly as `application/json`.
options.contentType = "application/json";
options.data = JSON.stringify(allowedAttrs);
return Backbone.Model.prototype.save.call(
this, allowedAttrs, options);
},
I found some problems with the accepted solution, as options.data modifies the way Backbone makes the calls. Better using options.attrs as this:
Backbone.Model.extend({
save: function (attrs, options) {
options = options || {};
attrs = _.extend({}, _.clone(this.attributes), attrs);
// Filter the data to send to the server
delete attrs.selected;
options.attrs = attrs;
// Proxy the call to the original save function
return Backbone.Model.prototype.save.call(this, attrs, options);
}
});
Since save uses toJSON we override it:
toJSON: function(options) {
var attr = _.clone(this.attributes);
delete attr.selected;
return attr;
},
But it may not work if you're using toJSON and need selected in views for example. Otherwise you probably need to override save method.
Set options.attrs will allow you customise api param:
var model = new Backbone.Model();
model.save(null, {
wait: true,
success: function() {
},
attrs: _.omit(model.attributes, 'selected')
});
If it is a one-off occasion, you could use mode.unset('selected', { silent:true }) (silent is set only to avoid firing the change event), to remove the attribute... This has the not so nice counter-effect of having to re-set it after saving though.
This said, I totally endorse one of the solutions above. Moreover if this is something you need on a more regular basis.
To set only desired values, use HTTP PATCH insead of HTTP POST. On the backbone side, just add a patch attribute to the save method:
entity.save(data,{patch:true})
Using save with this attribute, only fields passed as data are sent to the server.
Having this same issue, I decided to create a small module that can help : https://github.com/lupugabriel1/backbone-model-save
This is how you can use it in your models:
var myModel = new Backbone.ModelSave.extend({
notSync: ['name', 'age']
});

Backbone data mapping

For mapping my backbone models to what I get from the server I am using a technique described on the GroupOn Dev blog: https://engineering.groupon.com/2012/javascript/extending-backbone-js-to-map-rough-api-responses-into-beautiful-client-side-models/
However, this only maps incoming data to the model.
I would like this to go both ways, so that when I save a model, it prepares the models attributes to match the servers model.
What would be the best solution to prepare the output of the model?
I've run into this exact same issue where my server response is completely different from what I am able to post. I discovered within the mechanics of the Backbone.sync object a way to that I could post to my server a custom JSON object in the following statement in Backbone.sync:
if (!options.data && model && (method == 'create' || method == 'update')) {
params.contentType = 'application/json';
params.data = JSON.stringify(model.toJSON());
}
sync evaluates if options.data does not exist then sets the params.data to the stringified model. The options.data check keyed me off. If that exists, sync will use that instead of the model. So given this, I overrode my model.save so could pass in an attributes hash that my server expects.
Here's how I overrode it:
save : function(key, value, options) {
var attributes = {}, opts = {};
//Need to use the same conditional that Backbone is using
//in its default save so that attributes and options
//are properly passed on to the prototype
if (_.isObject(key) || key == null) {
attributes = key;
opts = value;
} else {
attributes = {};
attributes[key] = value;
opts = options;
}
//In order to set .data to be used by Backbone.sync
//both opts and attributes must be defined
if (opts && attributes) {
opts.data = JSON.stringify(attributes);
opts.contentType = "application/json";
}
//Finally, make a call to the default save now that we've
//got all the details worked out.
return Backbone.Model.prototype.save.call(this, attributes, opts);
}
So how do you use this in your case? Essentially what you'll do is create a method that reverses the mapping and returns the resulting JSON. Then you can invoke save from your view or controller as follows:
getReversedMapping : function() {
ver reversedMap = {};
...
return reversedMap;
},
saveToServer : function() {
this._model.save(this.getReverseMapping, {
success : function(model, response) {
...
},
error : function(model, response) {
...
}
})
}
Since your overridden save automatically copies the JSON you pass in to options.data, Backbone.sync will use it to post.
The answer by Brendan Delumpa works, but it over-complicates things.
Don't do this in your save method. You don't want to copy over these parameter checks each time (and what if they somehow change in Backbone?).
Instead, overwrite the sync method in your model like this:
var MyModel = Backbone.Model.extend({
...,
sync: function (method, model, options) {
if (method === 'create' || method === 'update') {
// get data from model, manipulate and store in "data" variable
// ...
options.data = JSON.stringify(data);
options.contentType = 'application/json';
}
return Backbone.Model.prototype.sync.apply(this, arguments);
}
});
That's all there is to it when you need to "prepare" the data in a server-ready format.

Categories

Resources