Set operations on nested model attributes that fail validation are triggering change events in backbone-nested, and I don't think that's how it's intended to work in Backbone. If you try invalid set operations on regular attributes, they do not trigger change events, and I think it should be the same for nested attributes.
Also, it looks like invalid sets on nested attributes are updating the changedAttributes for the model and these are not cleared out when future change events are fired. This is affecting integration with other libraries like Backbone.ModelBinding.
if (!this._validate(attrs, options)) return false;
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta name="generator" content="HTML Tidy for Windows (vers 14 February 2006), see www.w3.org">
<script src="http://code.jquery.com/jquery-latest.js" type="text/javascript"></script>
<link rel="stylesheet" href="http://code.jquery.com/qunit/git/qunit.css" type="text/css" media="screen">
<script type="text/javascript" src="http://code.jquery.com/qunit/git/qunit.js"></script>
<script type="text/javascript" src="http://underscorejs.org/underscore.js"></script>
<script type="text/javascript" src="http://documentcloud.github.com/backbone/backbone.js"></script>
<script type="text/javascript" src="https://raw.github.com/afeld/backbone-nested/master/backbone-nested.js"></script>
<script type="text/javascript">
$(document).ready(function(){
module("Backbone-Nested testing", {
setup: function() {
// Top level parent model with nested stuff
this.ParentBBModel = Backbone.NestedModel.extend({
validate : function(attributes) {
if (attributes.firstLevelChild.firstLevelChildAttrA.length < 25) {
return "firstLevelChild.firstLevelChildAttrA must be at least 25 characters";
} else if (attributes.parentAttrA.length < 16) {
return "parentAttrA must be at least 16 characters";
}
}
});
this.parentBBModel = new this.ParentBBModel({
parentAttrA : "parentAttrAValue",
firstLevelChild : {
firstLevelChildAttrA : "firstLevelChildAttrAValue",
firstLevelChildAttrB : "firstLevelChildAttrBValue",
secondLevelArray : [
{
secondLevelArrObjAttrA : "secondLevelArrObj0AttrAValue",
secondLevelArrObjAttrB : "secondLevelArrObj0AttrBValue"
},
{
secondLevelArrObjAttrA : "secondLevelArrObj1AttrAValue",
secondLevelArrObjAttrB : "secondLevelArrObj1AttrBValue"
},
{
secondLevelArrObjAttrA : "secondLevelArrObj2AttrAValue",
secondLevelArrObjAttrB : "secondLevelArrObj2AttrBValue"
}
]
},
firstLevelArray : [
{
firstLevelArrObjAttrA : "firstLevelArrObj0AttrAValue",
firstLevelArrObjAttrB : "firstLevelArrObj0AttrBValue"
},
{
firstLevelArrObjAttrA : "firstLevelArrObj1AttrAValue",
firstLevelArrObjAttrB : "firstLevelArrObj1AttrBValue"
},
{
firstLevelArrObjAttrA : "firstLevelArrObj2AttrAValue",
firstLevelArrObjAttrB : "firstLevelArrObj2AttrBValue"
}
]
});
},
teardown: function() {
}
});
test("change should not be triggered for invalid updates to regular attributes", function() {
var changeCallbackCalled = false;
this.parentBBModel.bind('change:parentAttrA', function(model, errors) {
changeCallbackCalled = true;
});
// Change a nested attribute to invalid value
this.parentBBModel.set({"parentAttrA" : "tooShort"});
// Verify there was no change made
equal( this.parentBBModel.get("parentAttrA"), "parentAttrAValue");
// The change event should not be triggered because the attribute was never changed (it failed validation)
ok(!(changeCallbackCalled));
});
test("change should not be triggered for invalid updates to nested attributes", function() {
var changeCallbackCalled = false;
this.parentBBModel.bind('change:firstLevelChild.firstLevelChildAttrA', function(model, errors) {
changeCallbackCalled = true;
});
// Change a nested attribute to invalid value
this.parentBBModel.set({"firstLevelChild.firstLevelChildAttrA" : "tooShort"});
// Verify there was no change made
equal( this.parentBBModel.get("firstLevelChild.firstLevelChildAttrA"), "firstLevelChildAttrAValue");
// The change event should not be triggered because the attribute was never changed (it failed validation)
ok(!(changeCallbackCalled));
});
test("changedAttributes should not include changes from previous change events, especially those that failed validation", function() {
var changeCallbackCalled = false;
this.parentBBModel.bind('change:parentAttrA', function(model, errors) {
changeCallbackCalled = true;
// Changed attributes should only include parentAttr because firstLevelChild.firstLevelChildAttrA
// failed validation and in any case was part of a previous event
equal( model.changedAttributes(), {"parentAttrA" : "parentAttrAValueChanged"} );
});
// Change a nested attribute to invalid value
this.parentBBModel.set({"firstLevelChild.firstLevelChildAttrA" : "tooShort"});
// Verify there was no change made
equal( this.parentBBModel.get("firstLevelChild.firstLevelChildAttrA"), "firstLevelChildAttrAValue");
// Change another attribute to a valid value
this.parentBBModel.set({"parentAttrA" : "parentAttrAValueChanged"});
ok(changeCallbackCalled);
});
});
</script>
<title></title>
</head>
<body>
<h1 id="qunit-header">
Backbone-Nested Test
</h1>
<h2 id="qunit-banner"></h2>
<div id="qunit-testrunner-toolbar"></div>
<h2 id="qunit-userAgent"></h2>
<ol id="qunit-tests"></ol>
<!-- THE FIXTURE -->
<div id="qunit-fixture"></div>
</body>
</html>