There are various strategies available, and the most suitable one depends on your specific needs.
One approach that stands out for its versatility and adaptability is as follows:
By "versatile", it means it can be applied not just to text fields but also to other types of inputs like checkboxes.
It's considered "flexible" due to its ability to accommodate any number of control groups where at least one control in each group must have a value. Moreover, there are no spatial restrictions - the controls in each group can be placed anywhere within the DOM (if necessary, they can be confined within a single form
).
This particular method involves creating a custom directive (requiredAny
) similar to ngRequired
, but tailored to consider the other controls within the same group. Once defined, the directive can be utilized in this manner:
<form name="myForm" ...>
<input name="inp1" ng-model="..." required-any="group1" />
<input name="inp2" ng-model="..." required-any="group1" />
<input name="inp3" ng-model="..." required-any="group1" />
<input name="inp4" ng-model="..." required-any="group2" />
<input name="inp5" ng-model="..." required-any="group2" />
</form>
In the above example, at least one of [inp1, inp2, inp3] must have a value since they belong to group1
.
Similarly, for [inp4, inp5] belonging to group2
.
The structure of the directive is outlined below:
app.directive('requiredAny', function () {
// Map for keeping track of each group's status.
var groups = {};
// Utility function: Determines if at least one control
// within the group has a value.
function determineIfRequired(groupName) {
var group = groups[groupName];
if (!group) return false;
var keys = Object.keys(group);
return keys.every(function (key) {
return (key === 'isRequired') || !group[key];
});
}
return {
restrict: 'A',
require: '?ngModel',
scope: {}, // An isolated scope is used for convenience
// in $watching and cleanup tasks (on destruction).
link: function postLink(scope, elem, attrs, modelCtrl) {
// If `ngModel` is not present or no group name is specified,
// further actions cannot be taken.
if (!modelCtrl || !attrs.requiredAny) return;
// Access the group's status object.
// Initialize if not yet created.
var groupName = attrs.requiredAny;
if (groups[groupName] === undefined) {
groups[groupName] = {isRequired: true};
}
var group = scope.group = groups[groupName];
// Cleanup logic when the element is removed.
scope.$on('$destroy', function () {
delete(group[scope.$id]);
if (Object.keys(group).length <= 1) {
delete(groups[groupName]);
}
});
// Update validity based on the group's status.
function updateValidity() {
if (group.isRequired) {
modelCtrl.$setValidity('required', false);
} else {
modelCtrl.$setValidity('required', true);
}
}
// Update group state and control validity.
function validate(value) {
group[scope.$id] = !modelCtrl.$isEmpty(value);
group.isRequired = determineIfRequired(groupName);
updateValidity();
return group.isRequired ? undefined : value;
}
// Re-validation whenever value changes
// or group's `isRequired` property alters.
modelCtrl.$formatters.push(validate);
modelCtrl.$parsers.unshift(validate);
scope.$watch('group.isRequired', updateValidity);
}
};
});
Although lengthy, once integrated with a module, it becomes simple to incorporate into your forms.
For a demonstration, check out this (not so) brief demo.