问题
UPDATE 1: the question will be enhanced based on the feedback from the comments.
UPDATE 2: Some progress has been made. Need additinoal help to get through. Please read below.
UPDATE 3: Bug fixing in provided sample code causing duplication in table rows when compiling the element using $compile(el[0])(scope);
.
On page load, a list of field names is retrieved from Database which indicates what fields are required using ajax call getRequiredFieldInfo()
. This call must be completed successfully before executing the related angular code under the directive check-if-required
to manipulate the required
attribute. This directive must loop over all input fields and mark them as required based on the list which is retrieved from the Database.
I did some research and found this post which seems to be the closest to my requirements:
https://stackoverflow.com/a/28207652/4180447
and finally found a working jsfiddle version here (updated):
http://jsfiddle.net/tarekahf/d50tr99u/
I can use the following simple approach:
<input name="firstName" type="text" foo ng-required="isFieldRequired('firstName')" />
The function isFieldRequired()
will check if the passed field name is found in the list, it will return true.
The problem with this approach is that I have to add this function to each and every field which might be required.
Also, will have to pass the field name each time. To be more efficient, I will have to use a directive on the parent element div
or fieldset
which will allow me to access all child elements, and process the required attributes for the all the input elements.
This directive need to be changed as follows:
To be added to the parent element of the group of fields whose
required
attribute will be processed and modified if needed.Compare the element name against the list of fields to set as required and apply the change accordingly.
The updated code (as I am researching the solution):
STYLE
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 - Form
<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/..."
+ "×tamp=" + 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);
}
});
})
})
}
};
}]);
I've already made some progress. However, I still need more help. Following is the status:
- Done: get required fields list from DB and then execute code in directive to manipulate the
required
attribute. Done: Loop over the child input elements from a given angular element
el
which is passed to link functionfunction (scope, el, attrs, ngModel)
.Done: Add
required
attribute to each child element ifisFieldRequired(fieldName)
is true?Done: Use promise to ensure all ajax DB calls and client updates are done before executing angular code.
How to recursively loop over the child elements if they are nested inside another
ng-form
subform ordiv
element?How to ensure that each element has ngModel object?
How to restrict the directive to
div
,fieldsset
or similar elements?
Tarek
回答1:
The following code will satisfy the main requirement, in addition, for each element under the div
block, it will allow adding attribute check-if-required-expr
. This new attribute may be used to call a scope boolean expression to decide the required
attribute in case the field is not found the list of required fields.
I was wondering if there is a way to use the standard ng-required
directive instead of the custom attribute check-if-required-expr
which basically does the same as ng-required
. The only problem if I use ng-required
is that it might override the logic of the required field if it was specified in the list.
So the question here: is there a way to find out if the required
attribute is set, and if yes, then do not check the required expression, otherwise, execute the ng-required
expression.
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>
Basically, in the above HTML, the input
field which has check-if-required-expr
, indicates that if this field is not found in the list of required fields
then execute the expression to decide if the field is required.
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();
}
});
});
//})
}
};
}]);
来源:https://stackoverflow.com/questions/40052902/angular-dynamic-required-validation-of-group-of-fields-with-field-highlight-if-i