I'm using Backbone.js with backbone.validation plugin to build a custom validator that checks if the email address (entered in a form input) is already taken.
The new validator is called emailAvailable and you can see below:
(notice: it's Coffescript, but at the bottom you'll find the code converted to standard javascript)
# ==================================
# MODELS
# ==================================
User = Backbone.Model.extend(
urlRoot: "/user"
validation:
email:
fn: "emailAvailable"
emailAvailable: (value, attr, computedState) ->
// Ajax call to server (Play framework 2.2.1): returns the string "email available" if it doesn't find the email and returns the email address if it find it
checkEmail = $.ajax(jsRoutes.controllers.Signup.isEmailExists(value))
checkEmail.done (msg) ->
emailFound = msg
if value is emailFound
return "already taken"
return
)
# ==================================
# VIEWS
# ==================================
SignUpView = Backbone.View.extend(
initialize: ->
Backbone.Validation.bind(this)
el: "body"
events:
"change input" : "validateInput"
validateInput: (event) ->
input = $(event.currentTarget)
inputName = event.currentTarget.name
inputValue = input.val()
this.model.set(inputName, inputValue)
if this.model.isValid(inputName)
input.removeClass "error"
input.addClass "valid"
else
input.removeClass "valid"
input.addClass "error"
...
This doesn't work and I can't get why. Where am I wrong?
EDIT: code converted to javascript
var SignUpView, User;
User = Backbone.Model.extend({
urlRoot: "/user",
validation: {
email: {
fn: "emailAvailable"
}
},
emailAvailable: function(value, attr, computedState) {
var checkEmail;
checkEmail = $.ajax(jsRoutes.controllers.Signup.isEmailExists(value));
checkEmail.done(function(msg) {
var emailFound;
emailFound = msg;
if (value === emailFound) {
return "already taken";
}
});
}
});
SignUpView = Backbone.View.extend({
initialize: function() {
return Backbone.Validation.bind(this);
},
el: "body",
events: {
"change input": "validateInput"
},
validateInput: function(event) {
var input, inputName, inputValue;
input = $(event.currentTarget);
inputName = event.currentTarget.name;
inputValue = input.val();
this.model.set(inputName, inputValue);
if (this.model.isValid(inputName)) {
input.removeClass("error");
return input.addClass("valid");
} else {
input.removeClass("valid");
return input.addClass("error");
}
}
});
Backbone.Validation sadly doesn't support asynchronous validation functions. This is basically limitation of default backbone validation flow. It was designed with only synchronous way of validation in mind.
You have basically 2 options:
specify async:false option for ajax call
implement your own validation flow for this case
I personally would go with option 2, since synchronous ajax calls will lock browser until call is completed.
Update note:
I did quick google search after I answered this question, and it looks like there is extension for Backbone.Validation, which allows asynch validation. Please note, that I haven't used, nor tested it in any way :)
Link: https://github.com/suevalov/async.backbone.validation
You will have to do your async validation function yourself. Here is how you can do it without any plugins, just a little logic and good old coding.
1) Let's start where your function that will save your model is. You will need a Deferred object, like this, before saving happens:
this.asyncValidation = $.Deferred();
2) Then you will have to manually call your own custom validate function:
this.model.asyncValidate();
3) Then you will have to wait until your async validation is done. Once it's done, just save your model:
$.when(this.checkDuplicatesFinished).done(function () {
4) Check your own model attribute that has the result of your own async validation:
if (self.model.asyncValidateOK) {
5) Save your model:
self.model.save();
Here is the code altogether:
this.asyncValidation = $.Deferred();
this.model.asyncValidate(asyncValidation);
var self = this;
$.when(this.checkDuplicatesFinished).done(function () {
if (self.model.asyncValidateOK) {
self.model.save();
}
});
Now, let's see your Model. Here is where the new custome validate function is going to be located. It's pretty much straightforward, you will need a boolean variable to store the result of the validation and the validate method itself.
window.app.MyModel = Backbone.Model.extend({
asyncValidateOK: true,
asyncValidate: function (defferedObject) {
var self = this;
var attrs = this.attributes; // a copy of the params that are automatically passed to the original validate method
$.get("someAsyncMethod", {}, function(result){
// do your logic/validation with the results
// and tell the referred object you finished
if (true){ //validation ok (use your own logic)
self.asyncErrors = []; // no errors
self.asyncValidateOK = true;
deferredObject.resolve();
} else { // validation not ok
self.asyncErrors.push();
self.asyncValidateOK = false;
deferredObject.resolve();
}
});
}
});
For more documentation check http://backbonejs.org/, but there's nothing related to this. Hope this helps anyone that ends up trying to async validate.
Related
I have a Jasmine 2.0.2 test that triggers an ajax request, but each time the request is fired, the mock ajax return should be a specific return value.
var setUpDeleteEventInAjax = function(spyEvent, idToReturn){
var spy;
spy = jasmine.createSpy('ajax');
spyAjaxEvent = spyOnEvent(spyEvent, 'click');
spyOn($, 'ajax').and.callFake(function (param) {
return {
id: idToReturn, // here I am trying to return a defined value
status: true
};
});
spyAjaxEvent.reset(); // this should reset all ajax evetns
};
...
beforeEach(function(){...})
afterEach(function(){...})
...
it('Deleting all the addresses should reveal the form', function () {
setUpDeleteEventInAjax('#delete',52670);
$('#delete').click();
expect($('.address-item').length).toEqual(4);
setUpDeleteEventInAjax('#delete-2',52671);
$('#delete-2').click();
expect($('.address-item').length).toEqual(2);
setUpDeleteEventInAjax('#delete-3',52672);
$('#delete-3').click();
expect($('.address-item').length).toEqual(0);
});
...
After the delete button is clicked ( delete,delete-2,delete-3 ) the total length address-items is reduced by 2, ( when the return from the server responds with a number - this is the crux of the mock).
However, jasmine complains that "ajax has already been spied upon." Is there a way to return a new value from the ajax mock in order to fulfill the test?
Actually the way I went about it was over engineered. Just needed a closure. Did not need my custom setUpDeleteEventInAjax function
it('Deleting all the addresses should reveal the form', function () {
var responses = [52670, 52671, 52672];
var ajaxResponses = function () {
return {
status: true,
id: responses.shift()
}
};
spyOn($, 'ajax').and.callFake(ajaxResponses);
$('#delete').click();
expect($('.address-item').length).toEqual(4);
$('#delete-2').click();
expect($('.address-item').length).toEqual(2);
expect($('.address-book')).toHaveClass('single-address');
$('#delete-3').click();
expect($('.address-item').length).toEqual(0);
});
I have a search form with a number of empty fields. After the user enters values into the fields and hits "Enter" I create a query string from the values (using $.param()) and send that to the server. All is well, until you empty a field and hit "Enter" again. Because that field previously had a value the attribute has already been set, so now when hitting submitting the attribute is still being added to the query string, only with no value attached.
An example, and some code:
Search.CustomerCard = Marionette.ItemView.extend({
template: "#customer-search-card",
tagName: "section",
className: "module primary-module is-editing",
initialize: function () {
this.model.on('change', function(m, v, options) {
console.log("Changed?", m.changedAttributes();
});
},
events: {
"click .js-save": "searchCustomers",
"keypress [contenteditable]": "searchCustomersOnEnter"
},
bindings: {
…
},
onRender: function () {
this.stickit();
},
searchCustomersOnEnter: function (e) {
if (e.keyCode !== 13) { return; }
e.preventDefault();
this.searchCustomers(e);
},
searchCustomers: function (e) {
e.preventDefault();
$(e.target).blur();
var query = $.param(this.model.attributes);
this.trigger("customers:search", query);
}
});
You'll see in the var query = $.param(this.model.attributes); line that I'm converting the model's attributes to a query string to send along to my API. You may also see I'm using backbone.stickit. How can I seamlessly unset any attributes that are empty before creating the query string?
In this scenario I'd just fix the input to $.param:
var params = _.clone(this.model.attributes);
_.each(params, function(value, key) {
if (value == "") {
delete params[key];
}
});
$.param(params);
There seems to be no proper utility available at Underscore, but could be easily provided similar to what is described here.
I ran into a similar problem when i was making my project, for me _.defaults worked, but maybe in your case, you can use one of filter, reduce or anyother underscore functions.
Wondering if any one can help me out here.
I have a single page on the client side running backbone.js
The server is running socket.io.
I have replaced Backbones default sync with the backbone.iobind sync to support socket.io.
I have a backbone model that on save gets an error response from the server (which is planned), and triggers the save's error function, however my model on the client side still updates to the new value. I have included wait:true which from my understanding waits for a success response from the server before updating, however this does not happen for me.
Server side socket.io code
io.sockets.on('connection', function (socket) {
socket.on('test:create', function (data, callback) {
//err - used to fake an error for testing purposes
var err = true;
if (err) {
// error
callback('serv error');
} else {
// success
// The callback accepts two parameters: error and model. If no error has occurred, send null for error.
callback(null, data);
}
});
Client Side Code
//HTML Code
//<div id="callBtns">
//<button id="runTest">Create</button>
//</div>
var CommunicationType = Backbone.Model.extend({
defaults: {
runTest: 'default'
},
urlRoot : '/test',
validate: function(attrs) {
//return 'error';
}
});
var BasicView = Backbone.View.extend({
el: $('#callBtns'),
initialize: function(){
_.bindAll(this);
},
events: {
"click #runTest": "runTestFn"
},
runTestFn: function() {
//causing the issue?
communicationType.set({runTest: 'value from set'});
communicationType.save({},{
success:function(model, serverResponse){
console.log('Success');
console.log(serverResponse);
},
error:function(model, serverResponse){
console.log('Error');
console.log(serverResponse);
console.log(JSON.stringify(communicationType));
},
wait: true
});
}
});
var communicationType = new CommunicationType();
var basicView = new BasicView();
I have noticed that this problem looks to be caused by the fact that I'm using set to update my model directly (communicationType.set({runTest: 'value from set'});)
If I don't set any values before saving, and pass the value in directly to save, like in the code below, it works correctly e.g.
communicationType.save({runTest: 'value from save'},{
success:function(....
However this then means that I can't ever set any new values/update my model without it going via the save function :/
So my question is, how can I get this working correctly (get the client model to not update if there is a server-side error) while still being able to use set?
Any answers are really appreciated! Many Thanks
This can't be done because your model has no way of knowing if it's data is valid until a request is made (model.save in this case).
If you want to prevent Backbone.Model.set from working when you pass invalid values, you must update the model's "validate" function to check for errors.
Given the following validate function:
validate: function(attrs) {
this.errors = [];
if (attrs.runTest === 'wrong_value') {
this.errors.push('error message');
}
return _.any(this.errors) ? this.errors : null;
}
on your CommunicationType model. Running this code in BasicView:
communicationType.set({runTest: 'wrong_value'});
will not change the model.
You can also use the validate function to check for server side errors by returning a errors array and checking for it:
validate: function(attrs) {
this.errors = [];
if (attrs.runTest === 'wrong_value') { // client side check
this.errors.push('error message');
}
if (attrs.errors && attrs.errors.length > 0) { // server side check
for (var key in attrs.errors) {
this.errors.push(attrs.errors[key]);
}
}
return _.any(this.errors) ? this.errors : null;
}
But this will only prevent changing the model when it is saved.
As a hack-ish alternative, you can add server requests inside the validate function that check for errors and prevent the "set" function from changing the model.
You may use set with silent: true so it won't fire change event.
I'm trying to create a backbone model using:
var newUser = new App.User({
name: $('input[name=new_user_name]').val(),
email: $('input[name=new_user_email]').val()
});
with the following validation:
App.User = Backbone.Model.extend({
validate: function(attrs) {
errors = [];
if (_.isEmpty(attrs.name)) {
errors.push("Name can't be blank");
}
if (_.isEmpty(attrs.email)) {
errors.push("Email can't be blank");
}
return _.any(errors) ? errors : null;
},
...
});
However, when the textbox values are empty, the model can't be created because the validations fail.
Is there a way to create a model and run isValid() to check the values are correct? Or is there some other way to run validations before the model is created?
From the backbone model creation code, it appears it throws the exception Error("Can't create an invalid model") upon failing validation, but would it be possible to check validations before hand?
You can create unvalid model with silent: true, but in this way collection don`t trow event 'add'. But you can sent default option to function add and catch this option in validate()
collection.add({option:'unvalid_value'}, {validation: false});
Model = Backbone.Model.extend({
validate: function(attributes, options){
if (options.validation === false)
return;
//Validation code
//.....
}
});
This way you can create model with invalid hash)
My understanding is that you want to create a user model and that model gets populated with attributes based on some form input.
The way you have it set up now, to me doesn't seem very backbone-y.
var newUser = new App.User({
name: $('input[name=new_user_name]').val(),
email: $('input[name=new_user_email]').val()
});
To me it feels like you're mixing up stuff that happens in your views with your model which is the object it should represent. I would probably create a basic blank Model first, then a View that compliments this model and pass the model in.
User = Backbone.Model.extend ({
defaults: {
name: undefined,
email: undefined
}
)};
UserView = Backbone.View.extend ({
events: {
// like click on submit link, etc.
'click #submitButton': 'onSubmit'
},
initialize: function() {
this.user = this.model;
},
onSubmit: function() {
// run some validation on form inputs, ex. look for blanks, then...
if ( /* validation passes */ ) {
var nameEntered = $('input[name=new_user_name]').val();
var emailEntered = $('input[name=new_user_email]').val();
this.user.set({name:nameEntered, email:nameEntered});
}
}
});
// Do it!
var newUser = new User();
var newUserView = new UserView({model:newUser});
One great way to validate the form INPUT rather than than the Model attributes is to use jquerytools validator. It works with HTML5 attributes such as pattern=regex and type=email and makes it super easy.
You cannot create an Invalid model in backbone.
If you are creating a model and passing values to the constructor the no validation will be done.
If you are adding models to a collection via fetch then invalid data will cause the model validation to fail and the model will not be added to the collection. This should trigger the error event so you should be listening for it in case your server is providing invalid data back.
If you want to make a model but make sure the data is valid you must:
var mymodel = new MyModel();
mymodel.set(attrs);
Even if you try to pass {silent: false} during model instantation backbone will override it since it will not let you create an invalid model and does not throw errors. (which we arent discussing here)
You can see this by looking at the source, but also look at this github ticket
I've developed a nice rich application interface using Backbone.js where users can add objects very quickly, and then start updating properties of those objects by simply tabbing to the relevant fields. The problem I am having is that sometimes the user beats the server to its initial save and we end up saving two objects.
An example of how to recreate this problem is as follows:
User clicks the Add person button, we add this to the DOM but don't save anything yet as we don't have any data yet.
person = new Person();
User enters a value into the Name field, and tabs out of it (name field loses focus). This triggers a save so that we update the model on the server. As the model is new, Backbone.js will automatically issue a POST (create) request to the server.
person.set ({ name: 'John' });
person.save(); // create new model
User then very quickly types into the age field they have tabbed into, enters 20 and tabs to the next field (age therefore loses focus). This again triggers a save so that we update the model on the server.
person.set ({ age: 20 });
person.save(); // update the model
So we would expect in this scenario one POST request to create the model, and one PUT requests to update the model.
However, if the first request is still being processed and we have not had a response before the code in point 3 above has run, then what we actually get is two POST requests and thus two objects created instead of one.
So my question is whether there is some best practice way of dealing with this problem and Backbone.js? Or, should Backbone.js have a queuing system for save actions so that one request is not sent until the previous request on that object has succeeded/failed? Or, alternatively should I build something to handle this gracefully by either sending only one create request instead of multiple update requests, perhaps use throttling of some sort, or check if the Backbone model is in the middle of a request and wait until that request is completed.
Your advice on how to deal with this issue would be appreciated.
And I'm happy to take a stab at implementing some sort of queuing system, although you may need to put up with my code which just won't be as well formed as the existing code base!
I have tested and devised a patch solution, inspired by both #Paul and #Julien who posted in this thread. Here is the code:
(function() {
function proxyAjaxEvent(event, options, dit) {
var eventCallback = options[event];
options[event] = function() {
// check if callback for event exists and if so pass on request
if (eventCallback) { eventCallback(arguments) }
dit.processQueue(); // move onto next save request in the queue
}
}
Backbone.Model.prototype._save = Backbone.Model.prototype.save;
Backbone.Model.prototype.save = function( attrs, options ) {
if (!options) { options = {}; }
if (this.saving) {
this.saveQueue = this.saveQueue || new Array();
this.saveQueue.push({ attrs: _.extend({}, this.attributes, attrs), options: options });
} else {
this.saving = true;
proxyAjaxEvent('success', options, this);
proxyAjaxEvent('error', options, this);
Backbone.Model.prototype._save.call( this, attrs, options );
}
}
Backbone.Model.prototype.processQueue = function() {
if (this.saveQueue && this.saveQueue.length) {
var saveArgs = this.saveQueue.shift();
proxyAjaxEvent('success', saveArgs.options, this);
proxyAjaxEvent('error', saveArgs.options, this);
Backbone.Model.prototype._save.call( this, saveArgs.attrs, saveArgs.options );
} else {
this.saving = false;
}
}
})();
The reason this works is as follows:
When an update or create request method on a model is still being executed, the next request is simply put in a queue to be processed when one of the callbacks for error or success are called.
The attributes at the time of the request are stored in an attribute array and passed to the next save request. This therefore means that when the server responds with an updated model for the first request, the updated attributes from the queued request are not lost.
I have uploaded a Gist which can be forked here.
A light-weight solution would be to monkey-patch Backbone.Model.save, so you'll only try to create the model once; any further saves should be deferred until the model has an id. Something like this should work?
Backbone.Model.prototype._save = Backbone.Model.prototype.save;
Backbone.Model.prototype.save = function( attrs, options ) {
if ( this.isNew() && this.request ) {
var dit = this, args = arguments;
$.when( this.request ).always( function() {
Backbone.Model.prototype._save.apply( dit, args );
} );
}
else {
this.request = Backbone.Model.prototype._save.apply( this, arguments );
}
};
I have some code I call EventedModel:
EventedModel = Backbone.Model.extend({
save: function(attrs, options) {
var complete, self, success, value;
self = this;
options || (options = {});
success = options.success;
options.success = function(resp) {
self.trigger("save:success", self);
if (success) {
return success(self, resp);
}
};
complete = options.complete;
options.complete = function(resp) {
self.trigger("save:complete", self);
if (complete) {
return complete(self, resp);
}
};
this.trigger("save", this);
value = Backbone.Model.prototype.save.call(this, attrs, options);
return value;
}
});
You can use it as a backbone model. But it will trigger save and save:complete. You can boost this a little:
EventedSynchroneModel = Backbone.Model.extend({
save: function(attrs, options) {
var complete, self, success, value;
if(this.saving){
if(this.needsUpdate){
this.needsUpdate = {
attrs: _.extend(this.needsUpdate, attrs),
options: _.extend(this.needsUpdate, options)};
}else {
this.needsUpdate = { attrs: attrs, options: options };
}
return;
}
self = this;
options || (options = {});
success = options.success;
options.success = function(resp) {
self.trigger("save:success", self);
if (success) {
return success(self, resp);
}
};
complete = options.complete;
options.complete = function(resp) {
self.trigger("save:complete", self);
//call previous callback if any
if (complete) {
complete(self, resp);
}
this.saving = false;
if(self.needsUpdate){
self.save(self.needsUpdate.attrs, self.needsUpdate.options);
self.needsUpdate = null;
}
};
this.trigger("save", this);
// we are saving
this.saving = true;
value = Backbone.Model.prototype.save.call(this, attrs, options);
return value;
}
});
(untested code)
Upon the first save call it will save the record normally. If you quickly do a new save it will buffer that call (merging the different attributes and options into a single call). Once the first save succeed, you go forward with the second save.
As an alternative to the above answer, you could achieve the same affect by overloading the backbone.sync method to be synchronous for this model. Doing so would force each call to wait for the previous to finish.
Another option would be to just do the sets when the user is filing things out and do one save at the end. That well also reduce the amount of requests the app makes as well