Home » Angularjs » Form validation – Required one of many in a group

Form validation – Required one of many in a group

Posted by: admin November 30, 2017 Leave a comment

Questions:

In the project I’m working on at the moment I currently have three textboxes and I need to validate that at least one of the text boxes has been populated.

I’ve been reading into custom validation with Angular directives and I understand you can set the validity of an input in a directive’s link function using the following:

ctrl.$parsers.unshift(function(viewValue) {
  // validation logic here
});

The problem I have is that I don’t need to set an individual input’s validity.. I need to invalidate the entire form if the criteria isn’t met. I just wonder how to approach this?

I’m thinking maybe I should create a directive that’s placed on the enclosing form itself and then make the form invalid?

I suppose I’m just looking for some guidance into how I should go about this because I’m a little unclear where to start – all the material I’m reading on custom validation seems to be for when you’re validating a specific input as opposed to a set of conditions on a form.

I hope I’ve made myself clear! Thanks..

Answers:

You can use ng-required to force the user to fill at least one field by checkingthe length attribute of the string.

You can do the following for example:

<form name="myForm">
            <input type="text" ng-model="fields.one" name="firstField" ng-required="!(fields.one.length || fields.two.length || fields.three.length)" />
            <br/>
            <input type="text" name="secondField" ng-required="!(fields.one.length || fields.two.length || fields.three.length)" ng-model="fields.two" />
            <br/>
            <input type="text" ng-model="fields.three" name="thirdField" ng-required="!(fields.one.length || fields.two.length || fields.three.length)" />
            <br/>
            <button type="submit" ng-disabled="!myForm.$valid">Submit</button>
</form>

See this working fiddle example for more details.

You can have more details about required vs ng-required by reading this question

Questions:
Answers:

There are several approaches and the best option depends on your exact requirements.

Here is one approach that I found to be generic enough and flexible.
By “generic” I mean it doesn’t only work for text-fields, but also for other kinds of inputs, such as check-boxes.
It’s “flexible” because it allows any number of control-groups, such that at least one control of each group must be non-empty. Additionally, there is no “spacial” constraint – the controls of each group can be anywhere inside the DOM (if required, it is easy to constrain them inside a single form).

The approach is based on defining a custom directive (requiredAny), similar to ngRequired, but taking into account the other controls in the same group. Once defined, the directive can be used like this:

<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 be non-empty, because they belong to group1.
The same holds for [inp4, inp5], which belong to group2.


The directive looks like this:

app.directive('requiredAny', function () {
    // Hash for holding the state of each group
    var groups = {};

    // Helper function: Determines if at least one control
    //                  in the group is non-empty
    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 isolate scope is used for easier/cleaner
                     // $watching and cleanup (on destruction)
        link: function postLink(scope, elem, attrs, modelCtrl) {
            // If there is no `ngModel` or no groupName has been specified,
            // then there is nothing we can do
            if (!modelCtrl || !attrs.requiredAny) return;

            // Get a hold on the group's state object
            // (if it doesn't exist, initialize it first)
            var groupName = attrs.requiredAny;
            if (groups[groupName] === undefined) {
                groups[groupName] = {isRequired: true};
            }
            var group = scope.group = groups[groupName];

            // Clean up when the element is removed
            scope.$on('$destroy', function () {
                delete(group[scope.$id]);
                if (Object.keys(group).length <= 1) {
                    delete(groups[groupName]);
                }
            });

            // Updates the validity state for the 'required' error-key
            // based on the group's status
            function updateValidity() {
                if (group.isRequired) {
                    modelCtrl.$setValidity('required', false);
                } else {
                    modelCtrl.$setValidity('required', true);
                }
            }

            // Updates the group's state and this control's validity
            function validate(value) {
                group[scope.$id] = !modelCtrl.$isEmpty(value);
                group.isRequired = determineIfRequired(groupName);
                updateValidity();
                return group.isRequired ? undefined : value;
            };

            // Make sure re-validation takes place whenever:
            //   either the control's value changes
            //   or the group's `isRequired` property changes
            modelCtrl.$formatters.push(validate);
            modelCtrl.$parsers.unshift(validate);
            scope.$watch('group.isRequired', updateValidity);
        }
    };
});


This might not be so short, but once included into a module, it is very easy to integrate into your forms.


See, also, this (not so) short demo.

Questions:
Answers:

It’s too late but might be can save some one’s time:

If there are only two fields, and want to make one of them required then

<input type="text" 
      ng-model="fields.one" 
      ng-required="!fields.two" />
<br/>
<input type="text" 
      ng-model="fields.two"
      ng-required="!fields.one"  />

If you have three like in question then

<input type="text" 
      ng-model="fields.one" 
      ng-required="!(fields.two || fields.three)" />
<br/>
<input type="text" 
      ng-model="fields.two"
      ng-required="!(fields.one || fields.three)"  />
<br/>
<input type="text" 
      ng-model="fields.three" 
      ng-required="!(fields.one|| fields.two)" />

If more than this, I will suggest to write a function on scope and watch it.

See the working example

Questions:
Answers:

modification to ExpertSystem’s answer (https://stackoverflow.com/a/24230876/4968547) so that his code works in the latest angularjs.

i changed the updateValidity() to set parse also to true/false

function updateValidity() {
            if (group.isRequired) {
                modelCtrl.$setValidity('required', false);
                modelCtrl.$setValidity('parse', false); 
            } else {
                modelCtrl.$setValidity('required', true);
                modelCtrl.$setValidity('parse', true);
            }
        }

now its working fine for me

Questions:
Answers:

Ran into this same problem last week; ExpertSystem’s solution was a good start, but I was looking for a few enhancements to it:

  • Use Angular 1.4.3
  • Use ngMessages

I eventually wound up with this example on JSFiddle – hope that helps inspire others in the same boat! Relevant JS code from the Fiddle:

var app = angular.module('myApp', ['ngMessages']);
app.controller('myCtrl', function ($scope) {
    $scope.sendMessage = function () {
        $scope.myForm.$submitted = true;

        if ($scope.myForm.$valid) {
            alert('Message sent !');
        }
    };
});

app.directive('requiredAny', function () {
    return {
        restrict: 'A',
        require: 'ngModel',
        link: function postLink(scope, elem, attrs, ctrl) {
            // If there is no 'ngModel' or no groupName has been specified,
            // then there is nothing we can do
            if (!ctrl || !attrs.requiredAny) { return };

            // If this is the first time we've used this directive in this scope,
            // create a section for it's data. If you need / want to make use of
            // an isolate scope you'll need to make 'var groups' scoped to the directive;
            // but then you may want to look in to clearing out group entries yourself
            if (!scope.__requiredAnyGroups) {
                scope.__requiredAnyGroups = {}
            }
            var groups = scope.__requiredAnyGroups;

            // Create a bucket for this group if one does not yet exist
            if (!groups[attrs.requiredAny]) {
                groups[attrs.requiredAny] = {};
            }
            var group = groups[attrs.requiredAny];

            // Create the entry for this control
            group[attrs.ngModel] = {
                ctrl: ctrl,
                hasValue: false
            };

            ctrl.$validators.requiredAny = function(view, value) {
                var thisCtrl = group[attrs.ngModel],
                        ctrlValue = (typeof value !== 'undefined') && value,
                        oneHasValue = false;

                thisCtrl.hasValue = ctrlValue;

                // First determine if any field in the group has a value
                for (var prop in group) {
                    if (group.hasOwnProperty(prop) && group[prop].hasValue) {
                        oneHasValue = true;
                        break;
                    }
                }

                // Set the validity of all other fields based on whether the group has a value
                for (var prop in group) {
                    if (group.hasOwnProperty(prop) && thisCtrl != group[prop]) {
                        group[prop].ctrl.$setValidity('requiredAny', oneHasValue);
                    }
                }

                // Return the validity of this field
                return oneHasValue;
            };
        }
    };
});

Questions:
Answers:

Here is a refactored take on ExpertSystems great post. I didn’t need the destroy method so I gutted it.

I also added a grayed out explanation that may help in your code. I use this directive for ALL my required fields. Meaning when I use this directive I no longer use ng-required, or required.

If you want a field required just pass in a unique group name. If you don’t want the field required then pass in null, and if you want to have many different groups just pass in a matching group name.

I believe there is a little more refactoring that could be done here. Angularjs states that when using $setValidity, that instead you should use $validators pipeline instead, but I could not get that to work. I am still learning this complex animal. If you have more info, post it!

app.directive('rsPartiallyRequired', function () {

 var allinputGroups = {};

 return {
   restrict: 'A',
   require: '?ngModel',
   scope: { },

   link: function(scope, elem, attrs, ctrl) {
     if( !ctrl || !attrs.rsPartiallyRequired ){ return } // no ngModel, or rsPartialRequired is null? then return.

    // Initilaize the following on load
    ctrl.$formatters.push( validateInputGroup ); // From model to view.
    ctrl.$parsers.unshift( validateInputGroup ); // From view to model.

    if ( ! allinputGroups.hasOwnProperty( attrs.rsPartiallyRequired )){ // Create key only once and do not overwrite it.
    allinputGroups[ attrs.rsPartiallyRequired ] = { isRequired: true } // Set new group name value to { isRequired: true }.
  }

    scope.inputGroup = allinputGroups[ attrs.rsPartiallyRequired ] // Pass { isRequired: true } to form scope.

    function validateInputGroup(value) {
    scope.inputGroup[ scope.$id ] = !ctrl.$isEmpty( value ); // Add to inputGroup ex: { isRequired: true, 01E: false }.
    scope.inputGroup.isRequired = setRequired( attrs.rsPartiallyRequired ); // Set to true or false.
    updateValidity(); // Update all needed inputs based on new user input.
    return scope.inputGroup.isRequired ? undefined : value
  }

    function setRequired(groupName) {
      if( ! allinputGroups[ groupName ] ){ return false } // No group name then set required to false.
      return Object.keys( allinputGroups[ groupName ] ).every( function( key ) { // Key is 'isRequired' or input identifier.
      return ( key === 'isRequired' ) || ! allinputGroups[ groupName ][ key ]
    });
  }

    scope.$watch('scope.inputGroup.isRequired', updateValidity); // Watch changes to inputGroup and update as needed.

    function updateValidity() { // Update input state validity when called.
      ctrl.$setValidity('required', scope.inputGroup.isRequired ? false : true );
    } 
  }
 }
});

// This directive sets input required fields for groups or individual inputs.  If an object in the template is given
// to the directive like this: 
// Object: { "name": "account_number", "attrs": { "required": { "group": "two"  }}}.
// HTML: <input type="text" rs-partially-required="{{ field.attrs.required.group }}" />
// Or anything where the evaluation is a string, for example we could use "groupOne" like this...
// HTML: <input type="text" rs-partially-required="groupOne" />
// Then this directive will set that group to required, even if it's the only member of group.  
// If you don't want the field to be required, simply give the directive a null value, like this...
// HTML: <input type="text" rs-partially-required="null" />
// However, when you want to use this directive say in an ngRepeat, then just give it a dynamic string for each input
// and link the inputs together by giving the exact matching string to each group that needs at least one field. ex:

// <input type="text" rs-partially-required="null" />
// <input type="text" rs-partially-required="one" />
// <input type="text" rs-partially-required="two" />
// <input type="text" rs-partially-required="one" />
// <input type="text" rs-partially-required="null" />
// <input type="text" rs-partially-required="three" />
// <input type="text" rs-partially-required="three" />
// <input type="text" rs-partially-required="three" />

// In the above example, the first and fifth input are not required and can be submitted blank.
// The input with group "two" is the only one in the group, so just that input will be required by itself.
// The 2 inputs with "one" will be grouped together and one or the other will require an input before
// the form is valid.  The same will be applied with group "three".
// For this form to be valid, group "two" will be required, and 1 input from group one will be required,  
// and 1 input from group three will be required before this form can be valid.

Questions:
Answers:

You can add required attribute for each of them , and at the end , you can rely your validation on each/all/or just one of them

        <form name="form" novalidate ng-submit="submit()">
        // novalidate is form disabling your browser's own validation mechanism

          <input type="text" required ng-model="texts.text1"> 
          <input type="text" required ng-model="texts.text2"> 
          <input type="text" required ng-model="texts.text3"> 
          // you can do validation in variety of ways , but one of them is to disable your submit button until one of the textboxes are filled correctly like this : 

          <button type="submit" ng-disabled="form.text1.$invalid && form.text2.$invalid && form.text3.$invalid"></button>      

        </form>

This way if just one of them is filled , button will be enable

I don’t know how you’re gonna show that form is not valid , but I think desabling the submit button is the general way

Questions:
Answers:

I had similar grouping requirement in my project and I wrote this.Interested people can use this

.directive('group',function(){
        return {
            require: '^form',
            link : function($scope,element,attrs,formCtrl){
                var ctrls =[];

                element.find(".group-member").each(function(){
                    var member = angular.element($(this));
                    var mdlCtrl = member.data("$ngModelController");
                    if(!mdlCtrl){
                        throw "Group member should have ng-model";
                    }
                    ctrls.push(mdlCtrl);
                });

                var errKey = attrs['name']+"GrpReqd";
                var min = attrs['minRequired'] || 1;
                var max = attrs['maxRequired'] || ctrls.length;

                $scope.validateGroup = function(){
                    var defined=0;
                    for(i=0;i<ctrls.length;i++){
                        if(ctrls[i].$modelValue){
                            defined++;
                        }
                    }
                    if(defined < min || defined > max){
                        formCtrl.$setValidity(errKey,false);
                    } else {
                        formCtrl.$setValidity(errKey,true);
                    }
                };

                //support real time validation
                angular.forEach(ctrls,function(mdlCtrl){
                    $scope.$watch(function () {
                          return mdlCtrl.$modelValue;
                       }, $scope.validateGroup);
                });

            }
        };
    })

HTML usage :

<div name="CancellationInfo" group min-required="1" max-required="1">
            <input type="text" class="form-control group-member" style="width:100%;" name="Field1" ng-model="data.myField"  />
            <input type="text" class="form-control group-member" style="width:100%;" name="Field1" ng-model="data.myField2"  />
            <input type="text" class="form-control group-member" style="width:100%;" name="Field2" ng-model="data.myField3"  />
        </div>

Here group directive identifies the logical grouping. This directive sits on an element without ng-model, a div in the above example. group directive receives 2 optional attribute min-required and max-required. Group members are identified using group-member class on individual fields. Group members are supposed to have an ng-model for binding. Since group directive doesn’t have an ng-model error will be emitted under yourForm.$error.CancellationInfoGrpReqd in the above case. Unique Error key is generated from the element name on which group directive is sitting with GrpReqd appended to it.