i am very confuse about using backbone.js model fetch method. See the following example
backbone router:
profile: function(id) {
var model = new Account({id:id});
console.log("<---------profile router-------->");
this.changeView(new ProfileView({model:model}));
model.fetch();
}
the first step, the model account will be instantiated, the account model looks like this.
define(['models/StatusCollection'], function(StatusCollection) {
var Account = Backbone.Model.extend({
urlRoot: '/accounts',
initialize: function() {
this.status = new StatusCollection();
this.status.url = '/accounts/' + this.id + '/status';
this.activity = new StatusCollection();
this.activity.url = '/accounts/' + this.id + '/activity';
}
});
return Account;
});
urlRoot property for what is it? After model object created, the profileview will be rendered with this this.changeView(new ProfileView({model:model}));, the changeview function looks like this.
changeView: function(view) {
if ( null != this.currentView ) {
this.currentView.undelegateEvents();
}
this.currentView = view;
this.currentView.render();
},
after render view, profile information will not display yet, but after model.fetch(); statement execute, data from model will be displayed, why? I really don't know how fetch works, i try to find out, but no chance.
I'm not entirely sure what your question is here, but I will do my best to explain what I can.
The concept behind the urlRoot is that would be the base URL and child elements would be fetched below it with the id added to that urlRoot.
For example, the following code:
var Account = Backbone.Model.extend({
urlRoot: '/accounts'
});
will set the base url. Then if you were to instantiate this and call fetch():
var anAccount = new Account({id: 'abcd1234'});
anAccount.fetch();
it would make the following request:
GET /accounts/abcd1234
In your case there, you are setting the urlRoot and then explicitly setting a url so the urlRoot you provided would be ignored.
I encourage you to look into the Backbone source (it's surprisingly succinct) to see how the url is derived: http://backbonejs.org/docs/backbone.html#section-65
To answer your other question, the reason your profile information will not display immediately is that fetch() goes out to the network, goes to your server, and has to wait for a reply before it can be displayed.
This is not instant.
It is done in a non-blocking fashion, meaning it will make the request, continue on doing what it's doing, and when the request comes back from the server, it fires an event which Backbone uses to make sure anything else that had to be done, now that you have the model's data, is done.
I've put some comments in your snippet to explain what's going on here:
profile: function(id) {
// You are instantiating a model, giving it the id passed to it as an argument
var model = new Account({id:id});
console.log("<---------profile router-------->");
// You are instantiating a new view with a fresh model, but its data has
// not yet been fetched so the view will not display properly
this.changeView(new ProfileView({model:model}));
// You are fetching the data here. It will be a little while while the request goes
// from your browser, over the network, hits the server, gets the response. After
// getting the response, this will fire a 'sync' event which your view can use to
// re-render now that your model has its data.
model.fetch();
}
So if you want to ensure your view is updated after the model has been fetched there are a few ways you can do that: (1) pass a success callback to model.fetch() (2) register a handler on your view watches for the 'sync' event, re-renders the view when it returns (3) put the code for instantiating your view in a success callback, that way the view won't be created until after the network request returns and your model has its data.
Related
I have a Backbone model object which I successfully save. However the response from the server once the object is saved is another object (not the same object I am saving (not my decision, it's a system I have to deal with)):
var userActivity = new UserActivity();
...some other logic here...
userActivity.save(null, {
wait: true,
success: function(model, response, options) {
dataLoader.getCachedObject(
function(cachedObject) {
// I want to update cachedObject object with new data coming back from the server, in a way that my views get updated on change event.
});
},
error: function(model, xhr, options){
}
});
I want to update cachedObject object with new data coming back from the server, in a way that my views get updated on change event.
How can I accomplish that? Do I call
cachedObject.parse(response)
Any help is greatly appreciated.
If I understand you correctly, each time your model successfully syncs with the server, you want to update the client-side attributes with the data coming from the server, right? You should be able to do it with something like this:
In your model:
initialize: function(){
this.on("sync", function(model, response, options){
this.set(response.newData);
}
}
Of course, change the newData key to whatever object the server uses to return the dat you want...
Now, each time the model successfully sync with the server, it will set its attributes to what is returned by the server, which will trigger a "change" event.
In your view, you can then have
initialize: function(){
this.listenTo(model, "change", this.render);
}
And the view wil rerender itself each time the models changes (which will happen after each save on the server).
Context
The situation as follows: Users can upload files in an application. They can do this at any time (and number of times).
I would like to show a spinner when any uploading is being done, and remove it when no uploading is happening at the moment.
My approach
The uploads are handles by an external file upload plugin (like blueimp) and on it's add method I grab the jqXHR object and add these to a backbone collection (which are images in my application, so I use this in combination with Marionette's collectionviews).
The following is part of a function called in an onRender callback of a Marionette Itemview:
// Get the file collection
var uploadFiles = SomeBackBoneCollection;
// Track how many deferreds are expected to finish
var expected = 0;
// When an image is added, get the jqXHR object
uploadFiles.bind('add', function(model) {
// Get jqXHR object and call function which tracks it
trackUploads(model.get('jqXHR'));
// Do something to show the spinner
console.log('start the spinner!');
// Track amount of active deferreds
expected++;
}, this);
// Track the uploads
function trackUploads(jqXHR) {
$.when(jqXHR).done(function(){
// A deferred has resolved, subtract it
expected--;
// If we have no more active requests, remove the spinner
if (expected === 0) {
console.log('disable the spinner!');
}
});
}
Discussion
This method works very well, although I'm wondering if there are any other (better) approaches.
What do you think about this method? Regarding this method, do you see any up- or downsides? Any other methods or suggestions anyone?
For example, it might be great to have some kind of array/object to which you can keep passing deferreds, and that a $.when is somehow monitoring this collection and resolves if at any moment everything is done. However, this should work such that you can keep passing deferred objects at any given time.
you can do this via events.
I am assuming each file is an instance of this model:
App.Models.File = Backbone.Model.extend({});
before the user upload the file, you are actually creating a new model, and save it.
uploadedFiles.create(new App.Models.File({...}));
so in your upload view...
//listen to collection events
initialize: function() {
//'request' is triggered when an ajax request is sent
this.listenTo(this.collection, 'request', this.renderSpinner);
//when the model is saved, sync will be triggered
this.listenTo(this.collection, 'sync', this.handleCollectionSync);
}
renderSpinner: function() {
//show the spinner if it is not already being shown.
}
ok, so, in 'handleCollectionSync' function, you want to decide if we wanna hide the spinner.
so how do we know if there're still models being uploaded? you check if there're new models in the collection (not saved models)
so in your collection, add a helper method:
App.Collections.Files = Backbone.Collection.extend({
//if there's a new model, return true
hasUnsavedModels: function() {
return this.filter(function(model) {
return model.isNew();
}).length > 0;
}
});
back to your view:
handleCollectionSync: function() {
//if there's no unsaved models
if(!this.collection.hasUnsavedModels()){
//removespinner
}
}
this should solve your problem assuming all the uploads are successful. you may want to complete this with error handling cases - it depends on what you wanna do with error case, but as long as you are not retrying it right away, you should remove it from the collection.
==========================================================================================
Edit
I'm thinking, if you allow the user to upload a file multiple times, you are not really creating new models, but updating existing ones, so the previous answer would not work. to work around this, I would track the status on the model itself.
App.Models.File = Backbone.Model.extend({
initialize: function() {
this.uploading = false; //default state
this.on('request', this.setUploading);
this.on('sync error', this.clearUploading);
}
});
then setUploading method should set uploading to true, clearUploading should change it to false;
and in your collection:
hasUnsavedModels: function() {
return this.filter(function(model) {
return model.uploading;
}).length > 0;
}
so in your view, when you create a new file
uploadNewFile: function(fileAttributes) {
var newFile = new App.Model.File(fileAttributes);
this.collection.add(newFile);
newFile.save();
}
I believe 'sync' and 'request' events are triggered on the collection too when you save models inside of it. so you can still listenTo request, sync, and error events on the collection, in the view.
I'm implementing a system that require access to Google Places JS API. I've been using rails for most of the project, but now I want to inject a bit of AJAX in one of my views. Basically it is a view that displays places near your location. For this, I'm using the JS API of Google places. A quick workflow would be:
1- The user inputs a text query and hits enter.
2- There is an AJAX call to request data from Google Places API.
3- The successful result is presented to the user.
The problem is primarily in step 2. I want to use backbone for this but when I create a backbone model, it requests to the 'rootURL'. This wouldn't be a problem if the requests to Places was done from the server but it is not.
A place call is done like this:
service = new google.maps.places.PlacesService(map);
service.nearbySearch(request, callback);
Passing a callback function:
function callback(results, status) {
if (status == google.maps.places.PlacesServiceStatus.OK) {
for (var i = 0; i < results.length; i++) {
var place = results[i];
createMarker(results[i]);
}
}
}
Is it possible to override the 'fetch' method in backbone model and populate the model with the successful Places result? Is this a bad idea?
It is possible to override the fetch method of your backbone model.
var mapModel = Backbone.Model.extend({
fetch: function (options) {
// do your call to google places here
},
callBackFunctionForGoogleMaps: function (results, status) {
// call back function here would set model properties
}
});
return mapModel;
This way you override fetch and remove the defaults behavior of Backbone to make an ajax call.
Just as an FYI if you want to override Backbone models fetch but still have the default behavior of model.fetch you can do the following. Note the return calling Backbone.Model.fetch.
var mapModel = Backbone.Model.extend({
fetch: function (options) {
// do any pre-fetch actions here
return Backbone.Model.fetch.call(options);
}
});
return mapModel;
It is probably not a bad idea to override the fetch method here because you are still fetching data for your model, just not through ajax calls on your end. It would be smart though to leave comments noting that you are overriding fetch in this manner for a reason.
I'm trying to get the backbone.js DELETE request to fire, but don't see any requests being made in my console.
I have collection model like so:
var Model = Backbone.Model.extend(
{
urlRoot: '/test',
defaults:{}
});
var TableList = Backbone.Collection.extend(
{
url: '/test',
model: Model
});
In my view I'm running this:
this.model.destroy();
Everything seems to be running fine, I can see output coming from the remove function that calls the destroy so I know it's getting there plus it also successfully runs an unrender method that I have. Can't see any requests being made to the sever though?
If I am not mistaken, you have to have an id property on your model to ensure that it hits the correct url. IE if your model was...
var Model = Backbone.Model.extend({
url: '/some/url'
});
var model = new Model({
id: 1
});
model.destroy(); // I THINK it will now try and DELETE to /some/url/1
Without an id it doesn't know how to build the url correctly, typically you'd fetch the model, or create a new one and save it, then you'd have a Url...
See if that helps!
I found the issue to my problem, thought not a solution yet. I'm not sure this is a bug with backbone or not, but I'm using ajaxSetup and ajaxPrefilter. I tried commenting it out and it worked. I narrowed it down to the ajaxSetup method and the specifically the use of the data parameter to preset some values.
Have you tried using success and error callbacks?
this.model.destroy({
success : _.bind(function(model, response) {
...some code
}, this),
error : _.bind(function(model, response) {
...some code
}, this);
});
Might be instructive if you're not seeing a DELETE request.
I have two set of collections. One is for the categories and the other is for the Items. I ned to wait for the categories to finish fetching everything for me to set the category for the Items to be fetched.
Also i everytime i click a category i must re-fetch a new Items Collection because i have a pagination going on everytime i click on a category it doesn't refresh or re-fetch the collection so the pagination code is messing with the wrong collection. Any ideas?
this.categoryCollection = new CategoryCollection();
this.categoryCollection.fetch();
this.itemCollection = new ItemCollection();
this.itemCollection.fetch();
Just ran into a similar situation. I ended up passing jquery.ajax parameters to the fetch() call. You can make the first fetch synchronous. From the backbone docs:
jQuery.ajax options can also be passed directly as fetch options
Your code could be simplified to something like:
this.categoryCollection.fetch({async:false});
this.itemCollection.fetch();
One quick way would be to just pass a callback into the first fetch() call that invokes the second. fetch() takes an options object that supports a success (and error) callback.
var self = this;
this.categoryCollection = new CategoryCollection();
this.categoryCollection.fetch({
success: function () {
self.itemCollection = new ItemCollection();
self.itemCollection.fetch();
}
});
Not the most elegant, but it works. You could probably do some creative stuff with deferreds since fetch() returns the jQuery deferred that gets created by the $.ajax call that happens.
For the pagination issue, it's difficult to tell without seeing what your pagination code is doing. You're going to have to roll the pagination stuff yourself since Backbone doesn't support it natively. What I'd probably do is create a new Collection for the page criteria that are being queried and probably create a server action I could hit that would support the pagination (mapping the Collection's url to the paginated server action). I haven't put a ton of thought into that, though.
I had to react to this thread because of the answers there.
This is ONLY WAY OF DOING THIS RIGHT!!!
this.categoryCollection = new CategoryCollection();
this.itemCollection = new ItemCollection();
var d1 = this.categoryCollection.fetch();
var d2 = this.itemCollection.fetch();
jQuery.when(d1, d2).done(function () {
// moment when both collections are populated
alert('YOUR COLLECTIONS ARE LOADED :)');
});
By doing that you are fetching both collections at same time and you can have event when both are ready. So you don't wait to finish loading first collections in order to fetch other, you are not making ajax calls sync etc that you can see in other answers!
Here is a doc on Deferred objects.
Note: in this example case when one or more deferred object fails it's not covered. If you want to cover that case also beside .done you will have to add .fail callback on .when and also add error handler that will mark failed d1 or d2 in this example.
I am using RelationalModel and I created a queued fetch, that only calls the 'change' event when done loading:
var MySuperClass = Backbone.RelationalModel.extend({
//...
_fetchQueue : [],
fetchQueueingChange : function(name){
//Add property to the queue
this._fetchQueue.push(name);
var me = this;
//On fetch finished, remove it
var oncomplete = function(model, response){
//From _fetchQueue remove 'name'
var i = me._fetchQueue.indexOf(name);
me._fetchQueue.splice(i, 1);
//If done, trigger change
if (me._fetchQueue.length == 0){
me.trigger('change');
}
};
this.get(name).fetch({
success: oncomplete,
error : oncomplete
});
},
//...
});
The class would call:
this.fetchQueueingChange('categories');
this.fetchQueueingChange('items');
I hope you can improve on this, it worked well for me.
I ended up with the same problem today and figured out a solution to this:
var self = this;
this.collection = new WineCollection();
this.collection.url = ApiConfig.winetards.getWineList;
this.collection.on("reset", function(){self.render()});
this.collection.fetch({reset: true});
Now when the fetch on the collection is complete a "reset" is triggered and upon "reset" call the render() method for the view.
Using {async: false} is not the ideal way to deal with Backbone's fetch().
just set jQuery to become synchronous
$.ajaxSetup({
async: false
});
this.categoryCollection.fetch();
this.itemCollection.fetch();
$.ajaxSetup({
async: true
});
This is the simplest solution, I guess. Of course, starting new requests while these fetches run will be started as synchronous too, which might be something you don't like.