Adding backbone.js validations framework

pull/105/merge
Michael Jett 13 years ago
parent 3402a3e463
commit c45991b561

@ -5,6 +5,7 @@
<script type="text/javascript" src="resources/bootstrap2/js/bootstrap.min.js"></script>
<script type="text/javascript" src="resources/js/underscore-min.js"></script>
<script type="text/javascript" src="resources/js/backbone-min.js"></script>
<script type="text/javascript" src="resources/js/backbone.validations.js"></script>
<script type="text/javascript" src="resources/js/app.js"></script>
</body>

@ -0,0 +1,286 @@
// Copyright (C) 2011 Neal Stewart
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
// of the Software, and to permit persons to whom the Software is furnished to do
// so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
(function(Backbone) {
// Premade Validators
Backbone.Validations = {};
var validators = {
"custom" : function(methodName, attributeName, model, valueToSet) {
return model[methodName](attributeName, valueToSet);
},
"required" : function(attributeName, model, valueToSet) {
var currentValue = model.get(attributeName);
var isNotAlreadySet = _.isUndefined(currentValue);
var isNotBeingSet = _.isUndefined(valueToSet);
if (_.isNull(valueToSet) || valueToSet === "" || (isNotBeingSet && isNotAlreadySet)) {
return "required";
} else {
return false;
}
},
"in" : function(whitelist, attributeName, model, valueToSet) {
return _.include(whitelist, valueToSet) ? undefined : "in";
},
"email" : function(type, attributeName, model, valueToSet) {
var emailRegex = new RegExp("[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?");
if (_.isString(valueToSet) && !valueToSet.match(emailRegex)) {
return "email";
}
},
"url" : function(type, attributeName, model, valueToSet) {
// taken from jQuery UI validation
var urlRegex = /^(https?|ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i;
if (_.isString(valueToSet) && !valueToSet.match(urlRegex)) {
return "url";
}
},
"number" : function(type, attributeName, model, valueToSet) {
return isNaN(valueToSet) ? 'number' : undefined;
},
"pattern" : function(pattern, attributeName, model, valueToSet) {
if (_.isString(valueToSet)) {
if (pattern.test(valueToSet)) {
return false;
} else {
return "pattern";
}
}
},
"min" : function(minimumValue, attributeName, model, valueToSet) {
if (valueToSet < minimumValue) {
return "min";
}
},
"max" : function(maximumValue, attributeName, model, valueToSet) {
if (valueToSet > maximumValue) {
return "max";
}
},
"minlength" : function(minlength, attributeName, model, valueToSet) {
if (_.isString(valueToSet)) {
if (valueToSet.length < minlength) { return "minlength"; }
}
},
"maxlength" : function(maxlength, attributeName, model, valueToSet) {
if (_.isString(valueToSet)) {
if (valueToSet.length > maxlength) { return "maxlength"; }
}
}
};
var customValidators = {};
var getCustomValidator = function(name) {
var cv = customValidators[name];
if (!cv) { throw "custom validator '"+name+"' could not be found."; }
return cv;
};
Backbone.Validations.addValidator = function(name, validator) {
if (validators.hasOwnProperty(name) || customValidators.hasOwnProperty(name)) {
throw "existing validator";
}
customValidators[name] = validator;
};
/*
The newValidate method overrides validate in Backbone.Model.
It has the same interface as the validate function which you
would provide.
The returned object looks like this:
{
attributeName : ["required", "of", "errors"],
otherAttributeName: ["and", "so", "on"]
}
*/
function newValidate(attributes) {
var errorsForAttribute,
errorHasOccured,
errors = {};
for (var attrName in this._attributeValidators) {
var valueToSet = attributes[attrName];
var validateAttribute = this._attributeValidators[attrName];
if (validateAttribute) {
errorsForAttribute = validateAttribute(this, valueToSet);
}
if (errorsForAttribute) {
errorHasOccured = true;
errors[attrName] = errorsForAttribute;
}
}
return errorHasOccured ? errors : false;
}
function createMinValidator(attributeName, minimumValue) {
return _.bind(validators.min, null, minimumValue);
}
function createMaxValidator(attributeName, maximumValue) {
return _.bind(validators.max, null, maximumValue);
}
/* createValidator takes in:
- the model
- the name of the attribute
- the type of validation
- the description of the validation
returns a function that takes in:
- the value being set for the attribute
and either returns nothing (undefined),
or the error name (string).
*/
function createValidator(attributeName, type, description) {
var validator,
validatorMethod,
customValidator;
if (type === "type") {
type = description;
}
validator = validators[type];
if (!validator) { validator = getCustomValidator(type); }
if (!validator) { throw "Improper validation type '"+type+"'" ; }
if (type !== "required") { // doesn't need the description
validator = _.bind(validator, null, description, attributeName);
} else {
validator = _.bind(validator, null, attributeName);
}
return validator;
}
function createAttributeValidator(attributeName, attributeDescription) {
var validatorsForAttribute = [],
type,
desc;
for (type in attributeDescription) {
desc = attributeDescription[type];
validatorsForAttribute.push(createValidator(attributeName, type, desc));
}
return function(model, valueToSet, hasOverridenError, options) {
var validator,
result,
errors = [];
for (var i = 0, length = validatorsForAttribute.length; i < length; i++) {
validator = validatorsForAttribute[i];
result = validator(model, valueToSet);
if (result) {
if (_.isArray(result)) {
errors = errors.concat(result);
} else {
errors.push(result);
}
}
}
if (errors.length) {
return errors;
} else {
return false;
}
};
}
function createValidators(modelValidations) {
var attributeValidations,
attributeValidators = {};
for (var attrName in modelValidations) {
attributeValidations = modelValidations[attrName];
attributeValidators[attrName] = createAttributeValidator(attrName, attributeValidations);
}
return attributeValidators;
}
var oldPerformValidation = Backbone.Model.prototype._performValidation;
function newPerformValidation(attrs, options) {
if (options.silent || !this.validate) return true;
var errors = this.validate(attrs);
if (errors) {
if (options.error) {
options.error(this, errors, options);
} else {
this.trigger('error', this, errors, options);
_.each(errors, function(error, name) {
this.trigger('error:' + name, this, errors, options);
}, this);
}
return false;
}
return true;
}
// save the old backbone
var oldModel = Backbone.Model;
// Constructor for our new Validations Model
Backbone.Validations.Model = Backbone.Model.extend({
constructor : function() {
// if they pass an object, construct the new validations
if (typeof this.validate === "object" && this.validate !== null) {
if (!this.constructor.prototype._attributeValidators) {
this.constructor.prototype._attributeValidators = createValidators(this.validate);
this.constructor.prototype.validate = newValidate;
this.constructor.prototype._validate = newPerformValidation;
}
}
oldModel.apply(this, arguments);
}
});
// Override Backbone.Model with our new Model
Backbone.Model = Backbone.Validations.Model;
// Requisite noConflict
Backbone.Validations.Model.noConflict = function() {
Backbone.Model = oldModel;
};
}(Backbone));
Loading…
Cancel
Save