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.
Related
I have been trying to access some methods of the same class from an AJAX call inside one of the methods but it does not work. What could be the issue here? I get this error Uncaught TypeError: this.createTimeline is not a function see the comments on the code below!
//sequenceRender is the class and below is one method
sequenceRender.prototype.ajaxSequence = function(){
this.ajaxSequence = $.ajax('getSequence.php', {
dataType: 'json',
timeout: 2000
});
this.ajaxSequence.done(function (data, status, jqXhr) {
console.log(data)
this.SEQUENCE=data // I cannot access properties
this.createTimeline() // or methods from same class
this.createWells() // from inside here
})
this.ajaxSequence.fail(function (jqXhr, textStatus, errorMessage) {
console.log(errorMessage)
})
}
Looks like this is going to have different values as you are trying to call this inside a event
Try adding the changes below and let me know if this works for you
sequenceRender.prototype.ajaxSequence = function() {
mainContext = this;
this.ajaxSequence = $.ajax('getSequence.php', {
dataType: 'json',
timeout: 2000
});
this.ajaxSequence.done(function (data, status, jqXhr) {
console.log(data)
mainContext.SEQUENCE=data
mainContext.createTimeline() // call the 'this' from here
mainContext.createWells()
})
this.ajaxSequence.fail(function (jqXhr, textStatus, errorMessage) {
console.log(errorMessage)
})
}
I am making a basic trello clone. Except instead of signing in, projects have a slug(i.e. 'www.example.com/1d754b6c')
If a user visits the root, a new slug is created on the back end. The user is then routed to www..com/1d754b6c, which sends another ajax call to get the projects ID. A view is then started. However my view is getting started before the slug -> ID ajax call is finished. Whats the best way to fix this? (I currently have a setTimeout as a temporary patch, I know that is not a good way to accomplish this)
router.js
Buckets.Routers.PageRouter = Backbone.Router.extend({
routes: {
'': 'newProject',
':token': 'displayProject'
},
newProject: function () {
new Buckets.Models.Project({});
},
displayProject: function (token) {
var that = this;
var project = new Buckets.Models.Project({token: token});
setTimeout(function(){
new Buckets.Views.showProject({
model: project
});
}, 500);
}
});
project.js
Buckets.Models.Project = Backbone.Model.extend({
url: function() {
return Buckets.BASE_URL + '/api/projects/' + (this.id)
},
initialize: function(options) {
var that = this;
if (options && options.token) {
that.token = options.token
$.ajax({
url: Buckets.BASE_URL + '/' + that.token,
dataType: 'json',
success: function( data, status ){
that.id = data;
},
error: function(xhr, textStatus, err) {
console.log(xhr);
}
});
} else {
$.ajax({
url: Buckets.BASE_URL + '/api/projects/new',
dataType: 'json',
success: function( data, status ){
that.token = data.token;
that.id = data.id;
Buckets.Routers.router.navigate('/' + that.token, true);
},
error: function(xhr, textStatus, err) {
console.log(xhr);
}
});
}
return this;
},
});
Try to use Backbone.Model.sync. .sync() returns a Promise so you can take full advantage of the Deffered/Promises standard.
When I want to pass variable urls to the fetch I override the model.fetch(). For your implemenation I'd first scrap the $.ajax in initialize() and override fetch like this
Buckets.Models.Project = Backbone.Model.extend({
fetch: function(options) {
var that = this;
if (options && options.token) {
this.url = Buckets.BASE_URL + '/' + that.token;
else
this.url = Buckets.BASE_URL + '/api/projects/new';
return Backbone.Model.prototype.fetch.call(this, options);
}
.fetch() eventually returns the result of sync() which is a Promise. That means that in your Router you'd do this:
displayProject: function (token) {
var that = this;
var project = new Buckets.Models.Project();
$.when(project.fetch({token: token})
// deffered.done() replaces the success callback in your $.ajax
.done(function() {
project.id = data;
new Buckets.Views.showProject({ model: project });
})
// deffered.fail() replaces the error callback in your $.ajax
.fail(function( jqXHR, textStatus, errorThrown) {
console.log(jqXHR);
});
}
And for completeness, you'd reweite newProject() similarly,
newProject: function () {
var project = new Buckets.Models.Project();
$.when(project.fetch({token: token})
.done(function( data, textStatus, jqXHR ) {
project.token = data.token;
project.id = data.id;
new Buckets.Views.showProject({ model: project });
})
.fail(function( jqXHR, textStatus, errorThrown) {
console.log(jqXHR);
});
}
Try it out. I started using this method of fetching when it was recommended to me by a major contributor to MarionetteJS, one of the premier opinionated Backbone frameworks. This method is easy to maintain and very responsive.
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'm calling a function, which makes an ajax GET to a url, like this:
// parameters = url, callback, boolean
that.mapUrl( window.location.search, function(spec) {
console.log("initial mapping done");
console.log(spec);
// do stuff
}, true);
mapUrl will trigger an Ajax request. Inside the Ajax done or success handler, I want to trigger my callback function, but doing it like this:
$.ajax({
method: 'GET',
url: obj[1],
context: $('body')
}).fail(function (jqXHR, textStatus, errorThrown) {
console.log("FAILED");
configuration = {
"errorThrown":errorThrown,
"textStatus": textStatus,
"jqXHR": jqXHR
}
}).done(function(value, textStatus, jqXHR) {
console.log("OK");
console.log(callback) // undefined!
configuration = {
"value":value,
"textStatus": textStatus,
"jqXHR": jqXHR
}
});
Question:
So I'm wondering how to pass my callback function into the ajax done-callback. Any idea how to do this?
Thanks!
EDIT
Here is the full mapURL function
that.mapUrl = function (spec, callback, internal) {
var key,
obj,
parsedJSON,
configuration = {"root" : window.location.href};
if (spec !== undefined && spec !== "") {
obj = spec.slice(1).split("=");
key = obj[0];
console.log(key);
switch (key) {
case "file":
$.ajax({
method: 'GET',
url: obj[1],
context: $('body')
}).fail(function (jqXHR, textStatus, errorThrown) {
console.log("FAILED");
configuration = {
"errorThrown":errorThrown,
"textStatus": textStatus,
"jqXHR": jqXHR
}
}).done(function(value, textStatus, jqXHR) {
console.log("OK");
configuration = {
"value":value,
"textStatus": textStatus,
"jqXHR": jqXHR
}
});
break;
default:
// type not allowed, ignore
configuration.src = [];
break;
}
}
return configuration;
};
It would be generally better to preserve the "promise" interface rather than passing callbacks into your code. This will allow you to better trap error conditions.
function mapUrl(url) {
return $.ajax(...)
.fail(...)
.then(function(data) {
// preprocess data and return it
});
}
where using .then you can manipulate the returned data before it's passed to the callback:
mapUrl(...).done(function(data) {
// data has been preprocessed
...
});
If the AJAX call fails, you can chain additional .fail handlers at this point too, which your current API would not permit. This "separation of concerns" would let you put nicer error handling UI in place, for example, without cluttering your AJAX code with UI-related code.
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/