Slow loading of AngularJS app in IE - add progress

2019-03-04 23:10发布

UPDATE1: Started using ngProgress, but not giving required effect in IE.
Final Update: Best solution found. See last answer below.


The AngularJS application has multiple tabs and each tab may have up to 100 fields. The Data is retrieved from DB using several Ajax calls and a related loop is used to initialize each of the following: validation rules, drop-down list items and finally the field values. In some cases, we are using combination of Javascript and AngularJS way to get the required effect.

Mind you that loading of Validation Rules involves modifying the directive such as ng-required and ng-max which will require to use $compile to activate the validation rule.

This question has two parts:

  • AngularJS App has noticeable slow loading effect under IE. Under Chrome Browser, loading speed is much better.

How we can troubleshoot and analyse slow loading issues under IE to pinpoint the location of the issue? How I can work on performance analysis tools under IE?

  • In the meantime, thinking to add Progress Bar to be updated after completing loading each of the data parts mentioned above: Validation Rules, drop-down list items, and field values.

I implemented ngProgress plugin in my project, and it works fine under Chrome, but under IE it is not giving the required effect. The progress bar will show and complete at the very end of page loading. It seems under IE that the progress bar won't show immediately at the start of the page rendering. Mind you that in my project I am using directive extensively, and large number of them use $compile service.

I did some research, and realized that IE won't update the DOM display immediately... it will wait until a later stage to update all at once, or at least this was my understanding. So the trick here is how to force the IE to reflect DOM changes as soon as possible.

I used this approach which helped get better results under IE:

app.controller('formMainController', ['$scope', '$timeout', '$interval', 'ngProgressFactory',
                function($scope, $timeout, $interval, $q, ngDialog, ngProgressFactory) {
    $scope.progressbar = ngProgressFactory.createInstance();
    $scope.progressbar.start();
    $scope.stopProgressbar = $interval(function(){
        $scope.progressbar.setParent(document.getElementsByTagName("BODY")[0]);
    },10);
       ...
       ...
       //After getting all data from DB
    $scope.mainPromise.then(function(success) {
        $interval.cancel($scope.stopProgressbar);
        $timeout(function(){
            $scope.progressbar.complete();
        }, 3000);
        return 'main promise done';
    })
}]);

With the above, under IE, I can see the progress bar showing much earlier than before, then it will make 2 step progress, then it will freeze for about 2 seconds, then continues normally. When watch the console window, I can see that it will freeze while the other many directives are being executed especially the one that uses $compile service with priority: 100 and terminal: true, .

Any idea how to make it better?

Note: This thread has similar problem, but I didn't find a relevant solution.

Tarek

5条回答
2楼-- · 2019-03-04 23:30

The problem got worse after reaching 1000+ fields. IE 11 took 3+ minutes to complete loading. I did further optimization and now the results are as follows for the time to complete loading:

It is confirmed that the bottleneck is in the loop that will load the validation rules and apply them on the elements, then it will perform compile using $compile service.

The validation rules are stored in DB using json format and retrieved using requiredFieldsPromise. See code sample below.

Following is the new updated code for directive check-if-required:

app.directive('checkIfRequired',  function($compile, $parse, $interpolate, $timeout, $q, BusinessLogic){
    return {
        priority: 100,
        terminal: true,
        restrict: 'A',
        require: '?^form',
        link: function (scope, el, attrs, ngForm) {
            var saveIsValidationRequired;
            var mainElmID = $interpolate(el[0].id)(scope);
            var resolvedPromise;
            var getChildren = function() {
                var resultChildren;
                //Return list of elements which were not compiled before 'compiled === undefined'
                resultChildren = $(':input', el);
                //Use code below just in case we want to extract the elements which are not compiled.
                /*resultChildren = $(':input', el).filter(function(){
                    var result;
                    result = 
                        ($(this).attr('compiled') === undefined)
                    return result;
                });*/
                //Use $interpolate to get the final result for each ID...
                for (var i=0; i < resultChildren.length; i++) {
                    if (resultChildren[i].id) {
                        resultChildren[i].id = $interpolate(resultChildren[i].id)(scope);
                    }
                }
                return resultChildren;
            }
            //User resolvedPromise when no such promise is available.
            resolvedPromise = $q.when('resolved');
            //Code improvement to make this directive more general
            //      Since this directive can be used from within an isolated scope directive such as 'photo-list-upload', then
            //      additional parameters are required to make it work properly.
            //      Make sure all required functions are defined or report warning.
            //      If the function is not defined within 'scope' it will be looked up from within 'BusinessLogic.getScope()'.
            //      If not found at all, default is used, and warning is reported.
            scope.getIsValidationRequired = scope.getIsValidationRequired || 
                                       (BusinessLogic.getScope().getIsValidationRequired) || 
                                       (console.warn("Directive 'check-if-required' element '%s' - function 'scope.getIsValidationRequired()' is not defined. It will always be false.", mainElmID), 
                                         function () {
                                           return false;
                                         }     
                                       );
            //The promise 'requiredFieldPromise' is used to retrieve list of validation rules from DB
            //Break Point Condition: scope.listData.photosFormName == "subjectPhotos"
            scope.stopExecValidations = scope.stopExecValidations || BusinessLogic.getScope().stopExecValidations || 
                        (console.warn("Directive 'check-if-required' element '%s' - function 'scope.stopExecValidations()' is not defined. Dummy function is used instead.", mainElmID),
                          function () {
                            //Dummy
                          }
                        );
            scope.requiredFieldsPromise = 
                scope.requiredFieldsPromise || (BusinessLogic.getScope().requiredFieldsPromise) || 
                (console.warn("Directive 'check-if-required' element '%s' - function 'scope.requiredFieldsPromise' is not defined. Resolved promise will be used.", mainElmID),
                 resolvedPromise);
            //If needed, stop validation while adding required attribute
            //Save current flag value
            saveIsValidationRequired = scope.getIsValidationRequired();
            scope.stopExecValidations();
            //remove the attribute `check-if-required` to avoid recursive calls
            el.removeAttr('check-if-required');         
            // NE-2808 - Define function to add validation message using $watch
            //                As soon as an error is detected, then 'title' will be set to the error
            //                Parameters:
            //                  - ngForm: Angualr Form 
            //                  - elm: The HTML element being validated
            //                  - errAttr: the name of the error attribute of the field within ngForm:
            //                      ngFormName.FieldName.$error.errAttributeName
            //                  - errMsg: The error message to be added to the title 
            //                  - msgVar1: optional substitution variable for the error message
            var addValidationMessage = function (ngForm, elm, errAttr, errMsg, msgVar1) {
                //Use $timeout to ensure validation rules are added and compiled.
                //After compile is done then will start watching errors
                $timeout(function(){
                    var elmModel;
                    var ngModelName="";
                    //Get the name of the 'ng-model' of the element being validated
                    elmModel = angular.element(elm).controller('ngModel');
                    if (elmModel && elmModel.$name) {
                        ngModelName = elmModel.$name;
                    }
                    if (!ngModelName) {
                        ngModelName = angular.element(elm).attr('ng-model');
                    }
                    if (ngModelName) {
                        scope.$watch(ngForm.$name + '.' + ngModelName + '.$error.' + errAttr,
                            function (newValue, oldValue){
                                //console.log("elm.id =", elm.id);
                                //The validation error message will be placed on the element 'title' attribute which will be the field 'tooltip'.
                                //newValue == true means there is error
                                if (newValue) {
                                    var msgVar1Val;
                                    //Perform variable substitution if required to get the final text of the error message.
                                    if (msgVar1) {
                                        msgVar1Val = scope.$eval(angular.element(elm).attr(msgVar1));
                                        errMsg = errMsg.format(msgVar1Val);
                                    }
                                    //Append the error to the title if neeeded
                                    if (elm.title) {
                                        elm.title += " ";
                                    } else {
                                        elm.title = "";
                                    }
                                    elm.title += errMsg;
                                } else {
                                    //Remove the error if valid.
                                    //child.removeAttribute('title');
                                    if (elm.title) {
                                        //Remplace the error message with blank.
                                        elm.title = elm.title.replace(errMsg, "").trim();
                                    }
                                }
                            });
                    } else {
                        //console.warn("Warning in addValidationMessage() for element ID '%s' in ngForm '%s'. Message: 'ng-model' is not defined.", elm.id, ngForm.$name)
                    }
                }, 1000);       
            }             
            function doApplyValidation(scope, el, attrs, ngForm) {
                var children;
                children = getChildren();
                mainElmID = $interpolate(el[0].id)(scope);
                validationList=formView.getRequiredField();
                for (var subformIdx=0; subformIdx < Object.keys(validationList).length; subformIdx++) {
                    var keySubform = Object.keys(validationList)[subformIdx];
                    var subform = validationList[keySubform];
                    var lastFieldID;
                    lastFieldID = Object.keys(subform)[Object.keys(subform).length-1];
                    for (var childIdx=0; childIdx < Object.keys(subform).length; childIdx++) {
                        var childID = Object.keys(subform)[childIdx];
                        var validObjects;
                        var childElm;
                        var child;
                        var elmScope;
                        var elmModel;
                        childID = childID.trim();
                        //Find the element with id = childID within the 'el' section.
                        //Use 'getChildren()' since the result list has ID values which are interpolated.
                        childElm = children.filter('#'+childID);
                        if (childElm.length) {
                            //Validation rule for 'childID': related element was found, and now will apply validation rule.
                            validObjects = subform[childID];
                            child = childElm.get(0);
                            elmScope = angular.element(child).scope() || scope;
                            elmModel = angular.element(child).controller('ngModel');
                            var maxlength = scope.$eval(angular.element(child).attr('ng-maxlength'));
                            //var errMsg = ("Number of characters entered should not exceed '{0}' characters.").format(maxlength);
                            // NE-2808 - add validation message if length exceeds the max
                            var errMsg = "Number of characters entered should not exceed '{0}' characters.";
                            addValidationMessage(ngForm, child, 'maxlength', errMsg, 'ng-maxlength');                           //Check if the element is not in "Required" list, and it has an expression to control requried, then
                            //... add the attribute 'ng-required' with the expression specified to the element and compile.
                            if (!angular.element(child).prop('required') && child.attributes.hasOwnProperty("check-if-required-expr")) {
                                console.error("Unexpected use for attribute 'check-if-required-expr' in directive 'check-if-required' for element ID '%s'. Will be ignored.", childID);
                            }
                            if (validObjects === "") {
                                //This means the field is required
                                angular.element(child).attr('ng-required', "true");
                            }
                            else if (angular.isArray(validObjects)) {
                                //This means that there is a list of validation rules
                                for (var idx=0; idx < validObjects.length; idx++) {
                                    var validObject = validObjects[idx];
                                    var test = validObject.test || "true"; //if not exist, it means the rule should always be applied
                                    var minLenExp = validObject.minlen;
                                    var maxLenExp = validObject.maxlen;
                                    var isRequiredExp = validObject.required || false;
                                    var readonlyExp = validObject.readonly || null;
                                    var pattern = validObject.pattern || "";
                                    var isCAPostalCode = validObject.isCAPostalCode || false;
                                    isRequiredExp = angular.isString(isRequiredExp)?isRequiredExp:isRequiredExp.toString();
                                    if (test) {
                                        var testEval = scope.$eval(test, elmScope);
                                        if (testEval) {
                                            if (minLenExp) {
                                                angular.element(child).attr('ng-minlength', minLenExp);
                                            }
                                            if (maxLenExp) {
                                                angular.element(child).attr('ng-maxlength', maxLenExp);
                                            }
                                            //If the "required" expression is '*skip*' then simply skip.
                                            //If '*skip*' is used, this means the required validation is already defined in code
                                            //and no need to replace it.
                                            if (isRequiredExp && isRequiredExp != '*skip*') {
                                                angular.element(child).attr('ng-required', isRequiredExp);
                                            }
                                            // NE-3211 - add readonly validation
                                            if (readonlyExp && readonlyExp != '*skip*') {
                                                angular.element(child).attr('ng-readonly', readonlyExp);
                                            }
                                            if (pattern) {
                                                angular.element(child).attr('ng-pattern', pattern);
                                            }
                                            if (isCAPostalCode) {
                                                angular.element(child).attr('ng-pattern', "/^([A-Z]\\d[A-Z] *\\d[A-Z]\\d)$/i");
                                                // NE-2808 - add validation message if postal code does not match the RegEx
                                                addValidationMessage(ngForm, child, 'pattern', "Invalid postal code.");
                                            }
                                            //delete the validation rule after it is implemented to improve performance
                                            delete subform[childID]
                                            //TODO: Apply only the first matching validation rule
                                            //      May required further analysis if more that one rule will be added.
                                            break;
                                        }
                                    }
                                }
                            }
                        }
                    } // for loop
                } // for loop
                //After done processing all elements under 'el', compile the parent element 'el'.
                $compile(el, null, 100)(scope);
                //If saved flag value is true, enable back validation
                if (saveIsValidationRequired) {
                    scope.startExecValidations();
                }
            }
            function applyValidationTimeout() {
                //Execute 'doApplyValidation()' in the next cycle, to ensure the child elements have been rendered.
                $timeout(function(){
                    //console.log('applyValidationTimeout', mainElmID);
                    doApplyValidation(scope, el, attrs, ngForm);                    
                }, 100)
            }
            scope.requiredFieldsPromise.then(function(success) {
                //Apply validation when the Required Fields and Validation Rules have been loaded.
                applyValidationTimeout();
            }, function(prmError){
                console.warn("Error occured in 'check-if-required' directive while retrieving 'requiredFieldsPromise' for element '%s': %s", mainElmID, prmError);
            });
        }
    }

});

Though the performance now is much better now, however, I realized that the problem is in using $compile, therefore, I am now thinking to find a solution by avoiding use of $compile. Here is my plan.

Instead of modifying the element HTML by adding 'ng-required' directive, then compile, instead, I can skip HTML and use the ngModel.NgModelController of the related HTML Element, then access the $validators to perform validation using code. If you read the code above, you will see that I have already accessed the ngModel.NgModelController for each element in variable elmModel. I think this variable will provide access to $validators which can be used to add validation to the element. Since the rules are now available in validationList variable, I will write a function to perform validation by looking up this list and apply the available validation on-the-fly.

This will be the improvement in the future sprints.

If you have any feedback, please let me know.

Tarek

查看更多
萌系小妹纸
3楼-- · 2019-03-04 23:31

This is the final version, and the best in terms of performance.

It is based on the following:

  • Load validation rules after elements have been rendered.
  • Execute validation rules just when required
  • Load validation rules using ngModelController.$validators property.
  • Do not use HTML code for validation.
  • Avoid using 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:

<body ng-app="myApp" ng-controller="formMainController as MainController" when-rendering-done="runWhenDone()">
...
...
</body>
查看更多
我命由我不由天
4楼-- · 2019-03-04 23:44

From what you have said the slow loading is due to AngularJS working as opposed to data loading [as evidenced by the fact its slower in IE than Chrome]. If this is true then a loading indicator wont help as it'll just freeze too.

You are far better off following normal performance techniques in angular such as:

  • Show less on the page, a user surely doesnt need to see 100 fields at once? Consider paging.
  • For items that won't change use bind once like so: {{::vm.name}}
  • On the subject of handle bars, its generally more efficient to use <div ng-bind="::vm.name"></div> rather than handle bars {{::vm.name}}
查看更多
聊天终结者
5楼-- · 2019-03-04 23:51

Here is my solution based on solution by @andrew above and using ngProgress Bar component.

CSS:

#ngProgress-container.block-editing {
    pointer-events: all;
    z-index: 99999;
    border: none;
    /* margin: 0px; */
    padding: 0px;
    width: 100%;
    height: 100%;
    top: 0px;
    left: 0px;
    cursor: wait;
    position: fixed;
    background-color: rgba(0, 0, 0, 0.33);
    margin-top:10px;
    #ngProgress {
        margin-top:-9px;
        width:5px; /* Force display progress as early as possible */
        opacity:1; /* Force display progress as early as possible */
    }
}

JS - in the beginning:

$scope.progressbar = ngProgressFactory.createInstance();
//To force display of progress bar as early as possible
$scope.progressbar.setParent(document.getElementsByTagName("BODY")[0]);
$scope.progressbar.set(1);
$scope.progressbar.getDomElement().addClass('block-editing');
$scope.stopProgressbar = $timeout(function(){
    $scope.progressbar.setParent(document.getElementsByTagName("BODY")[0]);
},10);
$timeout(function(){
    $scope.progressbar.start();
},100);

JS - in the end:

//Stop progress bar
$interval.cancel($scope.stopProgressbar);
$timeout(function(){
    //JIRA: NE-2984 - un-block editing when page loading is done
    $($scope.progressbar.getDomElement()).fadeOut(2000, function() {
        $($scope.progressbar.getDomElement()).removeClass('block-editing');
    });
    $scope.progressbar.complete();
}, 3000);
查看更多
老娘就宠你
6楼-- · 2019-03-04 23:52

Finally, I was able to achieve the best acceptable performance for Chrome and IE.

Following are the main changes that fixed the problem in the previous code:

  • Do not delete validation rule after applying it. The following code was commented:
    //delete validationList[childID]
  • Use HTML Rules instead of AngularJS 'ng-' rules when possible. For example, use required='required' instead of ng-required='true'.
  • Add the validation rule only if needed, so if it is 'false' not relevant, do not add it.
  • Do not add dynamic error message for 'ng-maxlen'
  • Use validation rules structure with one-layer, which is now seems working better.

Following is the updated code for directive check-if-required:

app.directive('checkIfRequired',  function($compile, $parse, $interpolate, $timeout, $q, BusinessLogic){
    return {
        priority: 100,
        terminal: true,
        restrict: 'A',
        require: '?^form',
        link: function (scope, el, attrs, ngForm) {
            var saveIsValidationRequired;
            var mainElmID = $interpolate(el[0].id)(scope);
            var resolvedPromise;
            var getChildren = function(el, doInterpolate) {
                var resultChildren;
                doInterpolate = (doInterpolate===undefined)?true:doInterpolate;
                //Return list of elements which were not compiled before 'compiled === undefined'
                resultChildren = $(':input', el);
                //Use code below just in case we want to extract the elements which are not compiled.
                //Use $interpolate to get the final result for each ID...
                if (doInterpolate) {
                    for (var i=0; i < resultChildren.length; i++) {
                        if (resultChildren[i].id) {
                            resultChildren[i].id = $interpolate(resultChildren[i].id)(scope);
                        }
                    }
                }
                //resultChildren = resultChildren.filter("[id!='']");
                return resultChildren;
            }
            //User resolvedPromise when no such promise is available.
            resolvedPromise = $q.when('resolved');
            //Code improvement to make this directive more general
            //      Since this directive can be used from within an isolated scope directive such as 'photo-list-upload', then
            //      additional parameters are required to make it work properly.
            //      Make sure all required functions are defined or report warning.
            //      If the function is not defined within 'scope' it will be looked up from within 'BusinessLogic.getScope()'.
            //      If not found at all, default is used, and warning is reported.
            scope.getIsValidationRequired = scope.getIsValidationRequired || 
                                       (BusinessLogic.getScope().getIsValidationRequired) || 
                                       (console.warn("Directive 'apply-validation' element '%s' - function 'scope.getIsValidationRequired()' is not defined. It will always be false.", mainElmID), 
                                         function () {
                                           return false;
                                         }     
                                       );
            //The promise 'requiredFieldPromise' is used to retrieve list of validation rules from DB
            //Break Point Condition: scope.listData.photosFormName == "subjectPhotos"
            scope.stopExecValidations = scope.stopExecValidations || BusinessLogic.getScope().stopExecValidations || 
                        (console.warn("Directive 'apply-validation' element '%s' - function 'scope.stopExecValidations()' is not defined. Dummy function is used instead.", mainElmID),
                          function () {
                            //Dummy
                          }
                        );
            scope.requiredFieldsPromise = 
                scope.requiredFieldsPromise || (BusinessLogic.getScope().requiredFieldsPromise) || 
                (console.warn("Directive 'apply-validation' element '%s' - function 'scope.requiredFieldsPromise' is not defined. Resolved promise will be used.", mainElmID),
                 resolvedPromise);
            //If needed, stop validation while adding required attribute
            //Save current flag value
            saveIsValidationRequired = scope.getIsValidationRequired();
            scope.stopExecValidations();
            //remove the attribute `check-if-required` to avoid recursive calls
            el.removeAttr('check-if-required');         
            //Define function to add validation message using $watch
            //                As soon as an error is detected, then 'title' will be set to the error
            //                Parameters:
            //                  - ngForm: Angualr Form 
            //                  - elm: The HTML element being validated
            //                  - errAttr: the name of the error attribute of the field within ngForm:
            //                      ngFormName.FieldName.$error.errAttributeName
            //                  - errMsg: The error message to be added to the title 
            //                  - msgVar1: optional substitution variable for the error message
            var addValidationMessage = function (ngForm, elm, errAttr, errMsg, msgVar1, elmScope, elmModel) {
                //Use $timeout to ensure validation rules are added and compiled and that the 'elmModel' is available.
                //After compile is done then will start watching errors
                $timeout(function(){
                    var ngModelName="";
                    //Get the name of the 'ng-model' of the element being validated
                    elmScope = elmScope || scope;
                    elmModel = elmModel || angular.element(elm).controller('ngModel');
                    if (elmModel && elmModel.$name) {
                        ngModelName = elmModel.$name;
                    }
                    if (!ngModelName) {
                        ngModelName = angular.element(elm).attr('ng-model');
                    }
                    if (ngModelName) {
                        elmScope.$watch(ngForm.$name + '.' + ngModelName + '.$error.' + errAttr,
                            function (newValue, oldValue){
                                //console.log("elm.id =", elm.id);
                                //The validation error message will be placed on the element 'title' attribute which will be the field 'tooltip'.
                                //newValue == true means there is error
                                if (newValue) {
                                    var msgVar1Val;
                                    //Perform variable substitution if required to get the final text of the error message.
                                    if (msgVar1) {
                                        msgVar1Val = elmScope.$eval(angular.element(elm).attr(msgVar1));
                                        errMsg = errMsg.format(msgVar1Val);
                                    }
                                    //Append the error to the title if neeeded
                                    if (elm.title) {
                                        elm.title += " ";
                                    } else {
                                        elm.title = "";
                                    }
                                    elm.title += errMsg;
                                } else {
                                    //Remove the error if valid.
                                    //child.removeAttribute('title');
                                    if (elm.title) {
                                        //Replace the error message with blank.
                                        elm.title = elm.title.replace(errMsg, "").trim();
                                    }
                                }
                            });
                    } else {
                        //console.warn("Warning in addValidationMessage() for element ID '%s' in ngForm '%s'. Message: 'ng-model' is not defined.", elm.id, ngForm.$name)
                    }
                }, 1000);       
            }
            //Refactor - apply validation rule for a given element with `childID`
            function applyValidationElement(childID, child, childElm, elmScope, elmModel, validationList) {
                //Validation rule for 'childID': related element was found, and now will apply validation rule.
                var validObjects;
                var errMsg;
                validObjects = validationList[childID];
                //Check if the element is not in "Required" list, and it has an expression to control requried, then
                //... add the attribute 'ng-required' with the expression specified to the element and compile.
                //No longer use `check-if-required-expr`, must report error if used.
                if (!angular.element(child).prop('required') && child.attributes.hasOwnProperty("check-if-required-expr")) {
                    console.error("Unexpected use for attribute 'check-if-required-expr' in directive 'apply-validation' for element ID '%s'. Will be ignored.", childID);
                }
                if (validObjects === "") {
                    //This means the field is required
                    angular.element(child).attr('required', 'required');
                }
                else if (angular.isArray(validObjects)) {
                    //This means that there is a list of validation rules
                    for (var idx=0; idx < validObjects.length; idx++) {
                        var validObject = validObjects[idx];
                        var test = validObject.test || "true"; //if not exist, it means the rule should always be applied
                        var minLenExp = validObject.minlen;
                        var maxLenExp = validObject.maxlen;
                        var isRequiredExp = validObject.required || "false";
                        var readonlyExp = (validObject.readonly || "").toString().trim();
                        var pattern = validObject.pattern || "";
                        var isCAPostalCode = (validObject.isCAPostalCode || "false").toString().trim();
                        isRequiredExp = (angular.isString(isRequiredExp)?isRequiredExp:isRequiredExp.toString()).trim();
                        if (test) {
                            var testEval = scope.$eval(test, elmScope);
                            if (testEval) {
                                if (minLenExp) {
                                    angular.element(child).attr('ng-minlength', minLenExp);
                                }
                                if (maxLenExp) {
                                    angular.element(child).attr('maxlength', maxLenExp);
                                    //For now, to improve performance, do not add validation message - if length exceeds the max
                                    //errMsg = "Number of characters entered should not exceed '{0}' characters.";
                                    //addValidationMessage(ngForm, child, 'maxlength', errMsg, 'ng-maxlength', elmScope, elmModel);
                                }

                                //Add attributes only if needed
                                if (isRequiredExp.toLowerCase() === "true") {
                                    angular.element(child).attr('required', 'required');
                                } else
                                //If the "required" expression is '*skip*' then simply skip.
                                //If '*skip*' is used, this means the required validation is already defined in code
                                //and no need to replace it.
                                if (isRequiredExp && isRequiredExp.toLowerCase() !== "false" && isRequiredExp !== '*skip*') {
                                    angular.element(child).attr('ng-required', isRequiredExp);
                                }

                                if (readonlyExp.toLowerCase() === "true" && readonlyExp != '*skip*') {
                                    angular.element(child).attr('readonly', 'readonly');
                                } else
                                //Add readonly validation
                                if (readonlyExp && readonlyExp.toLowerCase() !== "false" && readonlyExp != '*skip*') {
                                    angular.element(child).attr('ng-readonly', readonlyExp);
                                }
                                if (pattern) {
                                    angular.element(child).attr('ng-pattern', pattern);
                                }
                                if (isCAPostalCode.toLowerCase() === "true") {
                                    angular.element(child).attr('ng-pattern', "/^([A-Z]\\d[A-Z] *\\d[A-Z]\\d)$/i");
                                    //Add validation message if postal code does not match the RegEx
                                    addValidationMessage(ngForm, child, 'pattern', "Invalid postal code.", null, elmScope, elmModel);
                                }
                                //TODO: delete the validation rule after it is implemented to improve performance
                                //      verify if deleting the key is OK and will not distroy the for-loop index
                                //delete validationList[childID]
                                //TODO: Apply only the first matching validation rule
                                //      May required further analysis if more that one rule will be added.
                                break;
                            }
                        }
                    }
                }
            }
            //Check of trim for Field ID is done - not needed if field ID is already timmed in `validationList`.
            //BusinessLogic.getScope().mainVM.validFieldIDTrimDone = BusinessLogic.getScope().mainVM.validFieldIDTrimDone || false;
            //var validFieldIDTrimDone;
            function doApplyValidation(scope, el, attrs, ngForm) {
                var children;
                var fieldValidList;
                var validationStructOpt;
                var mainElmID;
                children = getChildren(el, true); //Do run interpolation of elements IDs
                //children = resultChildren = $(':input', el); //getChildren(el, false);  //Do not run interpolation of elements IDs
                mainElmID = $interpolate(el[0].id)(scope);
                validationList=formView.getRequiredField();
                //Get 'validationStructOpt' option:
                //      Option = 'onelayer' means there is no 'subform' layer
                //      Option = 'twolayers' means there is a 'subform' layer which is the default
                validationStructOpt = validationList.$structureOpt || 'twolayers';
                if (validationStructOpt === 'onelayer') {
                    //for (var fldIdx=0; fldIdx < Object.keys(validationList).length; fldIdx++) {
                    //console.log("One layer. Number of rules: ", Object.keys(validationList).length)
                    for (var fldIdx=0; fldIdx < children.length; fldIdx++) {
                        var childElm;
                        var child;
                        var childID;
                        var validObjects;
                        var elmScope;
                        var elmModel;
                        //childID = Object.keys(validationList)[fldIdx];
                        //if (childID.startsWith('$')) {
                        //  continue;
                        //}
                        //childElm = children.filter('#'+childID);
                        //child = childElm.get(0);
                        childElm = children.eq(fldIdx);
                        child = childElm.get(0);
                        child.id = $interpolate(child.id)(scope);
                        childID = child.id;
                        //if (childElm.length) {
                        if (childID && (childID in validationList)) {
                            //Validation rule for 'childID': related element was found, and now will apply validation rule.
                            validObjects = validationList[childID];
                            elmScope = angular.element(child).scope() || scope;
                            elmModel = angular.element(child).controller('ngModel');
                            applyValidationElement(childID, child, childElm, elmScope, elmModel, validationList);
                        }
                    }
                } else 
                if (validationStructOpt === 'twolayers') {
                    //angular.forEach(Object.keys(validationList), function(keySubform, subformIdx){
                    for (var subformIdx=0; subformIdx < Object.keys(validationList).length; subformIdx++) {
                        var keySubform = Object.keys(validationList)[subformIdx];
                        if (!keySubform.startsWith('$')) {
                            var subform = validationList[keySubform];
                            for (var childIdx=0; childIdx < Object.keys(subform).length; childIdx++) {
                                var childID = Object.keys(subform)[childIdx];
                                var validObjects;
                                var childElm;
                                var child;
                                var elmScope;
                                var elmModel;
                                //console.log(subform, validObjects, childID);
                                //Find the element with id = childID within the 'el' section.
                                //childElm = $('#'+childID, el);
                                //Use 'getChildren()' since the result list has ID values which are interpolated.
                                childElm = children.filter('#'+childID);
                                //console.log(el[0].id, childID);
                                if (childElm.length) {
                                    //Validation rule for 'childID': related element was found, and now will apply validation rule.
                                    validObjects = subform[childID];
                                    child = childElm.get(0);
                                    elmScope = angular.element(child).scope() || scope;
                                    elmModel = angular.element(child).controller('ngModel');
                                    applyValidationElement(childID, child, childElm, elmScope, elmModel, validationList[keySubform]);
                                }
                            }
                        }
                    } //Object.keys(validationList).length
                    //});
                }
                //After done processing all elements under 'el', compile the parent element 'el'.
                $compile(el, null, 100)(scope);
                //If saved flag value is true, enable back validation
                if (saveIsValidationRequired) {
                    scope.startExecValidations();
                }
            }

            function applyValidationTimeout() {
                //Execute 'doApplyValidation()' in the next cycle, to ensure the child elements have been rendered.
                $timeout(function(){
                    //console.log('applyValidationTimeout', mainElmID);
                    doApplyValidation(scope, el, attrs, ngForm);                    
                }, 100)
            }
            scope.requiredFieldsPromise.then(function(success) {
                //Apply validation when the Required Fields and Validation Rules have been loaded.
                applyValidationTimeout();
            }, function(prmError){
                console.warn("Error occured in 'apply-validation' directive while retrieving 'requiredFieldsPromise' for element '%s': %s", mainElmID, prmError);
            });
        }
    }
});

Following are the performance results for loading 1000+ fields and validation rules:

  • IE11 without loading validation rules: 35 to 40 seconds
  • IE11 with validation rules (one layer): 50 to 60 seconds
  • Chrome without loading validation rules: 7-9 seconds
  • Chrome with validation rules (one layer): 9-11 seconds
查看更多
登录 后发表回答