Slow loading of AngularJS app in IE - add progress bar

前端 未结 5 1190
谎友^
谎友^ 2021-01-27 08:08

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


The AngularJS application ha

相关标签:
5条回答
  • 2021-01-27 08:47

    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>
    
    0 讨论(0)
  • 2021-01-27 08:52

    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}}
    0 讨论(0)
  • 2021-01-27 08:52

    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);
    
    0 讨论(0)
  • 2021-01-27 08:59

    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:

    • IE11 : 1:30 (MM:SS)
    • Chrome: 00:20

    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

    0 讨论(0)
  • 2021-01-27 09:02

    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
    0 讨论(0)
提交回复
热议问题