如果无效,则需要对具有字段突出显示的字段组进行角度动态验证

Posted

技术标签:

【中文标题】如果无效,则需要对具有字段突出显示的字段组进行角度动态验证【英文标题】:Angular dynamic required validation of group of fields with field highlight if invalid 【发布时间】:2016-10-14 22:33:22 【问题描述】:

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

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

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

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

终于在这里找到了一个可用的 jsfiddle 版本(更新):

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

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

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

函数isFieldRequired()会检查传入的字段名是否在列表中找到,返回true。

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

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

该指令需要修改如下:

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

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

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

风格

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

HTML - 导航标签:

<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 属性。

完成:从传递给链接函数function (scope, el, attrs, ngModel) 的给定角度元素el 循环遍历子输入元素。

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

完成:使用 promise 确保在执行 Angular 代码之前完成所有 ajax DB 调用和客户端更新。

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

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

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

塔雷克

【问题讨论】:

我很懒,所以我个人倾向于只向表单或字段集添加一个指令,而不是向每个输入添加一个指令,并通过字段名称与名称属性查询元素,并根据需要设置为适用.然后在完成后调用 $apply() 一次,不需要使用$compile 非常感谢@charlietfl!我想我明白你的意思,但是,我仍然需要更多帮助。我会相应地更新问题。 【参考方案1】:

以下代码将满足主要要求,此外,对于div 块下的每个元素,它将允许添加属性check-if-required-expr。这个新属性可用于调用范围布尔表达式来决定required 属性,以防在必填字段列表中找不到该字段。

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

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

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();
                    
                );
            );
            //)
        
    ;
]);

【讨论】:

以上是关于如果无效,则需要对具有字段突出显示的字段组进行角度动态验证的主要内容,如果未能解决你的问题,请参考以下文章

jQuery Validation 突出显示两个字段的一个标签

获取表单角度的字段

角度材料 - mat-error 不显示输入字段的错误消息

数据库:如果任何行中的字段 X 具有值 Y,则排除“分组依据”组

如果发现无效的表单字段,则停止 ladda spinner

验证和突出显示表单字段