I have a custom sync method in my backbone.js app. All my models call this method, but since I override success in this method, my success methods from the individual models are no longer being called. Here's what I mean - Below is my custom sync method:
app.customSync = function(method, model, options) {
var success = options.success,
error = options.error,
customSuccess = function(resp, status, xhr) {
//call original, trigger custom event
if(con)console.log('in custom success');
success(resp, status, xhr);
},
customError = function(resp, status, xhr) {
if(con)console.log('in custom error');
error(resp, status, xhr);
};
options.success = customSuccess;
options.error = customError;
Backbone.sync(method, model, options);
};
Backbone.Model.prototype.sync = app.customSync;
Here is an example me trying to call success from a model save:
this.model.save({
success:function(model, response){
if(con)console.log('this is never called');
}
});
Does anyone know how I can still the custom sync with the custom success methods, and call success from my individual saves?
As a side note, I tried calling success msuccess in the model.save, but the msuccess was undefined in the custom sync.
The first argument to Model.save is a hash of attributes you wish to modify, the options come second and hold the success/error callbacks.
Try
this.model.save({}, {
success: function() {
console.log('save success');
}
});
And a Fiddle to see this at work http://jsfiddle.net/nikoshr/XwfTB/
Related
I'm trying to load JSON with a JS function, and then make the JSON objects that are loaded available to other functions in the same namespace. I've tried using return to serve up the array of objects retrieved, but that doesn't work. In the attached example, I've assigned the array of objects to a property in the namespaced object, but when I try to get that array of objects outside the main loadData function, all I get is null.
Here's my JS:
var myObj = {
jsonEndPoint: '/test/test.json',
dataObjects: null
}
myObj.loadData = function () {
$.ajax({
url: 'test.json',
type: 'POST',
dataType: 'json',
success: function (data) {
myObj.dataObjects = data.apiResults[0].league.season.draft.rounds[0].picks;
//console.log(myObj.dataObjects);
},
error: function (xhr, textStatus, errorThrown) {
console.log('Data Load Error: ' + textStatus);
}
});
}()
myObj.displayData = function() {
console.log(myObj.dataObjects)
}()
The full example can be seen here: http://zbl.me/test/index.html
The JSON file I'm loading is here: http://zbl.me/test/test.json
That is because JavaScript is asynchronous in nature — when you attempt to access the myObj.dataObjects in the myObj.displayData function, that object does not yet exist because the AJAX call has not been completed yet.
What you could do is that ensure that all functions that require newly added data from the AJAX call be run only when a .done() promise has been delivered from your AJAX call, by using $.when(). The logic is quite straightforward:
myObj.loadData() is now exclusively used to make the AJAX call. With regards to how we handle the done and fail events (which are previously .success() and .error() callbacks), we delegate that logic to the next function.
myObj.displayData() is now use to evaluate the promise returned by your AJAX call made with myObj.loadData(). You use $.when() to fetch the promise, and then simply chain .done() to deal with a successful call and .fail() to deal with the opposite :)
Here's your improved code:
var myObj = {
jsonEndPoint: '/test/test.json',
dataObjects: null
}
myObj.loadData = function () {
// We return the AJAX object so that we can evaluate the state later
// This is very simple :)
return $.ajax({
url: 'test.json',
type: 'POST',
dataType: 'json'
});
}()
myObj.displayData = function() {
// Instead of using the deprecated .success() and .error()
// ... we use .done() and .fail()
$.when(myObj.loadData).done(function(data) {
myObj.dataObjects = data.apiResults[0].league.season.draft.rounds[0].picks;
}).fail(function(xhr, textStatus, errorThrown) {
console.log('Data Load Error: ' + textStatus);
});
}()
If you are unsure, you can check the dummy code here: http://jsfiddle.net/teddyrised/5rbd2eqq/1/ I have used the built-in JSON response from JSfiddle to generate an artificial response, but the logic is exactly the same as yours.
Your displayData method is called even before the ajax is completed. So you either need to call displayData in the success callback of ajax or change the structure a bit so that its easy to call.
Why don't you instead do something like this
var myObj = {
jsonEndPoint: '/test/test.json',
dataObjects: null,
displayData: function() {
console.log(this.dataObjects);
},
loadData: function() {
$.ajax({
context: this,
url: 'test.json',
type: 'GET',
dataType: 'json',
success: function(data) {
this.dataObjects = data.apiResults[0].league.season.draft.rounds[0].picks;
console.log(myObj.dataObjects);
this.displayData();
},
error: function(xhr, textStatus, errorThrown) {
console.log('Data Load Error: ' + textStatus);
}
});
}
};
myObj.loadData();
Here is a demo
I'm using https://github.com/danialfarid/angular-file-upload for my file uploads. It provides a progress method that is called, when the xhr requests receives the progress event. This is from the source code of angular-file-upload:
xhr.upload.addEventListener('progress', function(e) {
deferred.notify(e);
}, false);
My problem is now, how should I test this using $httpBackend? I can test the success and error methods with
$httpBackend.expectPOST("http://localhost:9001/").respond('ok');
$httpBackend.expectPOST("http://localhost:9001/").respond(500, 'some error');
but I can't get the notify of the promise to fire. Is there a way to do this?
EDIT
The part that I want to test, is inside the progress method:
$upload.http({url: url, method: 'POST', data: file})
.progress(function(evt) {
// here is more code, that needs to be tested
self.$deferreds.upload.notify((100.0 * evt.loaded / evt.total).toFixed(2));
}).success(function(data, status, headers, config) {
self.$deferreds.upload.resolve(data);
}).error(function(response) {
self.$deferreds.upload.reject(response);
});
You want to spy on the $upload.http method and return a mocked object that lets you register progress, success and error callbacks.
spyOn($upload, 'http').andReturn({
progress: function (progressCallback) {
this.progressCallback = progressCallback;
},
success: function (errorCallback) {
this.errorCallback = errorCallback;
},
error: function (errorCallback) {
this.errorCallback = errorCallback;
}
});
Then you can call these callbacks synchronously in the tests:
it('should do something', function () {
$upload.progressCallback();
// or $upload.successCallback();
// or $upload.errorCallback();
expect('this').toBe('that');
});
We are speaking about unit-tests?
Why do you need to test 3rd-party at all?
Just mock this all part.
Probably I did not get the idea of the question, so need more code that should be covered by tests.
I have written a small Jquery plugin that makes it easy for me to implement Facebook like "likes" on any item in my application. My only issue now is that I struggle to implement the success / error callback of my plugin.
$('.user-like').like({
success: function(data, textStatus, jqXHR) {
$(this).text('Liked');
}
});
My issue with the above code is this line:
$(this).text('Liked');
I'm aware of what why the issue happens, I just can't find a good way to make it work like I want it. Let me explain how the script works and what my Goal is:
As you can see I'm passing the call along to the likeApi() function that executes an AJAX call. Further you see that I merge my Options with the defaults and that you can override the success and error callback of the AJAX object.
The issue is now that this in the above code is the scope of the AJAX call and not my original method. I want to allow the user to define his own success / error callback that depends on the result of the API call and allows me to do something based on the state if it was a success or failure so that I can change the like text for example. How can I do this?
(function ($) {
$.likeApi = function (action, options) {
if (action != 'like' && action != 'unlike') {
return false;
}
var options = jQuery.extend({}, jQuery.likeApi.defaults, options);
$.ajax({
type: "POST",
url: options.baseUrl + action + '.json',
data: {
data: {
Like: {
foreign_key: options.id,
model: options.model
}
}
},
success: options.success,
error: options.error,
dataType: 'json'
});
};
$.fn.like = function (options) {
var scopedOptions = options;
this.on('click', function(event) {
event.preventDefault();
$.likeApi('like', $.extend({}, scopedOptions,{
'id': $(event.target).data('like-fk'),
'model': $(event.target).data('like-model')
}));
});
return this;
};
$.fn.unlike = function (options) {
var scopedOptions = options;
this.on('click', function(event) {
event.preventDefault();
var result = $.likeApi('unlike', $.extend({}, scopedOptions,{
'id': $(event.target).data('like-fk'),
'model': $(event.target).data('like-model')
}));
alert(result);
});
return this;
};
$.likeApi.defaults = {
baseUrl: '/likes/likes/',
action: null,
model: null,
id: null,
error: function(jqXHR, textStatus, errorThrown) {
alert(textStatus);
},
success: function(data, textStatus, jqXHR) {
alert(textStatus);
}
};
}(jQuery));
Two options: you can maintain context by adding a variable that references the original this, or you can use jquery.proxy()
Option 1:
Maintain the context by adding a variable that references the original this like so:
(function ($) {
$.likeApi = function (action, options) {
var self = this;
Then you just call self whenever you are out of context.
If you want to keep self available externally, you can inject it using jquery extend.
http://api.jquery.com/jquery.extend/
options.success = $.extend(options.sucesss, {el: self});
inside your ajax call
$('.user-like').like({
success: function(data, textStatus, jqXHR) {
$(data.el).text('Liked');
}
});
Option 2:
Alternatively, you can use jQuery.proxy()
http://api.jquery.com/jquery.proxy/
proxy can change the this context for you...
I have a set of custom user data that I want to make an ajax call to, and in the event that there is no user data, make another ajax call to retrieve a default set of data, and then execute a function after parsing the data. Here's an example:
var oData = [],
exampleUrl = 'example.php';
$.ajax({
url: exampleUrl + '?query=getUserData',
contentType: 'application/json;odata=verbose',
headers: {
'accept': 'application/json;odata=verbose'
},
success : function(data, request){
// Request succeeded
// Check the results
if(data.length){
// There are custom user results!
// Parse the results
oData = data;
}
else{
// There were no custom user results...
// Run another query to retrieve default values
$.ajax({
url: examplUrl + '?query=getDefaultData',
contentType: 'application/json;odata=verbose',
headers: {
'accept': 'application/json;odata=verbose'
},
success : function(data, request){
// Request succeeded
// Check the results
if(data.length){
// There was some default data!
// Parse the results
oData = data;
}
else{
// No data was found...
// Attempt to be helpful
console.log('No Default data was found!');
}
},
error : function(data, request){
// There was an error with the request
// Attempt to be helpful
console.log('Error retrieving data:');
console.log(data);
console.log(request);
}
});
}
},
error : function(data, request){
// There was an error with the request
// Attempt to be helpful
console.log('Error retrieving Custom User data:');
console.log(data);
console.log(request);
},
complete : function(){
// Do something with the data
index.displayData(oData);
}
});
The issue is that if the second ajax call is run, oData doesn't contain any data at all when it's passed to index.displayData(). I'm guessing it has something to do with the asyncronous nature of ajax calls, but shouldn't 'complete' run after everything inside of 'success' runs?
I also know I probably shouldn't be using the ajax "Pyramid of Doom" and should be using promises, but I've tried them and keep getting the same results.
Thank you for your assistance!
As pointed out by Violent Crayon, you could try calling "complete" yourself instead of relying on JQuery's implicit control flow:
function getData(exampleUrl, onComplete){
$.ajax({
success : function(data, request){
if(data.length){
onConplete(data);
}else{
$.ajax({
success : function(data, request){
if(data.length){
onComplete(data);
}else{
console.log('No Default data was found!');
}
},
error : function(data, request){
console.log('Error retrieving data:');
}
});
}
},
error : function(data, request){
console.log('Error retrieving Custom User data:');
}
});
}
var oData = [];
getData('example.php', function(data){
oData = data;
index.displayData(oData);
}
BTW, note how you can have your async functions receive their own return and error callbacks. This can help reduce the pyramid of doom problem without needing to use promises and without needing to hardcode the return callback.
By working with promises, you can avoid the need to pass a callback into your function, and by defining a utility function you can avoid repetition of code.
//reusable utility function, which returns either a resolved or a rejected promise
function fetchData(queryString, cache) {
return $.ajax({
url: 'example.php',
data: { query: queryString },
type: 'JSON',//assumed
cache: cache,
contentType: 'application/json;odata=verbose',
headers: { 'accept': 'application/json;odata=verbose' }
}).then(function(data, textStatus, jqXHR) {
if (data && data.length) {
return data;
} else {
return $.Deferred().reject(jqXHR, 'no data returned').promise();//emulate a jQuery ajax failure
}
});
}
This allows promise methods to be used for a control structure, which :
is concise
uses chaining, not nesting
gives meaningful error messages.
//control structure
fetchData('getUserData', false).then(null, function(jqXHR, textStatus) {
console.log('Error retrieving Custom User data: ' + textStatus);
return fetchData('getDefaultData', true);
}).then(index.displayData, function(jqXHR, textStatus) {
console.log('Error retrieving default data: ' + textStatus);
});
Notes :
the null in .then(null, function(){...}) allows a successful response to drop straight through to the second .then(index.displayData, ...)
default data is cached while the user data is not. This is not necessary to make things work but will be faster next time the default data is required.
in the world of promises, this or something similar is the way to go.
Creating a custom sync() method in backbone.
I would like to do this the "right" and interfere with Backbone's normal functions as little as possible.
This is the code that I have so far:
var CustomSyncModel = Backbone.Model.extend({
sync:function(method, model, options){
var params = {
type: 'POST'
url: model.url(),
error: function(jqXHR, textStatus, errorThrown){
alert('error');
},
success: function(data, textStatus, jqXHR){
model.parse(data);
}
};
// Got this from line 1359 in Backbone.js developement library
// version 0.9.2:
$.ajax(_.extend(params, options));
}
});
The issue that I am having is that the line: $.ajax(_.extend(params, options)); seems to be overwriting the custom success and error functions that I created. But I'm also concerned about interfering with any custom callbacks or other functionality that may have been specified elsewhere in the application that is using this model.
What is the "correct" way to go about overriding the Backbone's sync() method?
Thanks!
If you look at Model#fetch you'll see the usual approach that Backbone uses:
fetch: function(options) {
//...
var success = options.success;
options.success = function(resp, status, xhr) {
if (!model.set(model.parse(resp, xhr), options)) return false;
if (success) success(model, resp);
};
//...
}
So Backbone just replaces the function with a new one that calls the original. In your case, you'd have something like this:
// We don't own options so we shouldn't modify it,
// but we can do whatever we want to a clone.
options = _(options).clone()
// Replace options.error with a wrapper.
var error = options.error;
options.error = function(jqXHR, textStatus, errorThrown) {
alert('error');
if(error)
error(jqXHR, textStatus, errorThrown);
};
// Replace options.success with a wrapper.
var success = options.success;
options.success = function(data, textStatus, jqXHR) {
model.parse(data);
if(success)
success(data, textStatus, jqXHR);
};
// We don't need error or success in here anymore.
var params = {
type: 'POST',
url: model.url()
};
$.ajax(_.extend(params, options));
BTW, your model.parse(data); in your success handler probably doesn't do anything useful, parse should just be a simple filter so you'd want to do something (such as a model.set call) with the model.parse(data) return value.