Skip to content

Commit

Permalink
Overhauling 'validate' -- Receives the computed new state of the attr…
Browse files Browse the repository at this point in the history
…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
jashkenas committed Jan 23, 2012
1 parent 71641fb commit ab164c4
Show file tree
Hide file tree
Showing 9 changed files with 59 additions and 50 deletions.
9 changes: 6 additions & 3 deletions backbone.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion examples/todos/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
15 changes: 10 additions & 5 deletions test/collection.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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() {
Expand All @@ -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() {
Expand Down Expand Up @@ -456,6 +460,7 @@ $(document).ready(function() {
set: function(attrs) {
equal(attrs.prop, 'value');
equal(this.collection, col);
return this;
}
});
col.model = Model;
Expand Down
19 changes: 8 additions & 11 deletions test/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.");

Expand Down Expand Up @@ -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;
Expand All @@ -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"});
Expand Down
2 changes: 1 addition & 1 deletion test/test-ender.html
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
2 changes: 1 addition & 1 deletion test/test-zepto.html
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
2 changes: 1 addition & 1 deletion test/test.html
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
56 changes: 30 additions & 26 deletions test/vendor/underscore-1.2.4.js → test/vendor/underscore-1.3.1.js
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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
// --------------------
Expand All @@ -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;
}
}
Expand All @@ -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);
Expand Down Expand Up @@ -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));
};
};

Expand Down Expand Up @@ -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;
};

Expand All @@ -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;
Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
};

Expand All @@ -793,7 +788,7 @@
};
if (!_.isArguments(arguments)) {
_.isArguments = function(obj) {
return !!(obj && hasOwnProperty.call(obj, 'callee'));
return !!(obj && _.has(obj, 'callee'));
};
}

Expand Down Expand Up @@ -843,6 +838,11 @@
return obj === void 0;
};

// Has own property?
_.has = function(obj, key) {
return hasOwnProperty.call(obj, key);
};

// Utility Functions
// -----------------

Expand Down Expand Up @@ -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.
Expand All @@ -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')
Expand Down

0 comments on commit ab164c4

Please sign in to comment.