I'm calling 'save' on my model and returning the new model as json in my PHP backend. When I step through the Backbone.Model.save method, I can see that it successfully gets the server response and then sets the model (in the options.success closure below). But when the execution is returned to my click handler, the model has the old properties (ie. the id is not set). What could be happening?
Here is my click handler:
addButtonClick: function(e) {
var data = $(e.target).closest('form').serializeObject();
var p = new Domain.Page; // this extends Backbone.Model
p.set(data);
p.save();
// ****
// **** AFTER p.save, the model p still has the OLD ATTRIBUTES... why??
// ****
return false;
}
Here is Backbone's save method:
// Set a hash of model attributes, and sync the model to the server.
// If the server returns an attributes hash that differs, the model's
// state will be `set` again.
save : function(attrs, options) {
options || (options = {});
if (attrs && !this.set(attrs, options)) return false;
var model = this;
var success = options.success;
options.success = function(resp, status, xhr) {
// ****
// **** NEXT LINE SUCCESSFULLY SETS THE MODEL WITH THE SERVER RESPONSE
// ****
if (!model.set(model.parse(resp, xhr), options)) return false;
if (success) success(model, resp, xhr);
};
options.error = wrapError(options.error, model, options);
var method = this.isNew() ? 'create' : 'update';
return (this.sync || Backbone.sync).call(this, method, this, options);
},
The save method is asynchronous. In other words, the model.set call inside save happens after the server has responded.
You are asking why the values are the same immediately after save is called. The answer is: at that point in time, the response has not been received by your code yet. The callback hasn't been called. model.set hasn't been called.
When you continue on and the event loop gets the response from the server (this may be a fraction of a second, it may be several seconds) later, your values will get set.
I think I figured out what was wrong here. And Brian you were right to say it had something to do with the async nature of the Backbone.save call. The thing is that I was using the DEBUGGER. This stops all execution. I actually don't really understand how an async call works under the hood, perhaps with threads? I assumed that after I stepped over the call to 'save' and then waited a sec, then the async part (whatever that is) of the 'save' call would execute in the background. But this is not the case. The debugger halts everything. So the options.success closure within 'save' always gets called sometime after stepping over 'save'. In short, this whole thing is due to me not understanding javascript and javascript debugging properly.
Related
When calling model.erase({failure..., success...}) the model is removed even when the server responds with a HTTP StatusCode 500. The failure listener is triggered correctly but i would expect that model is not destroyed then. I can see that it is destroyed because it gets removed from the store.
var rec = store.getAt(index);
rec.erase({
success:function(record, operation){
// Do something to notify user knows
}
failure:function(record, operation){
// correctly triggered when HTTP = 40x or 50x
// Would expect that record is still in store. Why not?
// Of course i could add it again to store with store.add(record) but is that the prefered way?
}
});
I am using an AJAX proxy in Extjs 6.0
Yes, the erase method removes the record from the store right away, without waiting for the server's response. The "hacky" way to handles you scenario will be:
set the record's dropped property to true;
save the record using the save method (it will generate a delete request but will keep the record in the store);
remove the record from the store on success, reset the dropped property to false of failure.
var record = store.getAt(index);
record.dropped = true;
record.save({
success: function() {
store.remove(record);
// do something to notify the user
}
failure: function() {
record.dropped = false;
}
});
The erase isn't really relevant here. Calling erase calls the model drop method, which marks it as pending deletion and removes it from any stores. Just because the server failed to delete it from the server doesn't necessarily mean you want it back in the store, it's still just pending deletion.
I use a interval of 10 seconds for sending a request to get the most recent data:
var pollInterval = 10000;
var poll;
poll= $interval(function()
{
getNewestData();//$resource factory to get server data
}, pollInterval );
This works fine voor 99% of the time, but if the internet speed is really slow(I have actually experienced this), It will send the next request before the current is finished. Is there a way to just skip the current interval request if the previous one is still busy? Obsiously I could just use booleans to keep the state of the request, but I wonder if there is a better(native to angular) way of doing this?
Use the $resolved property of the Resource object to check if the previous operation is done.
From the Docs:
The Resource instances and collections have these additional properties:
$promise: the promise of the original server interaction that created this instance or collection.
$resolved: true after first server interaction is completed (either with success or rejection), false before that. Knowing if the Resource has been resolved is useful in data-binding.
$cancelRequest: If there is a cancellable, pending request related to the instance or collection, calling this method will abort the request.
-- AngularJS ngResource $resource API Reference.
How about making the request, then waiting for that to complete and then wait 10 seconds before making the same request again? Something along this line:
var pollInterval = 10000;
var getNewestData = function () {
// returns data in promise using $http, $resource or some other way
};
var getNewestDataContinuously = function () {
getNewestData().then(function (data) {
// do something with the data
$timeout(function () {
getNewestDataContinuously();
}, pollInterval);
});
};
getNewestData is the function that actually makes the request and returns the data in a promise.
And once data is fetched, a $timeout is started with timer as 10 seconds which then repeats the process.
I have been given a Project which is written entirely in Backbone.js, which I am supposed to change according to our specific needs. I have been studying Backbone.js for the past 2 weeks. I have changed the basic skeleton UI and a few of the features as needed. However I am trying to understand the flow of the code so that I can make further changes.
Specifically, I am trying to search some content on Youtube. I have a controller which uses a collection to specify the url and parse and return the response. The code is vast and I get lost where to look into after I get the response. I tried to look into views but it only has a div element set. Could someone help me to proceed. I wont be able to share the code here, but a general idea of where to look into might be useful.
Code Snippet
define([
'models/youtubeModelForSearch',
'coretv/config',
'libs/temp/pagedcollection',
'coretv/coretv'
],function( youtubeModelForSearch, Config, PagedCollection, CoreTV ) {
"use strict";
return PagedCollection.extend({
model: youtubeModelForSearch,
initialize: function() {
this.url = 'http://gdata.youtube.com/feeds/api/videos/?v=2&alt=json&max-results=20';
},
fetch: function(options) {
if (options === undefined) options = {};
if (options.data === undefined) options.data = {};
//options.data.cmdc = Config.getCMDCHost();
//CoreTV.applyAccessToken(options);
PagedCollection.prototype.fetch.call(this, options);
},
parse: function(response) {
var temp = response.feed
/*temp["total"] = 20;
temp["start"] = 0;
temp["count"] = 10; */
console.log(temp);
return temp.entry;
},
inputChangeFetch: function(query) {
this.resetAll();
if(query) {
this.options.data.q = query;
// this.options.data.region = Config.api.region;
//this.options.data.catalogueId = Config.api.catalogueId;
this.setPosition(0);
}
}
});
});
Let's assume your collection endpoint is correctly set and working. When you want to get the data from the server you can call .fetch() on you collection.
When you do this, it will trigger an request event. Your views or anybody else can listen to it to perform any action.
When the data arrives from the server, your parse function is called, it is set using set or reset, depending the options you passed along fetch(). This will trigger any event related to the set/reset (see the documentation). During set/reset, the data retrieved from your server will be parsed using parse (you can skip it, passing { parse: false }.
Right after that, if you passed any success callback to your fetch, it will be called with (collection, response, options) as parameters.
And, finally, it will trigger a sync event.
If your server does not respond, it will trigger an error event instead of all this.
Hope, I've helped.
These days I'm trying to put up some tests for my first serious Backbone app. I had no problem so far with normal test but now I'm stuck trying to setting up an async test.
Basically my server API return a page with a 500 HTTP code error if I try to save a model with invalid attributes and I want to check if this trigger the right "error" state in Backbone.
I've tried to set-up the test in this way:
asyncTest("Test save Model function", function(){
expect(1);
var user = new User({});
var err_spy = this.spy();
user.on('error',err_spy);
user.save(user,{error:function(){
start();
equal( err_spy.callCount, 1, "Callback 'error' called once");
}});
});
The problem is that the error callback of the save function overrides the one in the model, so the only way to trigger it would be to do it manually:
user.trigger("error");
I don't think it is a right way to test because in my production environment there is no error callback for model's save function, but on the other hand I don't know how to tell Qunit to wait the ajax response to evaluate the test assertion.
Can someone suggest me a way to make it work? Thank you!
Something like this should do the trick. I'm going from memory here, but the sinon fake server should allow you to immediately return the 500 error state and subsequently invoke the spied-on function. You might need to tweak the server.respondWith(...) call.
asyncTest("Test save Model function", function(){
expect(1);
var user = new User({});
// Set up a fake 500 response.
var server = sinon.fakeServer.create();
server.respondWith(500, {}, "");
// Create the error callback.
var err_callback = function(){};
var err_spy = sinon.spy(err_callback);
user.save(user, {error: err_callback});
server.respond();
equal( err_spy.callCount, 1, "Callback 'error' called once");
server.restore();
});
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.