UPDATE1: Started using ngProgress, but not giving required effect in IE.
Final Update: Best solution found. See last answer below.
The AngularJS application ha
This is the final version, and the best in terms of performance.
It is based on the following:
ngModelController.$validators
property.directive
and $compile
completely Following is the code that will be used to load the validation rules:
//Define general function to add/remove error message from the title attribute
var conErrMsgSep = " | ";
var conErrMsgSepTrim = conErrMsgSep.trim();
function addRemoveValidationMessage(elem, isValid, errMsg) {
if (!isValid && elem.get(0).title.indexOf(errMsg) === -1){
//Add message
if (elem.get(0).title.trim()) {
elem.get(0).title += conErrMsgSep;
} else {
elem.get(0).title = "";
}
elem.get(0).title += errMsg;
} else
if (isValid && elem.get(0).title.indexOf(errMsg) !== -1) {
//Remove message
elem.get(0).title = (elem.get(0).title.replace(errMsg, "")).trim();
if (elem.get(0).title.endsWith(conErrMsgSepTrim)) {
elem.get(0).title = elem.get(0).title.substring(0, elem.get(0).title.length-1).trim();
}
if (elem.get(0).title.startsWith(conErrMsgSepTrim)) {
elem.get(0).title = elem.get(0).title.substring(1, elem.get(0).title.length).trim();
}
}
}
//Define Class/Object to handle adding/removing error messages
//This will be used to save the last error message used, and update 'title' correctly.
function AddRemoveValidationMessage(elem, validatorKey) {
this.elem = elem;
//validatorKey is the rule key. For now, not yet used.
this.validatorKey = validatorKey;
this.isValid = true; //Default is always valid
this.errMsg = "";
this.addRemoveMessage = function(isValid, errMsg) {
if (isValid === undefined) {
isValid = true;
}
if ((!this.isValid && !isValid) || isValid) {
//Last was invalid, and now also invalid, must reomve the old saved error message from 'title'
//and, if now valid, must remove the old saved error message also
addRemoveValidationMessage(this.elem, true, this.errMsg); //Remove message from 'title'
}
if (!isValid) {
//Add new error message if invalid
addRemoveValidationMessage(this.elem, false, errMsg, this.validatorKey);
//Save error message if invalid.
this.errMsg = errMsg;
} else {
//Clear error message if valid
this.errMsg = "";
}
//Save last validation status
this.isValid = isValid;
}
}
function addRequriedValidation(elem, elemModel, elemScope, isRequiredExp) {
var result;
if (!elemModel.$validators.required &&
isRequiredExp.toLowerCase() !== "false" && isRequiredExp !== '*skip*') {
elemModel.$validators.required = function (modelValue, viewValue) {
var errMsg = "Fill in the required value.";
var isValid;
var theElem = elem;
var theExpr = isRequiredExp;
var theVal = modelValue || viewValue;
var isRequiredExpVal = elemScope.$eval(theExpr);
isValid = !isRequiredExpVal || !elemModel.$isEmpty(theVal);
addRemoveValidationMessage(elem, isValid, errMsg)
return isValid;
}
}
}
function addReadonlyRule(elem, elemModel, elemScope, readonlyExp) {
var result=false;
var conSkip = "*skip*"
if (readonlyExp.toLowerCase() === "true" && readonlyExp !== conSkip && readonlyExp) {
elem.attr('readonly', true);
result = true;
} else
//Add readonly validation
if (readonlyExp && readonlyExp.toLowerCase() !== "false" && readonlyExp != conSkip) {
elemScope.$watch(readonlyExp, function(newVal){
var theElem = elem;
theElem.attr('readonly', newVal);
})
result = true;
//angular.element(child).attr('ng-readonly', readonlyExp);
}
return result;
}
function addMaxlengthValidation(elem, elemModel, elemScope, maxLenExp) {
var result;
if (!elemModel.$validators.maxlength && maxLenExp) {
elemModel.$validators.maxlength = function (modelValue, viewValue) {
var errMsg = "Number of characters should not exceeded '{0}' characters.";
var isValid;
var theElem = elem;
var theExpr = maxLenExp;
var maxLenExpVal = elemScope.$eval(theExpr);
isValid = (maxLenExpVal < 0) || elemModel.$isEmpty(viewValue) || (viewValue.length <= maxLenExpVal);
addRemoveValidationMessage(elem, isValid, errMsg.format(maxLenExpVal))
return isValid;
}
}
}
function addMinlengthValidation(elem, elemModel, elemScope, minLenExp) {
var result;
if (!elemModel.$validators.minlength && minLenExp) {
elemModel.$validators.minlength = function (modelValue, viewValue) {
var errMsg = "Number of characters should not be less than '{0}' characters.";
var isValid;
var theElem = elem;
var theExpr = minLenExp;
var minLenExpVal = elemScope.$eval(theExpr);
isValid = elemModel.$isEmpty(viewValue) || viewValue.length >= minLenExpVal;
addRemoveValidationMessage(elem, isValid, errMsg.format(minLenExpVal))
return isValid;
}
}
}
function addPatternValidation(elem, elemModel, elemScope, patternExp, validatorKey, errMsgMask) {
var result;
validatorKey = validatorKey || 'pattern';
if (!elemModel.$validators[validatorKey] && patternExp) {
//Use closures and self invoking function to maintain static value for the related validation function.
elemModel.$validators[validatorKey] = function () {
errMsgMask = errMsgMask || "The entered value '{0}' doesn't match the validation pattern '{1}'.";
var oAddRemoveValidationMessage;
var errMsg;
return function(modelValue, viewValue) {
//This is the actual validation function
var isValid;
var theElem = elem;
var theElemModel = elemModel;
var theExpr = patternExp.replaceAll("\\", "\\\\");
var patternExpVal = elemScope.$eval(theExpr);
if (angular.isString(patternExpVal) && patternExpVal.length > 0) {
patternExpVal = eval(patternExpVal) //new RegExp('^' + patternExpVal + '$');
}
if (patternExpVal && !patternExpVal.test) {
errMsg = 'Expected {0} to be a RegExp but was {1}. Element ID: {2}';
throw Error(errMsg.format(theExpr, patternExpVal, theElem[0].id));
}
patternExpVal = patternExpVal || undefined;
isValid = theElemModel.$isEmpty(viewValue) || angular.isUndefined(patternExpVal) || patternExpVal.test(viewValue);
//Create object to deal with adding and removing error messages from the element 'title' attribute.
//This object is saved within the 'elemModel' and will be used to save the last error message generated.
//This will allow the same object to remove the error message when the status becomes valid.
oAddRemoveValidationMessage =
oAddRemoveValidationMessage || (new AddRemoveValidationMessage(elem, validatorKey));
if (!isValid) {
errMsg = errMsgMask.format(viewValue, patternExpVal)
}
oAddRemoveValidationMessage.addRemoveMessage(isValid, errMsg);
return isValid;
}
}(); //Self invoking function
}
}
function addCAPostalCodeValidation(elem, elemModel, elemScope, isCAPostalCode) {
var result;
var errMsg = "The entered value '{0}' must be a valid Canadian Postal Code.";
var patternExp = "'/^([A-Z]\\d[A-Z] *\\d[A-Z]\\d)$/i'";
var validatorKey = 'caPostalCode';
isCAPostalCode = isCAPostalCode.toLowerCase();
if (isCAPostalCode === "true") {
addPatternValidation(elem, elemModel, elemScope, patternExp, validatorKey, errMsg)
} else
if (isCAPostalCode && isCAPostalCode !== "false") {
elemScope.$watch(isCAPostalCode, function(newVal){
if (elemModel.$validators[validatorKey]) {
delete elemModel.$validators[validatorKey];
}
if (newVal) {
addPatternValidation(elem, elemModel, elemScope, patternExp, validatorKey)
}
});
}
}
//Implement Load Validation Rules.
// - doReadonly - if true, it will only load readonly rules
// if false, it will only load validation rules
theService.loadValidationRules = function (doReadonly) {
var validationList;
var validationListKeys;
var validationKey;
var elem;
var elemModel;
var elemScope;
var validationRule;
var conLoaded = '*loaded*';
doReadonly = doReadonly || false;
validationList = formView.getRequiredField();
if (!validationList) {
console.error("Unexpected error in 'loadValidationRules()': 'validationList' is not initialized.")
return;
}
validationListKeys = Object.keys(validationList);
for (var idx=0; idx < validationListKeys.length; idx++) {
validationKey = validationListKeys[idx];
if (validationKey.startsWith('$')) {
continue;
}
elem = angular.element('#'+validationKey);
if (!elem.length) {
continue;
}
elemModel = elem.controller('ngModel');
if (!elemModel && !doReadonly) {
//We don't need 'ngMode' for readonly
console.warn("'ngModel' was not defined for element ID '%s'.", validationKey);
continue;
}
elemScope = elem.scope() || scope;
validationObjects = validationList[validationKey];
if (validationObjects === "") {
//This means field is required.
if (elemModel.$isEmpty(elemModel.$viewValue)){
elem.addClass('ng-invalid');
result = false;
}
} else
if (angular.isArray(validationObjects)) {
//Loop through validation rules, and flag invalid field by adding the relevant class
for (var ruleIdx=0; ruleIdx < validationObjects.length; ruleIdx++){
validationRule = validationObjects[ruleIdx];
var test = validationRule.test || "true"; //if not exist, it means the rule should always be applied
if (test) {
var testEval = elemScope.$eval(test);
if (testEval) {
var readonlyExp = ((validationRule.readonly || "").toString().trim()) || "false";
if (!doReadonly) {
var isRequiredExp = validationRule.required || "false";
var isRequiredExpVal;
var minLenExp = (validationRule.minlen || "").toString().trim();
var maxLenExp = (validationRule.maxlen || "").toString().trim();
var pattern = (validationRule.pattern || "").toString().trim();
var isCAPostalCode = (validationRule.isCAPostalCode || "false").toString().trim();
isRequiredExp = (angular.isString(isRequiredExp)?isRequiredExp:isRequiredExp.toString()).trim();
//Required Validation: add attributes only if needed
addRequriedValidation(elem, elemModel, elemScope, isRequiredExp);
addMaxlengthValidation(elem, elemModel, elemScope, maxLenExp);
addMinlengthValidation(elem, elemModel, elemScope, minLenExp);
addCAPostalCodeValidation(elem, elemModel, elemScope, isCAPostalCode);
} else
if (readonlyExp && readonlyExp !== conLoaded){
var readonlyLoaded;
readonlyLoaded = addReadonlyRule(elem, elemModel, elemScope, readonlyExp);
if (readonlyLoaded) {
validationRule.readonly = conLoaded;
}
}
}
}
//For now, just evaluate the first rule;
break;
}
} else {
console.error("Unexpected error in 'loadValidationRules()': type of 'validationObjects' is unknown with value = %o", validationObjects);
}
}
}
And the code below can be used to load and trigger validation:
function ngProcessReviewCore() {
//Add css class for invalid radion buttons
var appUrl = getAppURL();
var isFormValid = false;
if (BusinessLogic.isValidationDynamic()) {
isFormValid = $scope.mainForm.$valid;
} else {
isFormValid = $scope.mainForm.$valid;
}
if(isFormValid) {
//Submit to server here
} else {
$timeout(function () {
$scope.addValidationClassRadio();
});
popUpMsg("infoPopUp", "Please fill the required field and clear validation errors!");
}
}
//Integrated with Angular.
//This is needed to ensure validation is integrated with Angular.
//Implement manual validation
$scope.ngProcessReview = function () {
$scope.startExecValidations();
if (BusinessLogic.isValidationManual()) {
//Load validation rules before start of validation
BusinessLogic.loadValidationRules();
}
//Use timeout to give time for validations to be reflected
$timeout(function(){
ngProcessReviewCore();
}, 100)
}
and, to load the readonly
rules, you need to run this code when done rendering all elements:
$scope.runWhenDone = function () {
console.log('Load only readonly rules...');
var loadOnlyReadonlyRules = true;
BusinessLogic.loadValidationRules(loadOnlyReadonlyRules)
}
And you can use the directive when-rendering-done
as defined in this solutions:
...
...