Form validation - Required one of many in a group

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:

Answer

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

Answer

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

      // Update 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);
        }
      }

      // Update 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.

Answer

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

Answer

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

Answer

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;
            };
        }
    };
});
Answer

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.
Answer

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

Answer

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.

Tags

Recent Questions

Top Questions

Home Tags Terms of Service Privacy Policy DMCA Contact Us

©2020 All rights reserved.