UNPKG

angular-ui-form-validation

Version:

A plugin for performing validation in angularjs without writing lots of boilerplate code or duplicating logic.

741 lines (591 loc) 33.2 kB
angular_ui_form_validations = (function(){ var customValidations, createValidationFormatterLink, customValidationsModule, getValidationPriorityIndex, getValidationAttributeValue, getValidatorByAttribute, getCustomTemplateIfDefined, customTemplates, isCurrentlyDisplayingAnErrorMessageInATemplate, currentlyDisplayedTemplate, dynamicallyDefinedValidation, defaultFieldIsValidSuccessFn, $q, $timeout, $log; customTemplates = []; customValidations = []; var submitLink = function ($scope, $element, $attrs, ngModelController) { if(typeof($attrs['validationSubmit']) === 'undefined') { return; } var validationSubmit = getValidationAttributeValue($attrs['validationSubmit'], 'onSubmit', true); var formName = getValidationAttributeValue($attrs['validationSubmit'], 'formName', true); var form = angular.element('[name='+ formName +']'); if(form.length === 0){ form = $element.parents('[name=' + formName + ']'); } validationSubmit = validationSubmit.substring(0, validationSubmit.indexOf('(')); var submitFunction = $scope[validationSubmit]; var indexOfDot = validationSubmit.indexOf('.'); if (indexOfDot !== -1) { var model = validationSubmit.substring(0, indexOfDot); var modelFunction = validationSubmit.substring(indexOfDot + 1); submitFunction = $scope[model][modelFunction]; } var formIsValid = false;//TODO: logic to check if form is valid ($scope.$watch $form.pristine, ) $element.addClass('invalid'); $element.removeClass('valid'); var formIsSubmittable = function () { console.log('Entering formIsSubmittable'); formIsValid = true; $element.addClass('valid'); $element.removeClass('invalid'); }; var formIsNotSubmittable = function () { console.log('Entering formIsNotSubmittable'); formIsValid = false; $element.addClass('invalid'); $element.removeClass('valid'); }; var formValidityChangeListener = function (currentValidValue, previousValidValue) { console.log('Entering formValidityChangeListener', {args: arguments}); var valid = currentValidValue; if (valid === true) { formIsSubmittable(); } else { formIsNotSubmittable(); } }; var isFormValid = function (scope) { console.log('Entering isFormValid', {args: arguments}); var valid = false; var hasMissingRequiredField = false; var fields = form.children('input, select'); fields.each(function (index, field) { if(field.value.trim() === "" && $(field).attr('validation-field-required') === "true") { hasMissingRequiredField = true; false; } }); if(hasMissingRequiredField === true) { return false; } //At this point all fields that require a value have one and are therefore all validated as success or not //therefore the difference between validated fields and total required fields should be zero at this point //TODO: make valid true if this is the case var numTotalFields = fields.length; var numValidatedFields = form.children('.ValidationLiveSuccess').length; if(numTotalFields - numValidatedFields === 0) { valid = true; } return valid; }; $scope.$watch( function (scope) { console.log('Entering validation submit directive watch expression', { args: arguments }); return isFormValid(scope); }, function (valid) { console.log('Entering validation submit directive watch listener', { args: arguments }); if (valid === true) { formIsSubmittable(); } else { formIsNotSubmittable(); } }); $element.on('click', function () { if(formIsValid === true){ submitFunction.apply($scope, []); } }); formValidityChangeListener(isFormValid($scope)); }; dynamicallyDefinedValidation = { customValidationAttribute: 'validationDynamicallyDefined', errorCount: 0, latestElement: null, _errorMessage: 'Field is invalid', _success: function () {}, success: function () { return dynamicallyDefinedValidation._success && dynamicallyDefinedValidation._success.apply(this, arguments); }, errorMessage: function () { return dynamicallyDefinedValidation._errorMessage; }, validator: function (errorMessageElement, val, attr, element, model, modelCtrl, scope) { var valid, hydrateDynamicallyDefinedValidation, scopeValidations, setErrorIdentifier, setValidity, validatorArgs, deferred; validatorArgs = arguments; scopeValidations = scope[attr]; hydrateDynamicallyDefinedValidation = function (validation) { dynamicallyDefinedValidation._errorMessage = validation.errorMessage; dynamicallyDefinedValidation._success = validation.success; return validation; }; setErrorIdentifier = function (validation, index) { var identifier, clone; identifier = 'validationdynamicallydefined'; if(validation.identifier && validation.identifier !== '' && validation.identifier !== null) { identifier += validation.identifier.charAt(0).toUpperCase() + validation.identifier.slice(1).toLowerCase(); } else { identifier += index; } clone = angular.copy(validation); clone.identifier = identifier; return clone; }; setValidity = function (validation) { valid = validation.validator.apply(scope, validatorArgs); modelCtrl.$setValidity(validation.identifier, valid); return valid === true; }; Lazy(scopeValidations) .map(hydrateDynamicallyDefinedValidation) .map(setErrorIdentifier) .map(setValidity) .each(function(valid){ if(valid === false){ dynamicallyDefinedValidation.errorCount++; dynamicallyDefinedValidation.latestElement = element; return false; } return true; }); return valid; } }; onValidationComplete = function (fieldIsValid, value, validationAttributeValue, $element, model, ngModelController, $scope, customOnSuccess) { if(fieldIsValid) { $element.addClass('ValidationLiveSuccess'); $element.addClass($element.attr('validation-live-success-cls')); $element.removeClass($element.attr('validation-live-fail-cls')); customOnSuccess.call(this, value, validationAttributeValue, $element, model, ngModelController, $scope); } else { $element.removeClass('ValidationLiveSuccess'); $element.removeClass($element.attr('validation-live-success-cls')); $element.addClass($element.attr('validation-live-fail-cls')); } }; isCurrentlyDisplayingAnErrorMessageInATemplate = function (inputElement) { var isCurrentlyDisplayingAnErrorMessageInATemplate = false; Lazy(customTemplates) .each(function(template){ if(template.attr('templateUid') === inputElement.attr('templateUid')){ isCurrentlyDisplayingAnErrorMessageInATemplate = true; currentlyDisplayedTemplate = template; return false; } return true; }); return isCurrentlyDisplayingAnErrorMessageInATemplate; }; getValidationAttributeValue = function (attr, property, strict) { var value; if(attr === undefined) { return undefined; } property = property || 'value'; value = attr; try{ var json = JSOL.parse(attr); } catch (e) { } if(json !== null && typeof(json) === 'object'){ if(json.hasOwnProperty(property)){ hasProperty = true; value = json[property]; } else { hasProperty = false; value = undefined; if(strict !== true){ value = json.value; } } return value; } else if(strict === true){ //strict assumes you must be passing in an object attr return undefined; } return value; }; getValidationAttributeByPropertyName = function (attr, property) { var value; try{ value = JSOL.parse(attr)[property]; } catch (e) { value = null; } return value; }; getCustomTemplateIfDefined = function (attr, templateRetriever) { var deferred, templateUrl, promise; deferred = $q.defer(); promise = deferred.promise; try{ templateUrl = JSOL.parse(attr)['template']; if(templateUrl === undefined || templateUrl === null || templateUrl === '') { deferred.reject('No template url specified.'); } else { promise = templateRetriever.getTemplate(templateUrl); } } catch (e) { deferred.reject('Error retrieving custom error template: ' + e); } return promise; }; getValidatorByAttribute = function (customValidationAttribute) { var validator; Lazy(customValidations) .each(function (validation) { if(validation.customValidationAttribute === customValidationAttribute){ validator = validation.validator; return false; } return true; }); return validator; }; getValidationPriorityIndex = function (customValidationAttribute) { var index; Lazy(customValidations) .each(function(validation, i){ if(validation.customValidationAttribute === customValidationAttribute){ index = i; return false; } return true; }); return index; }; createValidationFormatterLink = function (formatterArgs, templateRetriever, q, timeout, log) { $q = q; $timeout = timeout; $log = log; return function($scope, $element, $attrs, ngModelController) { var customErrorMessage, errorMessage, errorMessageElement, modelName, model, propertyName, runCustomValidations, validationAttributeValue, customErrorTemplate; $timeout(function() { var getErrorMessageElement, addWatcherForDynamicallyDefinedValidations, addWatcherToWrapErrorInCustomTemplate, isValidValidationAttributeValue, getFormatterArgsErrorMessage, installErrorMessageElement, installSpecialErrorCases; var rawCustomValidationAttribute = $attrs[formatterArgs.customValidationAttribute]; validationAttributeValue = getValidationAttributeValue(rawCustomValidationAttribute); isValidValidationAttributeValue = ( validationAttributeValue && ( validationAttributeValue !== 'undefined' ) && ( validationAttributeValue !== 'false' ) ); getErrorMessageElement = function () { var ifCheckboxOrRadio = ''; if((/checkbox|radio/).test($element[0].type)){ ifCheckboxOrRadio = 'checkboxradioerror '; } return angular.element( '<span data-custom-validation-priorityIndex='+ getValidationPriorityIndex(formatterArgs.customValidationAttribute) + ' data-custom-validation-attribute='+ formatterArgs.customValidationAttribute + ' data-custom-field-name='+ $element.attr('name') + ' class="CustomValidationError '+ ifCheckboxOrRadio + formatterArgs.customValidationAttribute + ' '+ propertyName +'property">' + errorMessage + '</span>'); }; addWatcherForDynamicallyDefinedValidations = function () { $scope.$watch(function(){ return dynamicallyDefinedValidation.errorCount; }, function () { if (dynamicallyDefinedValidation.errorCount === 0) { return; } var currentElementFieldName = errorMessageElement.attr('data-custom-field-name'); var latestValidatedFieldName = dynamicallyDefinedValidation.latestElement.attr('name'); if(latestValidatedFieldName === currentElementFieldName) { errorMessageElement.html(dynamicallyDefinedValidation.errorMessage()); } }); }; addWatcherToWrapErrorInCustomTemplate = function (template) { var errorMessageToggled; customErrorTemplate = angular.element(template); customErrorTemplate.html(''); errorMessageToggled = function () { var templateUid = Math.random(); if(errorMessageElement.css('display') === 'inline' || errorMessageElement.css('display') === 'block') { $log.log('error showing'); $element.attr('templateUid', templateUid); customErrorTemplate.attr('templateUid', templateUid); errorMessageElement.wrap(customErrorTemplate); customTemplates.push(angular.element(errorMessageElement.parents()[0])); } else { $log.log('error NOT showing'); $element.removeAttr('templateUid'); if(errorMessageElement.parent().is('.' + customErrorTemplate.attr('class'))){ errorMessageElement.unwrap(customErrorTemplate); } } }; $scope.$watch(function (){ return errorMessageElement.css('display'); }, errorMessageToggled); $scope.$on('errorMessageToggled', errorMessageToggled); }; getFormatterArgsErrorMessage = function () { var errorMessage; if(typeof(formatterArgs.errorMessage) === 'function'){ errorMessage = formatterArgs.errorMessage(validationAttributeValue); } else { errorMessage = formatterArgs.errorMessage; } return errorMessage; }; installErrorMessageElement = function () { errorMessage = getFormatterArgsErrorMessage(); errorMessageElement = getErrorMessageElement(); $element.after(errorMessageElement); errorMessageElement.hide(); if(formatterArgs.customValidationAttribute === 'validationDynamicallyDefined') { addWatcherForDynamicallyDefinedValidations(); } getCustomTemplateIfDefined($attrs[formatterArgs.customValidationAttribute], templateRetriever) .then(function (template) { addWatcherToWrapErrorInCustomTemplate(template); }); customErrorMessage = getValidationAttributeByPropertyName($attrs[formatterArgs.customValidationAttribute], 'message'); if(customErrorMessage !== null) { errorMessageElement.html(customErrorMessage); } }; installSpecialErrorCases = function () { if (formatterArgs.customValidationAttribute === 'validationNoSpace') { $element.keyup(function (event){ if (event.keyCode === 8) { model[propertyName] = ($element.val().replace(/\s+$/, '')); } }); } if (formatterArgs.customValidationAttribute === 'validationConfirmPassword') { var passwordFieldId = $element.attr('passwordFieldId') || 'password'; var passwordFieldSelector = '#' + passwordFieldId; var validationConfirmPasswordHandlerSelector = passwordFieldSelector + ', #' + $element[0].id; var confirmPasswordElement = $element; var passwordElement = $element.parent().children(passwordFieldSelector); $($element.parent()).on('keyup blur', validationConfirmPasswordHandlerSelector, function (target) { console.log('Entering validationConfirmPassword keyup blur handler', {args: arguments}); console.log('validationConfirmPassword handler selector' + validationConfirmPasswordHandlerSelector); var passwordMatch, confirmPasswordIsDirty; confirmPasswordIsDirty = /dirty/.test(confirmPasswordElement.attr('class')); if(confirmPasswordIsDirty === false) { return; } passwordMatch = passwordElement.val() === $element.val(); console.log('--- validationConfirmPassword keyup blur handler passwordMatch', passwordMatch); ngModelController.$setValidity('validationconfirmpassword', passwordMatch); confirmPasswordElement.siblings('.CustomValidationError.validationConfirmPassword:first').toggle(! passwordMatch); onValidationComplete(passwordMatch, passwordMatch, validationAttributeValue, $element, model, ngModelController, $scope, function () { console.log('Entering validationConfirmPassword onCustomSuccess callback'); if (formatterArgs.success) { formatterArgs.success(); } }); }); return; } if (formatterArgs.customValidationAttribute === 'validationFieldRequired') { $element.parents('form').find('label[for='+$element.attr('id')+']').addClass('requiredFieldLabel'); } }; runCustomValidations = function (eventType) { console.log('Entering runCustomValidations', { eventType: eventType }); var isValid, value, customValidationBroadcastArg, currentlyDisplayingAnErrorMessage, currentErrorMessage, currentErrorMessageIsStale, currentErrorMessageValidator, currentErrorMessagePriorityIndex, currentErrorMessageIsOfALowerPriority, successFn; var evaluateAsValid = false; //assuming non-blur events suggest a keypress/keyup/keydown/input event //only blur and runCustomValidations events are always evaluated automatically regardless of validateWhileEntering if(eventType !== 'blur' && eventType !== 'runCustomValidations') { //validating non-blur events only when formatterArgs have specified to validateWhileEntering if(formatterArgs.validateWhileEntering && formatterArgs.validateWhileEntering === true) { //Do nothing continue on } else { //TOOD: figure out why returning here is causing the cursor to be set to last position and // slowing down UI by preventing key to be pressed while it is moved, thereby causing confusing and bad UX // return; evaluateAsValid = true; } } //Do not validate if input is pristine, i.e nothing entered by user yet if($element.hasClass('ng-pristine') && eventType !=='runCustomValidations'){ console.log('--- runCustomValidations not validating because pristine'); return; } successFn = formatterArgs.success || function(){}; function getCurrentlyDisplayingErrorMessage () { var fieldNameSelector, selector; fieldNameSelector = '[data-custom-field-name="'+ $element.attr('name') +'"]'; selector = '.CustomValidationError[style="display: inline;"]'+fieldNameSelector+', '+ '.CustomValidationError[style="display: block;"]'+fieldNameSelector; if(isCurrentlyDisplayingAnErrorMessageInATemplate($element)) { return currentlyDisplayedTemplate.children(selector); } else { return $element.siblings(selector); } } function getElementValue() { var value = $element.val().replace(/\s+$/, ''); if((/select/).test($element[0].type)){ value = $element[0].options[$element[0].selectedIndex].innerHTML; } if((/checkbox|radio/).test($element[0].type)){ value = $element[0].checked === true? 'true' : ''; } return value; } function toggleRequiredLabelClass() { if(value === '') { $element.parents('form').find('label[for='+$element.attr('id')+']').addClass('requiredFieldLabel'); } else { $element.parents('form').find('label[for='+$element.attr('id')+']').removeClass('requiredFieldLabel'); } } function runValidation() { return formatterArgs.validator(errorMessageElement, value, validationAttributeValue, $element, model, ngModelController, $scope, rawCustomValidationAttribute); } function getPropertyNameClass (pname) { return pname.replace('.', '\\.'); } function whenIsNotCurrentlyDisplayingAnErrorMessage() { $log.log('is not currently displaying an error message', customValidationBroadcastArg); var classNames = ".CustomValidationError."+ formatterArgs.customValidationAttribute + "." + getPropertyNameClass(propertyName) + "property:first"; $log.log(classNames); $element.siblings(classNames).toggle(!isValid); } function whenIsNotCurrentlyDisplayingAnErrorMessageInATemplate(){ $log.log('is not currently displaying an error message in a template', customValidationBroadcastArg); currentErrorMessageValidator = getValidatorByAttribute(currentErrorMessage.attr('data-custom-validation-attribute')); currentErrorMessageIsStale = currentErrorMessageValidator(errorMessageElement.clone(), value, $attrs[currentErrorMessage.attr('data-custom-validation-attribute')], $element, model, ngModelController, $scope, rawCustomValidationAttribute); currentErrorMessagePriorityIndex = parseInt(currentErrorMessage.attr('data-custom-validation-priorityIndex'), 10); currentErrorMessageIsOfALowerPriority = currentErrorMessagePriorityIndex >= getValidationPriorityIndex(formatterArgs.customValidationAttribute); if (currentErrorMessageIsStale || (!currentErrorMessageIsStale && currentErrorMessageIsOfALowerPriority && !isValid)) { currentErrorMessage.hide(); $element.siblings('.CustomValidationError.'+ formatterArgs.customValidationAttribute + '.' + getPropertyNameClass(propertyName) + 'property:first') .toggle(!isValid); } } function whenIsCurrentlyDisplayingAnErrorMessageInATemplate(){ $log.log('is currently displaying an error message in a template', customValidationBroadcastArg); currentErrorMessageValidator = getValidatorByAttribute(currentErrorMessage.attr('data-custom-validation-attribute')); currentErrorMessageIsStale = currentErrorMessageValidator( errorMessageElement, value, getValidationAttributeValue($attrs[currentErrorMessage.attr('data-custom-validation-attribute')]), $element, model, ngModelController ); currentErrorMessagePriorityIndex = parseInt(currentErrorMessage.attr('data-custom-validation-priorityIndex'), 10); currentErrorMessageIsOfALowerPriority = currentErrorMessagePriorityIndex >= getValidationPriorityIndex(formatterArgs.customValidationAttribute); if (currentErrorMessageIsStale || (!currentErrorMessageIsStale && currentErrorMessageIsOfALowerPriority && !isValid && currentlyDisplayedTemplate.children().attr('class').indexOf(formatterArgs.customValidationAttribute) === -1)) { currentErrorMessage.hide(); $element.siblings('.CustomValidationError.'+ formatterArgs.customValidationAttribute + '.' + getPropertyNameClass(propertyName) + 'property:first') .toggle(!isValid); $scope.$broadcast('errorMessageToggled'); } } currentErrorMessage = getCurrentlyDisplayingErrorMessage(); currentlyDisplayingAnErrorMessage = currentErrorMessage.length > 0; value = getElementValue(); if (formatterArgs.customValidationAttribute === 'validationFieldRequired') { toggleRequiredLabelClass(); } //TODO: using this temporarily because if we don't evaluate we have wierd UX whereby cursor moved to end of string if (evaluateAsValid === true) { isValid = true; } else { isValid = runValidation(); } console.log('--- runCustomValidations validity ' + isValid); ngModelController.$setValidity(formatterArgs.customValidationAttribute.toLowerCase(), isValid); var status = isValid === true ? ' passed' : ' failed'; customValidationBroadcastArg = { isValid: isValid, validation: $element.attr('id') + ' ' + formatterArgs.customValidationAttribute + status, model: model, controller: ngModelController, element: $element }; console.log('--- runCustomValidations customValidationBroadcastArg', customValidationBroadcastArg); console.log('--- runCustomValidations currentlyDisplayingAnErrorMessage ' + currentlyDisplayingAnErrorMessage); if(! currentlyDisplayingAnErrorMessage) { whenIsNotCurrentlyDisplayingAnErrorMessage(); } else if(! isCurrentlyDisplayingAnErrorMessageInATemplate($element)){ whenIsNotCurrentlyDisplayingAnErrorMessageInATemplate(); } if(isCurrentlyDisplayingAnErrorMessageInATemplate($element)) { console.log('--- runCustomValidations isCurrentlyDisplayingAnErrorMessageInATemplate'); whenIsCurrentlyDisplayingAnErrorMessageInATemplate(); } $scope.$broadcast('customValidationComplete', customValidationBroadcastArg); onValidationComplete(!(currentlyDisplayingAnErrorMessage || isCurrentlyDisplayingAnErrorMessageInATemplate($element) || !isValid), value, validationAttributeValue, $element, model, ngModelController, $scope, successFn); console.log('--- runCustomValidations returnValue ' + value); return value; }; if (isValidValidationAttributeValue === true) { modelName = $attrs.ngModel.substring(0, $attrs.ngModel.indexOf('.')); propertyName = $attrs.ngModel.substring($attrs.ngModel.indexOf('.') + 1); model = $scope[modelName]; installErrorMessageElement(); installSpecialErrorCases(); ngModelController.$parsers.push(function() { return runCustomValidations('input'); }); $element.on('blur', function (event) { runCustomValidations(event.type); }); $scope.$on('runCustomValidations', function () { runCustomValidations('runCustomValidations'); }); } }); }; }; customValidationsModule = angular.module('directives.customvalidation.customValidations', [ 'directives.invalidinputformatter.invalidInputFormatter', 'services.templateRetriever' ]) .factory('customValidationUtil', function (templateRetriever, $q, $timeout, $log) { return { createValidationLink: function (customValidation) { customValidations.push(customValidation); return createValidationFormatterLink(customValidation, templateRetriever, $q, $timeout, $log) } } }) .directive('input', function (customValidationUtil) { return { require: '?ngModel', restrict: 'E', link: customValidationUtil.createValidationLink(dynamicallyDefinedValidation) }; }) .directive('select', function (customValidationUtil) { return { require: '?ngModel', restrict: 'E', link: customValidationUtil.createValidationLink(dynamicallyDefinedValidation) }; }) .directive('select', function (customValidationUtil) { return { require: '?ngModel', restrict: 'E', link: customValidationUtil.createValidationLink({ customValidationAttribute: 'validationFieldRequired', validateWhileEntering: true, errorMessage: 'This is a required field', validator: function (errorMessageElement, val){ return (/\S/).test(val); } }) }; }) .directive('button', function (customValidationUtil) { return { restrict: 'E', link: submitLink }; }) .directive('a', function (customValidationUtil) { return { restrict: 'E', link: submitLink }; }); //shared config functions return { getValidationAttributeValue: getValidationAttributeValue }; })();