import {module} from './module'; import {loopbackValidations} from 'vendor'; directive.$inject = ['$interpolate', '$compile', '$window'] export function directive(interpolate, compile, $window) { return { restrict: 'A', require: ['ngModel', '^^form'], link: function (scope, element, attrs, ctrl) { 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; 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); input.$validators[key] = function(value) { return isValid(value, conf, messageNode); } } 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)); } scope.$on('destroy', function() { customValidator = null; }); } } function firstUpper(str) { return str.charAt(0).toUpperCase() + str.substr(1); } function createMessage(form, input, key) { var template = ''; var span = interpolate(template)({ form: form, input: input, key: key }); var element = angular.element(span); return element; } function createMessages(form, input) { var template = '
'; 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; } if (!message && defaultMessages[conf.validation]) { message = defaultMessages[conf.validation]; } if (!message) { message = 'is invalid'; } if (kind) { code += '.' + kind; if (message[kind]) { message = message[kind]; } else if (defaultMessages.common[kind]) { message = defaultMessages.common[kind]; } else { message = 'is invalid'; } } messageNode.text(message); // code valid = false; } 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(); } 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; }