The Problem
Angular
does support the concept of conditional required through the built-in directive ng-required
. And it works great in simple scenarios. Consider the following example where address2
is only required when address1
is not empty.
Address1 : <input name="address1" type="text" ng-model="address1" />
Address2 : <input name="address2" type="text" ng-model="address2" ng-required="!address1" />
This can be done using ng-required
as shown above. But what if we also want address2
to be 'not required' when address1
is empty? There's no such thing as ng-not-required
.
If we take a look at the source code of ng-required
, we can see that it is considered to be as valid only when the ngModel
is NOT empty:
var requiredDirective = function() {
return {
restrict: 'A',
require: '?ngModel',
link: function(scope, elm, attr, ctrl) {
if (!ctrl) return;
attr.required = true;
ctrl.$validators.required = function(modelValue, viewValue) {
return !attr.required || !ctrl.$isEmpty(viewValue);
};
attr.$observe('required', function() {
ctrl.$validate();
});
}
};
};
There's nothing wrong with ng-required
being implemented this way, as its name implies, it is used for adding required
restriction/validation on a form element conditionally.
But it does become limited or useless when we have more complex problems to solve. For example:
-
Conditional not required is not supported as we have already talked about above.
-
Support for requireness of one or more elements in a group is very limited.
The Solution
Create not-required
directive
We can fix problem #1 with a custom directive that is slightly modified from ng-required
, we call demo-not-required
:
app.directive('demoNotRequired', function($parse) {
return {
restrict: 'A',
require: '?ngModel',
link: function(scope, elm, attr, ctrl) {
if (!ctrl) return;
scope.$watch(attr.demoNotRequired, function() {
ctrl.$validate();
})
ctrl.$validators.notRequired = function(modelValue, viewValue) {
var isNotRequired = $parse(attr.demoNotRequired)(scope);
return !isNotRequired || ctrl.$isEmpty(viewValue);
};
}
};
});
As you can see, as opposite to ng-required
, demo-not-required
considers a form element to be valid when its ngModel IS empty rather than not.
Requireness of one or more elements in a group
With ng-required
and demo-not-required
in hand as building blocks, we can solve more complex problems.
At Least One Required
This is one of the common requirements in form validations. Among multiple form elements, at least one is required. In other words, it is ok to have more than one elements being non-empty, but it is not ok to all the elements being empty.
For example, we may want 'phone number' and 'email' to be 'at least one required'. And this can easily be done using ng-required
:
<input name="phone" type="text" ng-model="phone" ng-required="!email"/>
<input name="email" type="email" ng-model="email" ng-required="!phone"/>
At Most One Required
This is kind of the opposite to 'at least one required'. This is to say that among multiple form elements, at most one is required. In other words, it is fairly ok to have all the elements left blank, but if we were to put values, no more than one element can have values.
For example, some restaurant offers free salad or soup when you order any meal. And you can choose not to have either of them. This relationship can be implemented using demo-not-required
we created above:
<input name="salad" type="text" ng-model="salad" demo-not-required="!!soup" />
<input name="soup" type="text" ng-model="!!salad" />
Only One Required
This relationship is kind of a combination of 'at most one required' and 'at least one required' and can indeed be built as such. Take online payment as an example. Let's say you must leave either your credit card or debit card number as a method of payment:
<input name="credit" type="text" ng-model="credit" ng-required="!debit" demo-not-required="!!debit" />
<input name="debit" type="text" ng-model="debit" ng-required="!credit" demo-not-required="!!credit" />
Above implementation looks ok but rather redundant. Since the logic in this case is a combination of the two, we can create a new directive 'demo-one-required' that combines the logic of the two existing directives:
app.directive('demoOneRequired', function($parse) {
return {
restrict: 'A',
require: '?ngModel',
link: function(scope, elm, attr, ctrl) {
if (!ctrl) return;
scope.$watch(attr.demoOneRequired, function() {
ctrl.$validate();
})
ctrl.$validators.oneRequired = function(modelValue, viewValue) {
var isRequired = $parse(attr.demoOneRequired)(scope);
return isRequired ? !ctrl.$isEmpty(viewValue) : ctrl.$isEmpty(viewValue);
};
}
};
});
through which, the solution becomes clearer:
<input name="credit" type="text" ng-model="credit" demo-one-required="!debit" />
<input name="debit" type="text" ng-model="debit" demo-one-required="!credit" />
Refactoring
At this point, the directives we have built, namely: demo-not-required
, demo-one-required
all look similar. In fact, they only differ in the validation logic. What we can do is to build a general purpose conditional requireness validation directive that support all group and non-group relationship, including all the scenarios we have talked about. And the actual 'rule' that determines the group/non-group relationship is configurable.
For example, let's say this directive we are going to build is called demo-required
:
The controller will look like this:
app.controller('DemoCtrl', function($scope) {
$scope.validationConfig = {
requiredGroup1: {
ruleKey: 'ONLY_ONE',
message: 'Please choose one from Field A, Field B etc.'
}
};
});
The configurable rules:
var rulesConfig = {
ONLY_ONE: function(thatIsEmpty, thisIsEmpty) {
return thatIsEmpty ? thisIsEmpty : !thisIsEmpty;
},
AT_MOST_ONE: function(thatIsEmpty, thisIsEmpty) {
return thatIsEmpty || !thisIsEmpty;
},
AT_LEAST_ONE: function(thatIsEmpty, thisIsEmpty) {
return !thatIsEmpty || thisIsEmpty;
}
};
Then finally in the html, we simply specify the groupId for demo-required
:
<input type="text" name="field1" ng-model="field1" demo-required="requiredGroup1" />
<input type="text" name="field2" ng-model="field2" demo-required="requiredGroup1" />
The code
The directives we have built can be found in plunker.
The final demo-required
is still a work in progress, and can also be found in plunker.