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()));
});
Related
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.
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
I need to have two url properties inside my Backbone.Collection.extend() because if a collection is fetched then I need to use a specific url if the collection gets a new model then I want to change the url
module.exports = MessagesCollection = Backbone.Collection.extend({
initialize: function(models, options) {
this.id = options.id;
},
url: function() {
if (fetch method is called) {
return '/api/messages/' + this.id;
} else {
// here if a model is being added?
return '/api/messages'
}
},
model: MessageModel
});
The reason for this is because I only want to pull down the models from the server based on the user.
var me = new MeModel();
me.fetch({
success: function(response) {
App.data.me = me;
var messages = new MessagesCollection([], { id: response.get('user_id') });
messages.fetch({
success: function() {
App.data.messages = messages;
App.core.vent.trigger('app:start');
}
});
}
});
When the user creates a new model within the app I want it to go into the main collection?
Does this mean I should create a sub collection based on the main collection somehow?
Edit:
My create looks like this somewhere else in the app window.App.data.messages.create(Message); I am thinking maybe I could write something like
var me = new MeModel();
me.fetch({
success: function(response) {
App.data.me = me;
var messages = new MessagesCollection([], { id: response.get('user_id') });
var allMessages = new MessagesCollection();
messages.fetch({
success: function() {
App.data.messages = messages;
App.data.allMessages = allMessages;
App.core.vent.trigger('app:start');
}
});
}
});
Then create window.App.data.allMessages.create(Message);> It sounds like it can cause problems IDK any ideas?
Edit:
The above worked but I had to create a new Backbone.Collection.extend() passing the same model but just writing it like
var Backbone = require('backbone'),
MessageModel = require('../models/message');
module.exports = AllMessagesCollection = Backbone.Collection.extend({
model: MessageModel,
url: '/api/messages'
});
So let me really break this question down, is this solution problematic. What is the best way to do this? The worst thing I can think of is bandwidth, using this method I would constantly be sending requests!
If you need to use different url only when create new model you can override collection.create method:
var MessagesCollection = Backbone.Collection.extend({
initialize: function(models, options) {
this.id = options.id;
},
url: function() {
return '/api/messages/' + this.id;
},
create: function(model, options){
var extendedOptions = _.extend(options || {}, {url: '/api/messages'});
return this.constructor.__super__.create.call(this, model, extendedOptions);
}
});
I ve created a backbone model, which fetch json from a server. However, i want to update view with the new data, in specific time interval, not every time that the server sends data. What should i use with purpose to update backbone view every n milliseconds? I ve got the above code.
$(function() {
var Profile = Backbone.Model.extend();
var ProfileList = Backbone.Collection.extend({
model: Profile,
url: 'data.php'
});
var ProfileView = Backbone.View.extend({
el: "#profiles",
template: _.template($('#profileTemplate').html()),
render: function(eventName) {
_.each(this.model.models, function(profile){
var profileTemplate = this.template(profile.toJSON());
$(this.el).append(profileTemplate);
}, this);
return this;
}
});
var profiles = new ProfileList();
var profilesView = new ProfileView({model: profiles});
profiles.fetch({reset: true});
//profiles.bind('reset', function () { console.log(profiles); });
profiles.bind('reset', function () {
profilesView.render();
});
});
A simple solution would be:
profiles.fetch({reset: true});
setInterval(
function() {
profiles.fetch({reset: true});
}, 1000 // Time in milliseconds
);
I wouldn't say that it's a beautiful solution but I hope you get the idea. As far as I know there is no interval fetch, or something similar, implemented in Backbone - so you pretty much have to build your own.
EDIT
This is probably a better solution, I like it more atleast.
var ProfileList = Backbone.Collection.extend({
model : Profile,
url : "data.php",
xhr : null,
interval: null,
fetchWithInterval: function(options) {
var options = options || {},
self = this;
this.interval = setInterval(function(){
if(self.xhr !== null && self.xhr.readyState !== 4) {
return;
}
self.xhr = self.constructor.__super__.fetch.apply(self, options);
}, 1000);
this.xhr = self.constructor.__super__.fetch.apply(self, options);
},
stopFetchWithInterval: function() {
clearInterval(this.interval);
}
});
Use it with profiles.fetchWithInterval({reset: true}); and you can stop it with profiles.stopFetchWithInterval().
It also manages the xhr, so if the AJAX call isn't finished it will not start a new one. This is pretty handy if you want to fetch with a small interval or when your API is slow for some reason.
//A backbone model
var RequestModel = Backbone.Model.extend({});
//A backbone model
var ResponseModel = Backbone.Model.extend({});
RequestModel.save({
success: function (ResponseModel ) {
alert(ResponseModel .toJSON());
}
})
Can i have a separate Model for Request and Response, as both Request and Response does not match. Its a total RPC call and not a CRUD operation.
I've thought about this same problem before, and I feel there isn't a great way to achieve this in Backbone. The best I've come up with is to implement a fromResponse and toRequest method on the model, and override model.parse and model.sync to map the model object to them. Something like:
var Model = Backbone.Model.extend({
fromResponse: function(responseAttrs) {
var modelAttrs = {}; //map response attributes to modelAttrs
return modelAttrs;
},
toRequest: function() {
//map model attributes to response attributes here
var modelAttrs = this.toJSON();
var responseAttrs = {}; //map models attributes to requestAttrs
return responseAttrs;
},
parse: function(response) {
return this.fromResponse(response);
},
sync: function(method, model, options) {
options = options || {};
options.data = this.toRequest();
Backbone.sync(method, model, options);
}
});
If the parse and sync are overridden in some kind of a base class, then you only need to implement the fromResponse and toRequest mappers for each model.
Another option would be to override Backbone.sync altogether, and map each Model type to some kind of ModelRequestMapper and ModelResponseMapper object to (de-)serialize each model. I feel that would be more complicated, but might scale better, if you have lots of models.
/Code sample not tested