/** * Backbone.Validator * * Adds decoupled validator functionality that could be bound to model and view, as well as * validated plain hashes with built-in or custom validators * * @author Maksim Horbachevsky */ (function(factory) { if (typeof define === 'function' && define.amd) { define(['backbone', 'underscore'], factory); } else if (typeof exports === 'object') { module.exports = factory(require('backbone'), require('underscore')); } else { factory(window.Backbone, window._); } })(function(Backbone, _) { 'use strict'; var Validator = Backbone.Validator = { version: '0.3.4', /** * General validation method that gets attributes list and validations config and runs them all * * @param attrs * @param validations * @param {Object} [context] - validator execution context * @return {*} null if validation passed, errors object if not */ validate: function(attrs, validations, context) { var errors = {}; _.each(attrs, function(attrValue, attrName) { var validation = validations[attrName]; if (validation) { var error = this._validateAll(validation, attrName, attrValue, context, attrs); if (error.length) { errors[attrName] = _.uniq(error); } } }, this); return _.size(errors) ? errors : null; }, _validateAll: function(validations, attrName, attrValue, context, allAttrs) { context = context || this; return _.inject(_.flatten([validations || []]), function(errors, validation) { _.chain(validation).omit('message').each(function(attrExpectation, validatorName) { var validator = this._validators[validatorName]; if (!validator) { throw new Error('Missed validator: ' + validatorName); } var result = validator.fn.apply(context, [attrValue, attrExpectation, allAttrs]); if (result !== true) { var error = validation.message || result || Validator.createMessage(attrName, attrValue, attrExpectation, validatorName, context) || validator.message || 'Invalid'; if (_.isFunction(error)) { error = error.apply(context, [attrName, attrValue, attrExpectation, validatorName]); } error = Validator.formatMessage(error, attrName, attrValue, attrExpectation, validatorName, context); errors.push(error); } }, this).value(); return errors; }, [], this); }, /** * Add validator into collection. Will throw error if try to override existing validator * * Backbone.Validator.addValidator('minLength', function(value, expectation) { * return value.length >= expectation; * }, 'Field is too short'); * * @param {String} validatorName - validator name * @param {Function} validatorFn - validation function * @param {String} [errorMessage] - error message */ add: function(validatorName, validatorFn, errorMessage) { this._validators[validatorName] = { fn: validatorFn, message: errorMessage }; }, /** * Validators storage * * @private * @property _validators */ _validators: { }, /** * Fetching attributes to validate * @return {*} */ getAttrsToValidate: function(model, passedAttrs) { var modelAttrs = model.attributes, attrs, all; if (_.isArray(passedAttrs) || _.isString(passedAttrs)) { attrs = pick(modelAttrs, passedAttrs); } else if (!passedAttrs) { all = _.extend({}, modelAttrs, _.result(model, 'validation') || {}); attrs = pick(modelAttrs, _.keys(all)); } else { attrs = passedAttrs; } return attrs; }, /** * Override this hook to generate error message, e.g. using I18n * @returns {boolean} */ createMessage: function(/* attrName, attrValue, attrExpectation, validatorName, context */) { return false; }, /** * Override this hook to format all error messages, e.g. running them through _.template and * pass variables inside */ formatMessage: function(message /* attrName, attrValue, attrExpectation, validatorName, context */) { return message; } }; /** * Collection of methods that will be used to extend standard * view and model functionality with validations */ Validator.Extensions = { View: { /** * Bind passed (or internal) model to the view with `validated` event, that fires when model is * being validated. Calls `onValidField` and `onInvalidField` callbacks depending on validity of * particular attribute * * @param {Backbone.Model} [model] - model that will be bound to the view * @param {Object} options - optional callbacks `onValidField` and `onInvalidField`. If not passed * will be retrieved from the view instance or global `Backbone.Validator.ViewCallbacks` object */ bindValidation: function(model, options) { model = model || this.model; if (!model) { throw 'Model is not provided'; } this.listenTo(model, 'validated', function(model, attributes, errors) { var callbacks = _.extend({}, Validator.ViewCallbacks, _.pick(this, 'onInvalidField', 'onValidField'), options); errors = errors || {}; _.each(attributes, function(value, name) { var attrErrors = errors[name]; if (attrErrors && attrErrors.length) { callbacks.onInvalidField.call(this, name, value, attrErrors, model); } else { callbacks.onValidField.call(this, name, value, model); } }, this); }); } }, Model: { /** * Validation method called by Backbone's internal `#_validate()` or directly from model's instance * * @param {Object|Array} [attributes] - optional hash/array of attributes to validate * @param {Object} [options] - standard Backbone.Model's options list, including `suppress` option. When it's * set to true method will store errors into `#errors` property, but return null, so model seemed to be valid * * @return {null|Object} - null if model is valid, otherwise - collection of errors associated with attributes */ validate: function(attributes, options) { var validation = _.result(this, 'validation') || {}, attrs = Validator.getAttrsToValidate(this, attributes), errors = Validator.validate(attrs, validation, this); options = options || {}; errors = options.processErrors ? options.processErrors(errors) : Validator.ModelCallbacks.processErrors(errors); if (!options.silent) { _.defer(_.bind(this.triggerValidated, this), attrs, errors); } return options && options.suppress ? null : errors; }, /** * Override Backbone's method to pass properly fetched attributes list * @private */ _validate: function(attributes, options) { if (!options.validate || !this.validate) return true; var attrs = Validator.getAttrsToValidate(this, attributes), errors = this.validationError = this.validate(attrs, options) || null; if (errors) { this.trigger('invalid', this, errors, _.extend(options || {}, { validationError: errors })); } return !errors; }, /** * Triggering validation results (invalid/valid) with errors list if nay * @param {Object} attributes - validated attributes * @param {Object|null} errors */ triggerValidated: function(attributes, errors) { var attrs = Validator.getAttrsToValidate(this, attributes), errs = cleanErrors(errors); this.validationError = errs; this.trigger('validated', this, attrs, errs); this.trigger('validated:' + (errs ? 'invalid' : 'valid'), this, attrs, errs); }, /** * Checks if model is valid * * @param {Object} [attributes] - optional list of attributes to validate * @param {Object} [options] - standard Backbone.Model's options list * @return {boolean} */ isValid: function(attributes, options) { var attrs = Validator.getAttrsToValidate(this, attributes); return !this.validate || !this.validate(attrs, options); } } }; /** * Alternative to _.pick() - but also picks undefined/null/false values * * @param {Object} object - source hash * @param {Array} keys - needed keys to pick * @return {Object} */ var pick = function(object, keys) { return _.inject(_.flatten([keys]), function(memo, key) { memo[key] = object[key]; return memo; }, {}); }; /** * Cleanup errors object from empty error values * @param allErrors */ function cleanErrors(allErrors) { var errors = _.inject(allErrors, function(memo, fieldErrors, attr) { if (fieldErrors.length) { memo[attr] = _.isString(fieldErrors) ? [fieldErrors] : fieldErrors; } return memo; }, {}); return _.size(errors) ? errors : null; } Validator.ViewCallbacks = { onValidField: function(name /*, value, model*/) { var input = this.$('input[name="' + name + '"]'); input.removeClass('error'); input.next('.error-text').remove(); }, onInvalidField: function(name, value, errors /*, model*/) { var input = this.$('input[name="' + name + '"]'); input.next('.error-text').remove(); input.addClass('error').after('