Browse files

Overhauling 'validate' -- Receives the computed new state of the attr…

…s, not just the delta. Now runs on model create, raising an exception if you try to 'new' and invalid model ... also runs even if changed silently.
  • Loading branch information...
1 parent 71641fb commit ab164c450da4d5c883d5b2b2817fa6a416a4b55a @jashkenas jashkenas committed Jan 23, 2012
View
9 backbone.js
@@ -161,7 +161,9 @@
this.attributes = {};
this._escapedAttributes = {};
this.cid = _.uniqueId('c');
- this.set(attributes, {silent: true});
+ if (!this.set(attributes, {silent: true})) {
+ throw new Error("Can't create an invalid model");
+ }
this._changed = false;
this._previousAttributes = _.clone(this.attributes);
this.initialize.apply(this, arguments);
@@ -225,7 +227,7 @@
var now = this.attributes, escaped = this._escapedAttributes;
// Run validation.
- if (!options.silent && this.validate && !this._performValidation(attrs, options)) return false;
+ if (this.validate && !this._performValidation(attrs, options)) return false;
// Check for changes of `id`.
if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];
@@ -422,7 +424,8 @@
// if all is well. If a specific `error` callback has been passed,
// call that instead of firing the general `"error"` event.
_performValidation: function(attrs, options) {
- var error = this.validate(attrs, options);
+ var newAttrs = _.extend({}, this.attributes, attrs);
+ var error = this.validate(newAttrs, options);
if (error) {
if (options.error) {
options.error(this, error, options);
View
2 examples/todos/index.html
@@ -6,7 +6,7 @@
<link href="todos.css" media="all" rel="stylesheet" type="text/css"/>
<script src="../../test/vendor/json2.js"></script>
<script src="../../test/vendor/jquery-1.7.1.js"></script>
- <script src="../../test/vendor/underscore-1.2.4.js"></script>
+ <script src="../../test/vendor/underscore-1.3.1.js"></script>
<script src="../../backbone.js"></script>
<script src="../backbone-localstorage.js"></script>
<script src="todos.js"></script>
View
2 index.html
@@ -3061,7 +3061,7 @@ <h2 id="changelog">Change Log</h2>
</div>
- <script src="test/vendor/underscore-1.2.4.js"></script>
+ <script src="test/vendor/underscore-1.3.1.js"></script>
<script src="test/vendor/jquery-1.7.1.js"></script>
<script src="test/vendor/json2.js"></script>
<script src="backbone.js"></script>
View
15 test/collection.js
@@ -73,8 +73,9 @@ $(document).ready(function() {
var CustomSetCollection = Backbone.Collection.extend({
model: CustomSetModel
});
- var col = new CustomSetCollection([{ num_as_string: 2 }]);
- equal(col.length, 1);
+ raises(function(){
+ new CustomSetCollection([{ num_as_string: 2 }]);
+ });
});
test("Collection: update index when id changes", function() {
@@ -363,7 +364,9 @@ $(document).ready(function() {
model: ValidatingModel
});
var col = new ValidatingCollection();
- equal(col.create({"foo":"bar"}),false);
+ raises(function(){
+ equal(col.create({"foo":"bar"}),false);
+ });
});
test("Collection: a failing create runs the error callback", function() {
@@ -378,8 +381,9 @@ $(document).ready(function() {
var flag = false;
var callback = function(model, error) { flag = true; };
var col = new ValidatingCollection();
- col.create({"foo":"bar"}, { error: callback });
- equal(flag, true);
+ raises(function(){
+ col.create({"foo":"bar"}, { error: callback });
+ });
});
test("collection: initialize", function() {
@@ -456,6 +460,7 @@ $(document).ready(function() {
set: function(attrs) {
equal(attrs.prop, 'value');
equal(this.collection, col);
+ return this;
}
});
col.model = Model;
View
19 test/model.js
@@ -177,10 +177,10 @@ $(document).ready(function() {
ok(changeCount == 1, "Change count should NOT have incremented.");
a.validate = function(attrs) {
- equal(attrs.foo, void 0, 'ignore values when unsetting');
+ equal(attrs.foo, void 0, "don't ignore values when unsetting");
};
a.unset('foo');
- ok(a.get('foo') == null, "Foo should have changed");
+ equal(a.get('foo'), void 0, "Foo should have changed");
delete a.validate;
ok(changeCount == 2, "Change count should have incremented for unset.");
@@ -364,7 +364,7 @@ $(document).ready(function() {
var lastError;
var model = new Backbone.Model();
model.validate = function(attrs) {
- if (attrs.admin) return "Can't change admin status.";
+ if (attrs.admin != this.get('admin')) return "Can't change admin status.";
};
model.on('error', function(model, error) {
lastError = error;
@@ -374,23 +374,20 @@ $(document).ready(function() {
equal(model.get('a'), 100);
equal(lastError, undefined);
result = model.set({admin: true}, {silent: true});
- equal(lastError, undefined);
- equal(model.get('admin'), true);
+ equal(lastError, "Can't change admin status.");
+ equal(model.get('admin'), void 0);
result = model.set({a: 200, admin: true});
equal(result, false);
equal(model.get('a'), 100);
- equal(lastError, "Can't change admin status.");
});
test("Model: validate on unset and clear", function() {
var error;
var model = new Backbone.Model({name: "One"});
model.validate = function(attrs) {
- if ("name" in attrs) {
- if (!attrs.name) {
- error = true;
- return "No thanks.";
- }
+ if (!attrs.name) {
+ error = true;
+ return "No thanks.";
}
};
model.set({name: "Two"});
View
2 test/test-ender.html
@@ -7,7 +7,7 @@
<script type="text/javascript" src="vendor/ender-jeesh.js"></script>
<script type="text/javascript" src="vendor/qunit.js"></script>
<script type="text/javascript" src="vendor/jslitmus.js"></script>
- <script type="text/javascript" src="vendor/underscore-1.2.4.js"></script>
+ <script type="text/javascript" src="vendor/underscore-1.3.1.js"></script>
<script type="text/javascript" src="../backbone.js"></script>
<script type="text/javascript" src="events.js"></script>
View
2 test/test-zepto.html
@@ -7,7 +7,7 @@
<script type="text/javascript" src="vendor/zepto-0.6.js"></script>
<script type="text/javascript" src="vendor/qunit.js"></script>
<script type="text/javascript" src="vendor/jslitmus.js"></script>
- <script type="text/javascript" src="vendor/underscore-1.2.4.js"></script>
+ <script type="text/javascript" src="vendor/underscore-1.3.1.js"></script>
<script type="text/javascript" src="../backbone.js"></script>
<script type="text/javascript" src="events.js"></script>
View
2 test/test.html
@@ -10,7 +10,7 @@
QUnit.config.reorder = false;
</script>
<script type="text/javascript" src="vendor/jslitmus.js"></script>
- <script type="text/javascript" src="vendor/underscore-1.2.4.js"></script>
+ <script type="text/javascript" src="vendor/underscore-1.3.1.js"></script>
<script type="text/javascript" src="../backbone.js"></script>
<script type="text/javascript" src="noconflict.js"></script>
View
56 test/vendor/underscore-1.2.4.js → test/vendor/underscore-1.3.1.js
@@ -1,4 +1,4 @@
-// Underscore.js 1.2.4
+// Underscore.js 1.3.1
// (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc.
// Underscore is freely distributable under the MIT license.
// Portions of Underscore are inspired or borrowed from Prototype,
@@ -48,26 +48,21 @@
// Create a safe reference to the Underscore object for use below.
var _ = function(obj) { return new wrapper(obj); };
- // Export the Underscore object for **Node.js** and **"CommonJS"**, with
- // backwards-compatibility for the old `require()` API. If we're not in
- // CommonJS, add `_` to the global object.
+ // Export the Underscore object for **Node.js**, with
+ // backwards-compatibility for the old `require()` API. If we're in
+ // the browser, add `_` as a global object via a string identifier,
+ // for Closure Compiler "advanced" mode.
if (typeof exports !== 'undefined') {
if (typeof module !== 'undefined' && module.exports) {
exports = module.exports = _;
}
exports._ = _;
- } else if (typeof define === 'function' && define.amd) {
- // Register as a named module with AMD.
- define('underscore', function() {
- return _;
- });
} else {
- // Exported as a string, for Closure Compiler "advanced" mode.
root['_'] = _;
}
// Current version.
- _.VERSION = '1.2.4';
+ _.VERSION = '1.3.1';
// Collection Functions
// --------------------
@@ -85,7 +80,7 @@
}
} else {
for (var key in obj) {
- if (hasOwnProperty.call(obj, key)) {
+ if (_.has(obj, key)) {
if (iterator.call(context, obj[key], key, obj) === breaker) return;
}
}
@@ -94,7 +89,7 @@
// Return the results of applying the iterator to each element.
// Delegates to **ECMAScript 5**'s native `map` if available.
- _.map = function(obj, iterator, context) {
+ _.map = _.collect = function(obj, iterator, context) {
var results = [];
if (obj == null) return results;
if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context);
@@ -511,7 +506,7 @@
hasher || (hasher = _.identity);
return function() {
var key = hasher.apply(this, arguments);
- return hasOwnProperty.call(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments));
+ return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments));
};
};
@@ -617,7 +612,7 @@
_.keys = nativeKeys || function(obj) {
if (obj !== Object(obj)) throw new TypeError('Invalid object');
var keys = [];
- for (var key in obj) if (hasOwnProperty.call(obj, key)) keys[keys.length] = key;
+ for (var key in obj) if (_.has(obj, key)) keys[keys.length] = key;
return keys;
};
@@ -640,7 +635,7 @@
_.extend = function(obj) {
each(slice.call(arguments, 1), function(source) {
for (var prop in source) {
- if (source[prop] !== void 0) obj[prop] = source[prop];
+ obj[prop] = source[prop];
}
});
return obj;
@@ -738,17 +733,17 @@
if ('constructor' in a != 'constructor' in b || a.constructor != b.constructor) return false;
// Deep compare objects.
for (var key in a) {
- if (hasOwnProperty.call(a, key)) {
+ if (_.has(a, key)) {
// Count the expected number of properties.
size++;
// Deep compare each member.
- if (!(result = hasOwnProperty.call(b, key) && eq(a[key], b[key], stack))) break;
+ if (!(result = _.has(b, key) && eq(a[key], b[key], stack))) break;
}
}
// Ensure that both objects contain the same number of properties.
if (result) {
for (key in b) {
- if (hasOwnProperty.call(b, key) && !(size--)) break;
+ if (_.has(b, key) && !(size--)) break;
}
result = !size;
}
@@ -767,7 +762,7 @@
// An "empty" object has no enumerable own-properties.
_.isEmpty = function(obj) {
if (_.isArray(obj) || _.isString(obj)) return obj.length === 0;
- for (var key in obj) if (hasOwnProperty.call(obj, key)) return false;
+ for (var key in obj) if (_.has(obj, key)) return false;
return true;
};
@@ -793,7 +788,7 @@
};
if (!_.isArguments(arguments)) {
_.isArguments = function(obj) {
- return !!(obj && hasOwnProperty.call(obj, 'callee'));
+ return !!(obj && _.has(obj, 'callee'));
};
}
@@ -843,6 +838,11 @@
return obj === void 0;
};
+ // Has own property?
+ _.has = function(obj, key) {
+ return hasOwnProperty.call(obj, key);
+ };
+
// Utility Functions
// -----------------
@@ -897,6 +897,12 @@
// guaranteed not to match.
var noMatch = /.^/;
+ // Within an interpolation, evaluation, or escaping, remove HTML escaping
+ // that had been previously added.
+ var unescape = function(code) {
+ return code.replace(/\\\\/g, '\\').replace(/\\'/g, "'");
+ };
+
// JavaScript micro-templating, similar to John Resig's implementation.
// Underscore templating handles arbitrary delimiters, preserves whitespace,
// and correctly escapes quotes within interpolated code.
@@ -907,15 +913,13 @@
str.replace(/\\/g, '\\\\')
.replace(/'/g, "\\'")
.replace(c.escape || noMatch, function(match, code) {
- return "',_.escape(" + code.replace(/\\'/g, "'") + "),'";
+ return "',_.escape(" + unescape(code) + "),'";
})
.replace(c.interpolate || noMatch, function(match, code) {
- return "'," + code.replace(/\\'/g, "'") + ",'";
+ return "'," + unescape(code) + ",'";
})
.replace(c.evaluate || noMatch, function(match, code) {
- return "');" + code.replace(/\\'/g, "'")
- .replace(/[\r\n\t]/g, ' ')
- .replace(/\\\\/g, '\\') + ";__p.push('";
+ return "');" + unescape(code).replace(/[\r\n\t]/g, ' ') + ";__p.push('";
})
.replace(/\r/g, '\\r')
.replace(/\n/g, '\\n')

0 comments on commit ab164c4

Please sign in to comment.