Backbone.js newbie - cannot get model error event to fire - backbone.js

I am looking at backbone.js for the first time. I'm currently looking at model validation, but this test script which I've taken directly from a teaching text is not firing the error event as expected.
Person = Backbone.Model.extend({
// If you return a string from the validate function,
// Backbone will throw an error
validate: function(attributes) {
if (attributes.age < 0 && attributes.name != "Dr Manhatten") {
return "You can't be negative years old";
}
},
initialize: function() {
alert("Welcome to this world");
this.bind("error", function(model, error) {
// We have received an error, log it, alert it or forget it :)
alert(error);
});
}
});
var person = new Person;
person.set({ name: "Mary Poppins", age: -1 });
// Will trigger an alert outputting the error
var person = new Person;
person.set({ name: "Dr Manhatten", age: -1 });
// God have mercy on our souls
My page for testing this is dead simple as follows:
<html>
<body>
<script type="text/javascript" src="Scripts/jquery-1.9.1.min.js"></script>
<script type="text/javascript" src="Scripts/underscore-min.js"></script>
<script type="text/javascript" src="Scripts/backbone-min.js"></script>
<script type="text/javascript" src="Scripts/test4.js"></script>
</body>
</html>
All I'm seeing are the two "Welcome to this world" alerts. Any ideas?

Your source code is probably based on an older version of Backbone, Model.validate changed quite a bit over time.
From the changelog
0.9.10 : Model validation is now only enforced by default in Model#save and no longer enforced by default
upon construction or in Model#set, unless the {validate:true} option is passed.
and
validate model.validate(attributes, options)
This method is left undefined, and you're encouraged to override it with your custom
validation logic, if you have any that can be performed in JavaScript.
By default validate is called before save, but can also be called
before set if {validate:true} is passed. [...] Failed validations trigger an "invalid" event.
So :
to validate your model when setting attributes, use a validate:true option
bind to the invalid event
and use Events.on instead of the deprecated bind
Your code could look like this
Person = Backbone.Model.extend({
// If you return a string from the validate function,
// Backbone will throw an error
validate: function(attributes) {
if (attributes.age < 0 && attributes.name != "Dr Manhatten") {
return "You can't be negative years old";
}
},
initialize: function() {
console.log("Welcome to this world");
this.on("invalid", function(model, error) {
// We have received an error, log it, alert it or forget it :)
console.log(error);
});
}
});
var person = new Person;
person.set({ name: "Mary Poppins", age: -1 }, {validate:true});
// Will trigger an alert outputting the error
var person = new Person;
person.set({ name: "Dr Manhatten", age: -1 }, {validate:true});
And a Fiddle http://jsfiddle.net/nikoshr/udm8A/ (be sure to open a console, I converted the alert calls to console.log)

Related

I'm not calling $apply explicitly but still get Error: [$rootScope:inprog] $apply already in progress

In a angular factory I have a method to create a new item, which has a connection to a user and a price to add to that users "items" array (like a shopping cart). So I have to see if the user is present in my the local users array if not then on the server and if not then create the user.
Code looks like this:
var saveItem = function (item) {
var user = filterUserById(item.ownerId);
if (user) {
user.createItem(item);
} else {
repository.getUserById(item.ownerId).then(
function (serverUser) {
var userViewModel = repository.getUserViewModel(serverUser);
userViewModel.createItem(item);
users.push(userViewModel);
}
, function () {
user = {
id: item.ownerId,
items: [
createItemDto(item)
]
};
repository.createUser({ id: user.id }, user);
users.push(repository.getUserViewModel(user));
});
}
};
No matter which of the "cases" occurs (user was found localy, on the server or was created and added) I get an error:
Error: [$rootScope:inprog] $apply already in progress
http://errors.angularjs.org/1.3.0-beta.18/$rootScope/inprog?p0=%24apply
I recon this may have to do with the fact that I'm using resources in my repository, but I don't think resource should (since it's a part of angular..). Here's the user.createItem method, code:
user.createItem = function (item) {
var resource = userResource
, itemDto = createItemDto(item)
, command = [{
Type: 'add',
Name: 'items',
Value: itemDto
}];
resource.createItem({ id: item.ownerId }, command);
this.items.push(itemDto);
};
Y U NO WERK!? PLS HLP! :'(
P.S. I don't have any explicit calls to apply, compile or digest anywhere in my code.
Found the problem! I had put a small code line to set focus on the correct input after the item was added and form was emptied. This consisted of a
$('selector').focus();
This was colliding with digest cycle... Solution:
$timeout($('selector').focus());
Try wrapping your call to user.createItem(item) in a $timeout function:
$timeout(function() {
user.createItem(item);
}, 0);
It's possible you could be triggering some other call to $scope.$apply() some other way.
Alternatively, try using $scope.$evalAsync(function())
Here's some good info: inprog

Backbone.Model: set collection as property

I'm new with backbone and faced the following problems. I'm trying to emulate some sort of "has many relation". To achieve this I'm adding following code to initialize method in the model:
defaults: {
name: '',
tags: []
},
initialize: function() {
var tags = new TagsCollection(this.get('tags'));
tags.url = this.url() + "/tags";
return this.set('tags', tags, {
silent: true
});
}
This code works great if I fetch models through collection. As I understand, first collection gets the data and after that this collection populates models with this data. But when I try to load single model I get my property being overridden with plain Javascript array.
m = new ExampleModel({id: 15})
m.fetch() // property tags get overridden after load
and response:
{
name: 'test',
tags: [
{name: 'tag1'},
{name: 'tag2'}
]
}
Anyone know how to fix this?
One more question. Is there a way to check if model is loaded or not. Yes, I know that we can add callback to the fetch method, but what about something like this model.isLoaded or model.isPending?
Thanks!
"when I try to load single model I get my property being overridden with plain Javascript array"
You can override the Model#parse method to keep your collection getting overwritten:
parse: function(attrs) {
//reset the collection property with the new
//tags you received from the server
var collection = this.get('tags');
collection.reset(attrs.tags);
//replace the raw array with the collection
attrs.tags = collection;
return attrs;
}
"Is there a way to check if model is loaded or not?"
You could compare the model to its defaults. If the model is at its default state (save for its id), it's not loaded. If it doesn't, it's loaded:
isLoaded: function() {
var defaults = _.result(this, 'defaults');
var current = _.wíthout(this.toJSON(), 'id');
//you need to convert the tags to an array so its is comparable
//with the default array. This could also be done by overriding
//Model#toJSON
current.tags = current.tags.toJSON();
return _.isEqual(current, defaults);
}
Alternatively you can hook into the request, sync and error events to keep track of the model syncing state:
initialize: function() {
var self = this;
//pending when a request is started
this.on('request', function() {
self.isPending = true;
self.isLoaded = false;
});
//loaded when a request finishes
this.on('sync', function() {
self.isPending = false;
self.isLoaded = true;
});
//neither pending nor loaded when a request errors
this.on('error', function() {
self.isPending = false;
self.isLoaded = false;
});
}

Nested backbone model results in infinite recursion when saving

This problem just seemed to appear while I updated to Backbone 1.1. I have a nested Backbone model:
var ProblemSet = Backbone.Model.extend({
defaults: {
name: "",
open_date: "",
due_date: ""},
parse: function (response) {
response.name = response.set_id;
response.problems = new ProblemList(response.problems);
return response;
}
});
var ProblemList = Backbone.Collection.extend({
model: Problem
});
I initially load in a ProblemSetList, which is a collection of ProblemSet models in my page. Any changes to the open_date or due_date fields of any ProblemSet, first go to the server and update that property, then returns. This fires another change event on the ProblemSet.
It appears that all subsequent returns from the server fires another change event and the changed attribute is the "problems" attribute. This results in infinite recursive calls.
The problem appears to come from the part of set method of Backbone.Model (code listed here from line 339)
// For each `set` attribute, update or delete the current value.
for (attr in attrs) {
val = attrs[attr];
if (!_.isEqual(current[attr], val)) changes.push(attr);
if (!_.isEqual(prev[attr], val)) {
this.changed[attr] = val;
} else {
delete this.changed[attr];
}
unset ? delete current[attr] : current[attr] = val;
}
// Trigger all relevant attribute changes.
if (!silent) {
if (changes.length) this._pending = true;
for (var i = 0, l = changes.length; i < l; i++) {
this.trigger('change:' + changes[i], this, current[changes[i]], options);
}
}
The comparison on the problems attribute returns false from _.isEqual() and therefore fires a change event.
My question is: is this the right way to do a nested Backbone model? I had something similar working in Backbone 1.1. Other thoughts about how to proceed to avoid this issue?
You reinstantiate your problems attribute each time your model.fetch completes, the objects are different and thus trigger a new cycle.
What I usually do to handle nested models:
use a model property outside of the attributes handled by Backbone,
instantiate it in the initialize function,
set or reset this object in the parent parse function and return a response omitting the set data
Something like this:
var ProblemSet = Backbone.Model.extend({
defaults: {
name: "",
open_date: "",
due_date: ""
},
initialize: function (opts) {
var pbs = (opts && opts.problems) ? opts.problems : [];
this.problems = new ProblemList(pbs);
},
parse: function (response) {
response.name = response.set_id;
if (response.problems)
this.problems.set(response.problems);
return _.omit(response, 'problems');
}
});
parse gets called on fetch and save (according to backbone documentation), this might cause your infinite loop. I don't think that the parse function is the right place to create the new ProblemsList sub-collection, do it in the initialize function of your model instead.

Backbone validate function not getting called

My validate function is not getting called in the code below:-
var Vehicle = Backbone.Model.extend({
color: 'green',
validate: function (attrs) {
var validColors = ['white', 'red', 'blue', 'yellow'];
var colorIsValid = function (attrs) {
if (!attrs.color) return true;
return _.contains(validColors, attrs.color);
}
if(!colorIsValid(attrs)) {
return "color must be one of: " +validColors.join(",");
}
}
});
var car = new Vehicle();
car.on('error', function (model, error) {
console.log(error);
});
console.log(car.get('color'));
car.set('color', 'muave');
Please see fiddle
http://jsfiddle.net/vineet85/Fa8jr/5/
Can someone tell me why the validate function is not getting called?
In Backbone.js validate is called automatically on save but not on set.
If you want validations to run when setting a value you will need to use the validate option. e.g.
car.set('color', 'muave', {validate: true});
See http://backbonejs.org/#Model-validate
The error event is triggered when an error occurs, typically on the server, when trying to save the object. See http://backbonejs.org/#Events-catalog
If you want to catch validation failures try handling the invalid event:
car.on('invalid', function (model, error) {
console.log(error);
});
Seems need to write some extra code for to run the validate.
like below:
car.set('color', 'muave', {validate:true});

loosing model instance after saving in backbone

I'm using backbone with tastypie adapter and jquery for interacting with DOM. I found something in backbone that I can't understand. I tried to simplify the code for this question.
Here is my app.js:
$(document).ready(function() {
Point = Backbone.Model.extend({
defaults:{
lat:0,
lng:0
},
urlRoot: '/api/v1/point' ,
initialize: function(attributes){
var that = this;
$('#b1').bind('click',function() {
that.set('lat',that.get('lat')+1);
});
$('#b2').bind('click',function() {
that.save();
});
$('#b3').bind('click',function() {
console.log(that.get('lat'));
});
}
});
point = new Point ();
//****
$('#b1').click();
$('#b2').click();
$('#b3').click(); // >> 1
$('#b1').click();
$('#b2').click();
$('#b3').click(); // >> 2
//****
});
And my html:
<script src="/static/js/jquery.js"></script>
<script src="/static/js/backbone.js"></script>
<script src="/static/js/backbone-tastypie.js"></script>
<script src="/static/js/app.js"></script>
<button id="b1">b1</button>
<button id="b2">b2</button>
<button id="b3">b3</button>
As you see, I'm calling clicking on buttons in the code and the console is logging '1' and then '2' as expected.
The problem occurs when I am removing code between stars (//*) and pressing these buttons 'b1', 'b2', 'b3', 'b1', 'b2', 'b3' manually in browser and got '1' and '1' in console.
I wait for response from server for 1 second and check for it in console but the behavior of app is still the same: '1', '2' with a code with stars and '1', '1', with the button pressing.
Do you know why?
I wait for response from server for 1 second and check for it in console but [...]
Changing the model manually (click the button using your mouse) instead of programmatically (click()) is the crucial part here.
I assume you do not return the final model from your server, as Backbone update's your model with that data.
See section 53 and
if (options.wait) {
if (attrs && !this._validate(attrs, options)) return false;
current = _.clone(this.attributes);
}
section 55
options.success = function(resp, status, xhr) {
done = true;
var serverAttrs = model.parse(resp);
if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs);
if (!model.set(serverAttrs, options)) return false;
if (success) success(model, resp, options);
};
of the Backbone.js documentation. Especially these lines:
var serverAttrs = model.parse(resp); (Parse model data from response)
if (!model.set(serverAttrs, options)) return false; (Update your model's attributes)
When your first code (the one where you click() the buttons) runs, it increases your models attribute and loggs it before the server returns. Thats
Note: using the wait option or a setTimeout() in your 2nd press of button three in your first code would equal the behaviour.
But what you want, is to return a valid model from your server (after persisting it).
Off topic tip: you should not bind your model and your view in the models initializer or constructor as it would couple them too tight and they wouldn't be interchangable. (For this question, it is certainly acceptable. ;)

Resources