can-validate-legacy
Version:
Validation plugin for CanJS that provides an abstraction layer to your validation library of choice (Shim may be required).
304 lines (264 loc) • 8.75 kB
JavaScript
"use strict";
var assign = require("can-assign");
var Map = require("can-map");
var canReflect = require("can-reflect");
var validate = require("can-validate-legacy");
var compute = require("can-compute");
var deepAssign = function() {
var objects = [].slice.call(arguments);
var out = {};
for (var i=0; i<objects.length; i++) {
assign(out, objects[i]);
}
return out;
};
var isEmptyObject = function(value) {
return canReflect.size(value) === 0;
};
var proto = Map.prototype;
var oldSet = proto.__set;
var ErrorsObj;
var defaultValidationOpts;
var config = {
errorKeyName: "errors",
validateOptionCacheKey: "validateOptions"
};
var resolveComputes = function (itemObj, opts) {
var processedObj = {};
// Loop through each validation option
canReflect.eachKey(opts, function (item, key) {
var actualOpts = item;
if (typeof item === "function") {
// create compute and add it to computes array
actualOpts = item(itemObj.value);
}
// build the map for the final validations object
processedObj[key] = actualOpts;
});
return processedObj;
};
// Gets properties from the map"s define property.
var getPropDefineBehavior = function (behavior, attr, define) {
var prop;
var defaultProp;
if (define) {
prop = define[attr];
defaultProp = define["*"];
if (prop && prop[behavior] !== undefined) {
return prop[behavior];
} else {
if (defaultProp && defaultProp[behavior] !== undefined) {
return defaultProp[behavior];
}
}
}
};
// Default Map for errors object. Useful to add instance helpers
ErrorsObj = Map.extend({}, {
hasErrors: function () {
return !isEmptyObject(this.attr());
}
});
// Default validation options to extend passed options from
defaultValidationOpts = {
mustValidate: false,
validateOnInit: false
};
var getValidateFromCache = function () {
var validateCacheKey = "__" + config.validateOptionCacheKey;
// Create cache object in map instance, if it doesn't exist
if (!this[validateCacheKey]) {
this[validateCacheKey] = {};
}
return this[validateCacheKey];
};
var initProperty = function (key, value) {
var validateOpts;
var mapValidateCache;
var propIniting;
// Creat shortcut to cache
mapValidateCache = getValidateFromCache.call(this);
// If validate options don't exist in cache for current prop, create them
if (mapValidateCache[key] && !isEmptyObject(mapValidateCache[key])) {
validateOpts = mapValidateCache[key];
propIniting = false;
} else {
// Copy current prop"s validation properties to cache
validateOpts = assign({}, getPropDefineBehavior("validate", key, this.define));
// Need to build computes in the next step
propIniting = true;
}
// Do validate if prop has any validate options
if (typeof validateOpts !== "undefined") {
//create validation computes only when initing the map
if (propIniting) {
validateOpts = deepAssign(
defaultValidationOpts,
validateOpts,
// Find any functions, converts them to computes and returns
// nice object for shim to use
this._processValidateOpts({key: key, value: value}, validateOpts)
);
mapValidateCache[key] = validateOpts;
}
return true;
}
return false;
};
// add method to prototype that validates entire map
var oldSetup = proto.setup;
var oldInit = proto.init;
proto.setup = function () {
this._initValidate = true;
oldSetup.apply(this, arguments);
};
proto.init = function () {
this._initValidation();
this._initValidate = false;
if (oldInit) {
oldInit.apply(this, arguments);
}
};
assign(Map.prototype, {
_initValidation: function () {
var self = this;
var validateCache = getValidateFromCache.call(this);
canReflect.eachKey(this.define, function (props, key) {
if (props.validate && !validateCache[key]) {
initProperty.call(self, key, self[key]);
}
});
},
/**
* @function validate Validate
* @deprecated {1.0} `validate` is deprecated and will be removed in version 1.0.
* Use `_validate` instead.
*
* @description Runs validation on the entire map instance. Actual behavior of
* "validate all" is defined by the registered shim.
*/
validate: function () {
return this._validate();
},
/**
* @function _validate _Validate
* @hide
* @description Runs validation on the entire map instance. Actual behavior of
* "validate all" is defined by the registered shim (`validate`).
*/
_validate: function () {
var validateOpts = getValidateFromCache.call(this);
var processedOpts = {};
var self = this;
// Loop through validate options
canReflect.eachKey(this.define, function (value, key) {
if (value.validate) {
processedOpts[key] = resolveComputes({key: key, value: self.attr(key)}, validateOpts[key]);
}
});
var errors = validate.validate(this.serialize(), processedOpts);
// Process errors if we got them
// TODO: This creates a new instance every time.
this.attr("errors", new ErrorsObj(errors));
return isEmptyObject(errors);
},
/**
* @function _validateOne Validate One
* @hide
* @description Main method used by `Map.define` setter when a property changes.
* Runs validation on a property. Actual behavior of "validate one" is defined
* by the registered shim (`once`).
*
* It also handles setting the errors property on the map instance and then
* manages the errors for the current property within the errors object.
*
* @param {object} item A key/value object
* @param {object} opts Object that contains validation config.
* @return {boolean} True if method found that the property can be saved; if
* validation fails and the property must validate (`mustValidate` property),
* this will be `false`.
*/
_validateOne: function (item, opts) {
var errors;
var allowSet = true;
// run validation
errors = validate.once(item.value, assign({}, opts), item.key);
// Process errors if we got them
if (errors && errors.length > 0) {
// Create errors property if doesn't exist
if (!this.attr("errors")) {
this.attr("errors", new ErrorsObj({}));
}
// Apply error response to observable
this.attr("errors").attr(item.key, errors);
// Don't set value if `mustValidate` is true
if (opts.mustValidate === true) {
allowSet = false;
}
} else {
// clear errors for this property if they exist
if (this.attr("errors") && this.attr("errors").attr(item.key)) {
this.attr("errors").removeAttr(item.key);
}
}
return allowSet;
},
/**
* @function _processValidateOpts Process Validate Opts
* @hide
* @description Allows the ability to pass computes in validation properties,
* this allows for things like making a property required based on the value on
* another property.
*
* Processes validation options, creates computes from functions and adds
* listeners to computes.
* @param {object} itemObj Property to validate
* @param {object} opts Map of validation options
*/
_processValidateOpts: function (itemObj, opts) {
var processedObj = {};
var computes = [];
var self = this;
// Loop through each validation option
canReflect.eachKey(opts, function (item, key) {
processedObj[key] = item;
if (typeof item === "function") {
// create compute and add it to computes array
var newCompute = compute(item.bind(self));
computes.push({key: key, compute: newCompute});
processedObj[key] = newCompute;
}
});
// Using the computes array, create necessary listeners
// We do this afterwards instead of inline so we can have access
// to the final set of validation options.
canReflect.each(computes, function (item) {
item.compute.bind("change", function () {
itemObj.value = self.attr(itemObj.key);
self._validateOne(itemObj, processedObj);
});
});
return processedObj;
}
});
// Override the prototype"s __set with a more validate-y one.
proto.__set = function (prop, value, current, success, error) {
// allowSet is changed only if validation options exist and validation returns errors
var allowSet = true;
var checkValidate = initProperty.call(this, prop, value);
var validateOpts = getValidateFromCache.call(this)[prop];
var mapIniting = this._initValidate;
if (checkValidate !== false) {
validateOpts = resolveComputes({key: prop, value: value}, validateOpts);
// If validate opts are set and not initing, validate properties
// If validate opts are set and initing, validate properties only if validateOnInit is true
if ((validateOpts && !mapIniting) || (validateOpts && mapIniting && validateOpts.validateOnInit)) {
// Validate item
allowSet = this._validateOne({key: prop, value: value}, validateOpts);
}
}
// Call old __set, in most cases, this will be the define plugin"s set.
if (allowSet) {
oldSet.call(this, prop, value, current, success, error);
}
};