salix/client/core/src/validation.js

339 lines
8.6 KiB
JavaScript
Raw Normal View History

import {module} from './module';
2017-02-01 17:55:02 +00:00
import {loopbackValidations} from 'vendor';
2017-02-01 17:55:02 +00:00
directive.$inject = ['$interpolate', '$compile', '$window']
export function directive(interpolate, compile, $window) {
return {
restrict: 'A',
require: ['ngModel', '^^form'],
link: function (scope, element, attrs, ctrl) {
2017-02-01 17:55:02 +00:00
let vnValidations = $window.validations;
if(!attrs['vnValidation'])
return;
let split = attrs['vnValidation'].split('.');
if(split.length != 2)
throw new Error(`vnValidation: Attribute must have this syntax: [entity].[field]`);
let entityName = firstUpper(split[0]);
let fieldName = split[1];
let entity = vnValidations[entityName];
if(!entity)
throw new Error(`vnValidation: Entity '${entityName}' doesn't exist`);
let validations = entity.validations[fieldName];
if(!validations)
return;
let input = ctrl[0],
form = ctrl[1],
messages = [],
customValidator = {},
parentMessage;
2017-02-01 17:55:02 +00:00
let i = 0;
for(let conf of validations){
let key = `v${i++}`;
let messageNode = createMessage(form.$name, input.$name, key);
customValidator[key] = messageNode;
messages.push(messageNode);
2017-02-01 17:55:02 +00:00
input.$validators[key] = function(value) {
return isValid(value, conf, messageNode);
}
}
2017-02-01 17:55:02 +00:00
if(messages.length > 0) {
parentMessage = angular.element(createMessages(form.$name, input.$name));
messages.forEach(function (item) {
parentMessage.append(item);
});
messages = null;
element.after(compile(parentMessage)(scope));
}
2017-02-01 17:55:02 +00:00
scope.$on('destroy', function() {
customValidator = null;
});
}
}
function firstUpper(str) {
return str.charAt(0).toUpperCase() + str.substr(1);
}
function createMessage(form, input, key) {
var template = '<span class="mdl-textfield__error" ng-show="{{form}}.$dirty && {{form}}.{{input}}.$error.{{key}}"></span>';
var span = interpolate(template)({ form: form, input: input, key: key });
var element = angular.element(span);
return element;
}
function createMessages(form, input) {
var template = '<div ng-show="{{form}}.$dirty && {{form}}.{{input}}.$invalid"></div>';
return interpolate(template)({ form: form, input: input });
}
// Loopback code with modifications
function isValid(value, conf, messageNode) {
let valid = true;
let inst = {value: value};
let validator = validators[conf.validation];
if(!validator)
return true;
validator.call(inst, 'value', conf, err);
function err(kind) {
var message, code = conf.code || conf.validation;
if (conf.message) {
message = conf.message;
}
2017-02-01 17:55:02 +00:00
if (!message && defaultMessages[conf.validation]) {
message = defaultMessages[conf.validation];
}
2017-02-01 17:55:02 +00:00
if (!message) {
message = 'is invalid';
}
2017-02-01 17:55:02 +00:00
if (kind) {
code += '.' + kind;
if (message[kind]) {
message = message[kind];
} else if (defaultMessages.common[kind]) {
message = defaultMessages.common[kind];
} else {
message = 'is invalid';
}
}
2017-02-01 17:55:02 +00:00
messageNode.text(message); // code
valid = false;
}
2017-02-01 17:55:02 +00:00
return valid;
}
}
module.directive('vnValidation', directive);
// Code portion of 'lib/validations.js' from 'loopback-datasource-juggler' package
/*!
* Presence validator
*/
function validatePresence(attr, conf, err, options) {
if (blank(this[attr])) {
err();
}
}
/*!
* Absence validator
*/
function validateAbsence(attr, conf, err, options) {
if (!blank(this[attr])) {
err();
}
}
/*!
* Length validator
*/
function validateLength(attr, conf, err, options) {
if (nullCheck.call(this, attr, conf, err)) return;
var len = this[attr].length;
if (conf.min && len < conf.min) {
err('min');
}
if (conf.max && len > conf.max) {
err('max');
}
if (conf.is && len !== conf.is) {
err('is');
}
}
/*!
* Numericality validator
*/
function validateNumericality(attr, conf, err, options) {
if (nullCheck.call(this, attr, conf, err)) return;
if (typeof this[attr] !== 'number' || isNaN(this[attr])) {
return err('number');
}
if (conf.int && this[attr] !== Math.round(this[attr])) {
return err('int');
}
}
/*!
* Inclusion validator
*/
function validateInclusion(attr, conf, err, options) {
if (nullCheck.call(this, attr, conf, err)) return;
if (!~conf.in.indexOf(this[attr])) {
err();
}
}
/*!
* Exclusion validator
*/
function validateExclusion(attr, conf, err, options) {
if (nullCheck.call(this, attr, conf, err)) return;
if (~conf.in.indexOf(this[attr])) {
err();
}
}
/*!
* Format validator
*/
function validateFormat(attr, conf, err, options) {
if (nullCheck.call(this, attr, conf, err)) return;
if (typeof this[attr] === 'string') {
if (!this[attr].match(conf['with'])) {
err();
}
} else {
err();
}
}
/*!
* Custom validator
*/
function validateCustom(attr, conf, err, options, done) {
if (typeof options === 'function') {
done = options;
options = {};
}
conf.customValidator.call(this, err, done);
}
/*!
* Uniqueness validator
*/
function validateUniqueness(attr, conf, err, options, done) {
if (typeof options === 'function') {
done = options;
options = {};
}
if (blank(this[attr])) {
return process.nextTick(done);
}
var cond = {where: {}};
cond.where[attr] = this[attr];
if (conf && conf.scopedTo) {
conf.scopedTo.forEach(function(k) {
var val = this[k];
if (val !== undefined)
cond.where[k] = this[k];
}, this);
}
var idName = this.constructor.definition.idName();
var isNewRecord = this.isNewRecord();
this.constructor.find(cond, options, function(error, found) {
if (error) {
err(error);
} else if (found.length > 1) {
err();
} else if (found.length === 1 && idName === attr && isNewRecord) {
err();
} else if (found.length === 1 && (
!this.id || !found[0].id || found[0].id.toString() != this.id.toString()
)) {
err();
}
2017-02-01 17:55:02 +00:00
done();
}.bind(this));
}
var validators = {
presence: validatePresence,
absence: validateAbsence,
length: validateLength,
numericality: validateNumericality,
inclusion: validateInclusion,
exclusion: validateExclusion,
format: validateFormat,
custom: validateCustom,
uniqueness: validateUniqueness,
};
var defaultMessages = {
presence: 'can\'t be blank',
absence: 'can\'t be set',
'unknown-property': 'is not defined in the model',
length: {
min: 'too short',
max: 'too long',
is: 'length is wrong',
},
common: {
blank: 'is blank',
'null': 'is null',
},
numericality: {
'int': 'is not an integer',
'number': 'is not a number',
},
inclusion: 'is not included in the list',
exclusion: 'is reserved',
uniqueness: 'is not unique',
};
/**
* Checks if attribute is undefined or null. Calls err function with 'blank' or 'null'.
* See defaultMessages. You can affect this behaviour with conf.allowBlank and conf.allowNull.
* @param {String} attr Property name of attribute
* @param {Object} conf conf object for validator
* @param {Function} err
* @return {Boolean} returns true if attribute is null or blank
*/
function nullCheck(attr, conf, err) {
// First determine if attribute is defined
if (typeof this[attr] === 'undefined') {
if (!conf.allowBlank) {
err('blank');
}
return true;
} else {
// Now check if attribute is null
if (this[attr] === null) {
if (!conf.allowNull) {
err('null');
}
return true;
}
}
return false;
}
/*!
* Return true when v is undefined, blank array, null or empty string
* otherwise returns false
*
* @param {Mix} v
* Returns true if `v` is blank.
*/
function blank(v) {
if (typeof v === 'undefined') return true;
if (v instanceof Array && v.length === 0) return true;
if (v === null) return true;
if (typeof v === 'number' && isNaN(v)) return true;
if (typeof v == 'string' && v === '') return true;
return false;
}