如果无效

时间:2016-10-14 22:33:23

标签: angularjs validation dynamic required subform

更新1:根据评论的反馈,问题将得到加强 更新2:已取得一些进展。需要额外的帮助才能通过。请阅读以下内容 更新3:使用$compile(el[0])(scope);编译元素时,提供的示例代码中的错误修复导致表行重复。

在页面加载时,将从数据库中检索字段名称列表,该列表指示使用ajax调用getRequiredFieldInfo()需要哪些字段。在执行指令check-if-required下的相关角度代码之前,必须成功完成此调用才能操作required属性。该指令必须遍历所有输入字段,并根据从数据库中检索的列表将其标记为需要。

我做了一些研究,发现这篇文章似乎最符合我的要求:

https://stackoverflow.com/a/28207652/4180447

最后在这里找到了一个有效的jsfiddle版本(更新):

http://jsfiddle.net/tarekahf/d50tr99u/

我可以使用以下简单方法:

<input name="firstName" type="text" foo ng-required="isFieldRequired('firstName')" />

函数isFieldRequired()将检查列表中是否找到传递的字段名称,它将返回true。

这种方法的问题是我必须将此功能添加到可能需要的每个字段。

此外,每次都必须传递字段名称。为了提高效率,我将不得不在父元素divfieldset上使用一个指令,它允许我访问所有子元素,并处理所有输入元素所需的属性。 / p>

此指令需要更改如下:

  • 要添加到将在需要时处理和修改required属性的字段组的父元素。

  • 将元素名称与要设置的字段列表进行比较,并相应地应用更改。

更新的代码(因为我正在研究解决方案):

风格

input.ng-invalid, li.ng-invalid {
    background:#F84072;
    border: 2px red solid;
}

HTML - NAVIGATION TABS:

<ul  class="nav nav-pills">
    <li ng-class="{'ng-invalid':mainForm.homeForm.$invalid && mainPromiseResolved}" class="active"><a data-toggle="pill" href="#home"><%=homeTabName%></a></li>
    <li ng-class="{'ng-invalid':mainForm.clientForm.$invalid && mainPromiseResolved}"><a data-toggle="pill" href="#menu1"><%=clientTabName%></a></li>
    <li ng-class="{'ng-invalid':mainForm.appraiserForm.$invalid && mainPromiseResolved}"> <a data-toggle="pill" href="#menu2"><%=appraiserTabName%></a></li>
    <li ng-class="{'ng-invalid':mainForm.propertyForm.$invalid && mainPromiseResolved}"><a data-toggle="pill" href="#menu3"><%=propertyTabName%></a></li>
    <li ng-class="{'ng-invalid':mainForm.serviceForm.$invalid && mainPromiseResolved}"><a data-toggle="pill" href="#menu4"><%=servicesTabName%></a></li>
    <li ng-class="{'ng-invalid':mainForm.constructionStage.$invalid && mainPromiseResolved}"><a data-toggle="pill" href="#menu5"><%=constructionStageTabName%></a></li>    
    <li ng-class="{'ng-invalid':mainForm.costForm.$invalid && mainPromiseResolved}"><a data-toggle="pill" href="#menu6"><%=costTabName%></a></li>  
    <li ng-class="{'ng-invalid':mainForm.certificationForm.$invalid && mainPromiseResolved}" ng-click="redrawCanvas()"><a data-toggle="pill" href="#menu7"><%=certificationTabName%></a></li>
    <li ng-class="{'ng-invalid':mainForm.photosForm.$invalid && mainPromiseResolved}"><a data-toggle="pill" href="#menu8"><%=photoTabName%></a></li>
    <li ng-class="{'ng-invalid':mainForm.mapForm.$invalid && mainPromiseResolved}"><a data-toggle="pill" href="#menu9"><%=locationTabName%></a></li>    
</ul>

HTML - 表单

<div id="menu2" class="tab-pane fade" ng-form="appraiserForm">
    <fieldset ng-disabled="isAppraiserSigned()" check-if-required>
        <input type="text" id="appraiser_name" name="appraiser_name" ng-model="sigRoles.appraiser.roleNameModel" style="width: 536px; ">
        <input type="text" id="appraiser_company" style="width: 536px; ">
        ... 
        ... 
    </fieldset>
</div>

的javascrip:

app.controller('formMainController', ['$scope', '$timeout', '$q', function($scope, $timeout, $q) {

    $scope.runProcessAndInit = function () {
        var q = $q.defer();     //Create a promise controller
        angular.element(document).ready(function(){
            //perform all client updates here
            q.resolve('success');   //notify execution is completed successfully - inside document 'ready' event.
        })
    return q.promise;   //return the promise object.
    }
    //mainPromiseResolved is used to indicate all ajax calls and client updates are done.
    $scope.mainPromiseResolved = false;
    $scope.mainPromise = $scope.runProcessAndInit();
    $scope.mainPromise.then(function(success) {
        //debugger;
        $scope.$broadcast('event:force-model-update');
        //mainPromiseResolved is mainly used in angular validation to prevent showing errors until all client updates are done.
        $scope.mainPromiseResolved = true;
        return 'main promise done';
    })
    $scope.isFieldRequired = function (prmFieldName) {
        var isFound = false;
        var oRequiredField = formView.getRequiredField();
        findField: {
            for(var subformName in oRequiredField) {
                isFound = prmFieldName in oRequiredField[subformName];
                if (isFound) {
                    break findField;
                }
            }
        }
        return isFound;
    }    
    function getRequiredFieldInfo() {
        var q = $q.defer();
        var appUrl = getAppURL();   
        $.get(appUrl + "/servlet/..."
                    + "&timestamp="     + new Date().getTime(), 
                    function(data, status){
            //console.log("json fields:" + data);           
            var obj = JSON.parse(data);
            formView.setRequiredField(obj);
            q.resolve('success');
            // console.log(JSON.stringify(formView.getRequiredField()));            
        });
        return q.promise;
    }
    $scope.requiredFieldsPromise = getRequiredFieldInfo(); 
}]);

app.directive('checkIfRequired', ['$compile', function ($compile) {
    return {
        require: '?ngModel',
        link: function (scope, el, attrs, ngModel) {
            if (!ngModel) {
                //return;
            }
            //debugger;
            var children = $(":input", el);
            angular.element(document).ready(function (){
                scope.requiredFieldsPromise.then(function(success) {
                    //remove the attribute to avoid recursive calls
                    el.removeAttr('check-if-required');
                    //Comment line below as it caused duplication in table raws, and I don't know why.
                    //$compile(el[0])(scope);
                    angular.forEach(children, function(value, key) {
                        //debugger;
                        if (scope.isFieldRequired(value.id)) {
                            angular.element(value).attr('required', true);
                            //el.removeAttr('check-if-required');
                            $compile(value)(scope);
                        }
                    });
                })
            })
        }
    };
}]); 

我已经取得了一些进展。但是,我还需要更多的帮助。以下是状态:

  • 完成:从DB获取必填字段列表,然后执行指令中的代码以操纵required属性。
  • 完成:循环来自给定角元素el的子输入元素,该元素传递给链接函数function (scope, el, attrs, ngModel)

  • 完成:如果required为真,则为每个子元素添加isFieldRequired(fieldName)属性?

  • 完成:使用promise确保在执行角度代码之前完成所有ajax数据库调用和客户端更新。

  • 如果子元素嵌套在另一个ng-form子表单或div元素中,如何递归循环子元素?

  • 如何确保每个元素都有ngModel对象?

  • 如何将指令限制为divfieldsset或类似元素?

塔雷克

1 个答案:

答案 0 :(得分:0)

以下代码将满足主要要求,此外,对于div块下的每个元素,它将允许添加属性check-if-required-expr。如果未找到字段所需字段,则可以使用此新属性调用范围布尔表达式来确定required属性。

我想知道是否有办法使用标准ng-required指令而不是自定义属性check-if-required-expr,它基本上与ng-required相同。如果我使用ng-required,唯一的问题是如果在列表中指定了它,它可能会覆盖必填字段的逻辑。

这里的问题是:有没有办法找出是否设置了required属性,如果是,则不检查所需的表达式,否则,执行ng-required表达式。< / p>

HTML

<div id='signature-pad' class="m-signature-pad break" ng-class="{'ng-invalid':certificationForm[theRoleData.signatureBase64].$invalid && mainPromiseResolved}" check-if-required>
...
    <div class="m-signature-pad--body">
        <canvas id="appraiser_signature_section" redraw ng-signature-pad="signature" ng-hide="isSigned()">
        </canvas>
        <img ng-src="{{signatureDataURL()}}" ng-hide="!isSigned()" load-signature-image>
        <input id="{{theRoleData.signatureBase64}}" name="{{theRoleData.signatureBase64}}" type="text" ng-hide="true" ng-model="signatureBase64" check-if-required-expr="sigDetailsAvail(theRoleData)" force-model-update/>
    </div>
...

</div>

基本上,在上面的HTML中,input字段check-if-required-expr表示如果在list of required fields中找不到此字段,则执行表达式以确定字段是否为必需的。

的JavaScript

//Define directive check-if-required
//This directive will loop over all child input elements and add the required attributes if needed
app.directive('checkIfRequired', ['$compile', '$timeout', '$parse', function ($compile, $timeout, $parse) {
    return {
        /*require: '?ngModel',*/
        require: '?^form',
        link: function (scope, el, attrs, ngForm) {
            /*if (!ngModel) {
                return;
            }*/
            var saveIsValidationRequired;
            var children;
            saveIsValidationRequired = scope.isValidationRequired;  //Save current flag value
            scope.stopExecValidations();
            el.removeAttr('check-if-required');
            $timeout(function() {
                //Get all input elements of the descendants of `el` 
                children = $(":input", el);
                //Run the following as early as possible but just wait (using promise) until 
                //  the list of required fields is retrieved from Database
                //scope.requiredFieldsPromise.then(function(success) {
                scope.requiredFieldsPromise.then(function(success) {
                    //The line below caused duplication of the table in construction stage, so it is removed and no impact
                    //$compile(el[0])(scope);
                    angular.forEach(children, function(child, key) {
                        var elmScope;
                        var elmModel;
                        try {
                            if(child && child.id) {
                                elmScope = angular.element(child).scope() || scope;
                                elmModel = angular.element(child).controller('ngModel');
                                if (ngForm && elmModel && ngForm[elmModel.$name]) {
                                    scope.$watch(function(){
                                        //Watch the errors for the defined field - convert to JSON string.
                                        return JSON.stringify(ngForm[elmModel.$name].$error);
                                    }, function (newValue, oldValue){
                                        //The validation error message will be placed on the element 'title' attribute which will be the field 'tooltip'. 
                                        var maxlength;
                                        var minlength;
                                        if (angular.isDefined(newValue)) {
                                            if (ngForm[elmModel.$name].$error.maxlength) {
                                                //If invalid, add the error message if number of entered characters is more than the defined maximum
                                                maxlength = scope.$eval(angular.element(child).attr('ng-maxlength'));
                                                child.title = ("Number of characters entered should not exceed '{0}' characters.").format(maxlength);
                                            } else {
                                                //Remove the error if valid.
                                                child.removeAttribute('title');
                                            }
                                        }
                                    });
                                }
                                if (scope.isFieldRequired(child.id)) {
                                    angular.element(child).attr('ng-required', "true");
                                    $compile(child)(elmScope);
                                }
                                //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")) {
                                    var isRequiredExpr = child.attributes["check-if-required-expr"].child;
                                    angular.element(child).attr('ng-required', isRequiredExpr);
                                    $compile(child)(elmScope);
                                }
                                var validObjects = scope.getFieldValidation(child.id);
                                if (angular.isArray(validObjects)) {
                                    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;
                                        isRequiredExp = angular.isString(isRequiredExp)?isRequiredExp:isRequiredExp.toString();
                                        //scope.$evalAsync(function(){
                                            if (test && (minLenExp || maxLenExp || isRequiredExp)) {
                                                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);
                                                    }
                                                    //Change how '$compile()' is used.
                                                    //      After reserach, found there is bug in Angular which is causing the fillowing issues when using '$compile()':
                                                    //      1. Duplicate values for drop-down list items.
                                                    //      2. Inteference with dateppciker Angular UI Bootstrap control
                                                    //      If this still happes, more research is needed to resolve this problem.
                                                    //      This is still work-in-progress. More research is needed.
                                                    //The compile statement below will be replaced ...
                                                    $compile(child)(elmScope, function (clone) {
                                                        angular.element(child).after(clone);     
                                                        angular.element(child).remove();
                                                    });
                                                    //Apply only the first matching validation rule
                                                    break;
                                                }
                                            }
                                    }
                                }
                            }
                        } catch (e) {
                            console.error("Error occuured in 'checkIfRequired' directive while applying validation logic on element ID '%s'. Error is: '%s'", child.id, e);
                        }
                    });
                    //If saved flag value is ture, enable validation
                    if (saveIsValidationRequired) {
                        scope.startExecValidations();
                    }
                });
            });
            //})
        }
    };
}]);