34

How can I skip validation of nested forms with AngularJS? I have to make an outer form valid even when its child form is invalid.

In the example below outer form should be valid (fOuter.$valid must be true). By default, it is not. Is there an option?

Code (jsFiddle):

<div ng-app ng-controller="Ctrl">  
    <ng-form name="fOuter">  
        <h3>Outer form (valid={{fOuter.$valid}})</h3>  
        <input type="text" name="txtOuter" ng-model="outer" placeholder="(required)" required />  
        <ng-form name="fInner">  
            <h3>Inner form (valid={{fInner.$valid}})</h3>  
            <input type="text" name="txtInner" ng-model="inner" placeholder="(required)" required />  
        </ng-form>  
    </ng-form>  
</div>
Antelle
  • 633
  • 1
  • 7
  • 11

12 Answers12

39

Here is my solution inspired by mbernath, that isolates completely the form itself from its father.

This solution take care of the:

  • Form validity ($valid, $invalid)
  • Form interaction ($pristine, $dirty)
  • Nested forms validity and interaction

See it in action in this JSFiddle.

angular.module('isolateForm',[]).directive('isolateForm', [function () {
    return {
        restrict: 'A',
        require: '?form',
        link: function (scope, elm, attrs, ctrl) {
            if (!ctrl) {
                return;
            }

            // Do a copy of the controller
            var ctrlCopy = {};
            angular.copy(ctrl, ctrlCopy);

            // Get the parent of the form
            var parent = elm.parent().controller('form');
            // Remove parent link to the controller
            parent.$removeControl(ctrl);

            // Replace form controller with a "isolated form"
            var isolatedFormCtrl = {
                $setValidity: function (validationToken, isValid, control) {
                    ctrlCopy.$setValidity(validationToken, isValid, control);
                    parent.$setValidity(validationToken, true, ctrl);
                },
                $setDirty: function () {
                    elm.removeClass('ng-pristine').addClass('ng-dirty');
                    ctrl.$dirty = true;
                    ctrl.$pristine = false;
                },
            };
            angular.extend(ctrl, isolatedFormCtrl);
        }
    };
}]);

To use it just call the directive "isolate-form" :

<form name="parent">
    <input type="text" ng-model="outside"/>
    <ng-form name="subform" isolate-form>
        <input type="text" ng-model="inside"/>
    </ng-form>
</form>
91K00
  • 452
  • 4
  • 7
  • 1
    worked like a charm! thanks for the very explanatory example thought. – thicolares Aug 22 '14 at 20:47
  • You're welcome! I had to find a solution at work and present the solution. So I imagined this ;) – 91K00 Sep 23 '14 at 08:14
  • 3
    Does this no longer work? Because right now in the fiddle, all child forms need to be valid before the parent form is valid, and that's the opposite of what the original question asks. – Arwin Jan 06 '16 at 15:59
  • Works great but took me some time to realise the isolated form must be of type ng-form. – ceebreenk Apr 18 '16 at 20:59
  • 4
    This no longer works as of Angular 1.6 due to limitations on copying objects containing scopes. `angular.copy` will produce an error (https://docs.angularjs.org/error/ng/cpws) – joshschreuder Jan 25 '17 at 02:32
  • 1
    @joshschreuder so how can we copy objects containing scopes in 1.6? – galbru Feb 15 '17 at 12:11
  • @Mr.Bean I didn't try overly hard, but I couldn't find a workaround to get this directive working. I investigated our use case for this directive, and found it was waaaay overkill for what was required (don't trigger dirty on the form when a particular field was changed), and instead wrote a directive that just disables the dirty validation for that field. – joshschreuder Feb 16 '17 at 03:32
  • I solved this on Angular 1.6 using [@Christopher Lenz's answer](http://stackoverflow.com/a/37481846/3687018) – McGiogen Apr 20 '17 at 09:02
  • I'm using angular 1.6 and solved the issue removing the "// Replace form controller with a 'isolated form' " section. It works but I did not investigate too much – Marco C. Apr 24 '18 at 15:20
19

I faced the same problem. Inside a larger form I needed to have a subform with several controls that shouldn't touch the state of the parent form.

Here's my solution: I wrote a directive "null-form" that removes the subform from the parent form and that does not send any state changes its parent.

angular.module('nullForm',[]).directive('nullForm', [function () {
  return {
    restrict: 'A',
    require: '?form',
    link: function link(scope, element, iAttrs, formController) {

      if (! formController) {
        return;
      }

      // Remove this form from parent controller
      var parentFormController = element.parent().controller('form');
      parentFormController.$removeControl(formController);

      // Replace form controller with a "null-controller"
      var nullFormCtrl = {
        $addControl: angular.noop,
        $removeControl: angular.noop,
        $setValidity: angular.noop,
        $setDirty: angular.noop,
        $setPristine: angular.noop
      };

      angular.extend(formController, nullFormCtrl);
    }
  };
}]);

You can then use it like this:

<form name="parent">
  <input type="text" ng-model="outside"/>
  <ng-form name="subform" null-form>
    <input type="text" ng-model="inside"/>
  </ng-form>
</form>

Any change or negative validation of "inside" won't take an effect on "parent".

There's one downside, however, due to this solution: subform will not have any state either nor will its CSS classes like ng-invalid etc. work. To accomplish this you would need to re-implement this functionality from the original form-controller.

mbernath
  • 251
  • 2
  • 7
  • 1
    Good idea. One note: Nesting form tags in invalid HTML. In angular you should use ng-form for the inner form element. But, nevertheless, that works. Thank you! – westor Jun 04 '14 at 10:08
  • You're absolutely right. I've just corrected the code. – mbernath Jun 06 '14 at 14:00
  • You just saved my day. This is exactly what I was thinking of building, allowing me to have "temporaly" forms inside directives that does not affect any possible parent form. – Andrés Monge Moreno Aug 27 '15 at 09:18
  • Great solution! .... Why do you need to replace the formController with a nullFormCtrl? Can't you just have the code that removes the child form from the parent form. That way, you get the best of both worlds: 1. the child form won't touch the parent, 2. your child form also has state! .... Unless I missing something? – James Lawson Apr 20 '16 at 10:22
  • It's been a long time since I've done that, but if I remember it right, you need to replace the (child) formController with the nullFormCtrl, because the implementation of a formController will always propagate it's state upwards to any parent form. This is actually doing the trick. Removing the child form from the parent's children is just for sanity. – mbernath Apr 21 '16 at 15:35
14

At least with Angular 1.5 it seems to be enough to remove the nested form from the parent using $removeControl:

module.directive('isolateForm', function() {
  return {
    restrict: 'A',
    require: '?form',
    link: function(scope, element, attrs, formController) {
      if (!formController) {
        return;
      }

      var parentForm = formController.$$parentForm; // Note this uses private API
      if (!parentForm) {
        return;
      }

      // Remove this form from parent controller
      parentForm.$removeControl(formController);
    }
  };
});

Et voila, pristine and validity states of the parent are no longer affected by the nested form.

Christopher Lenz
  • 720
  • 8
  • 13
2

I found out the solution that worked best was Anton's.

Setting the nullFormCtrl suggested by mbernath disables validation on the child form (thxs for paving the way though...).

The only change that I made was in the way the parentForm is accessed. angular does provide a method for that.

.directive('isolateForm', [function () {
    return {
        restrict: 'A',
        require: '?form',
        link: function link(scope, element, iAttrs, formController) {

            if (!formController) {
                return;
            }

            // Remove this form from parent controller
            formController.$$parentForm.$removeControl(formController)

            var _handler = formController.$setValidity;
            formController.$setValidity = function (validationErrorKey, isValid, cntrl) {
                _handler(validationErrorKey, isValid, cntrl);
                formController.$$parentForm.$setValidity(validationErrorKey, true, this);
            }
        }
    };
}]);
AviG
  • 161
  • 4
0

In Angular forms can be nested. This means that the outer form is valid when all of the child forms are valid as well.

So there is no way to make outer form to be valid automatically (through $valid key) when one of inner invalid.

Try to use error.required

   <h3>Outer form (valid={{!fOuter.txtOuter.$error.required}})</h3>

Demo Fiddle

From Angular ngForm docs:

The other way should be to use controller, like:

<h3>Outer form (valid={{isOuterFormValid}})</h3>

controller

$scope.isOuterFormValid = true;

// here, add listener on each input and change flag `isOuterFormValid`
... 
Maxim Shoustin
  • 77,483
  • 27
  • 203
  • 225
0

I am a newbie to Angular however, please check whether the below approach helps.

<div ng-app ng-controller="Ctrl"> <ng-form name="fOuter"> <h3>Outer form (valid={{fOuter.$valid}})</h3> <ng-form name="fInner1"> <h3>Inner form 1 (valid={{fInner1.$valid}})</h3> <input type="text" name="txtInner1" ng-model="outer" placeholder="(required)" required /> </ng-form> <ng-form name="fInner2"> <h3>Inner form 2 (valid={{fInner2.$valid}})</h3> <input type="text" name="txtInner2" ng-model="inner" placeholder="(required)" required /> </ng-form> </ng-form> </div>

0

I had the same issue and resolve it with bit change in local copy of angular.js file itself.

Basically, I added new function to the FormController as below:

form.$resetParent = function() {
    parentForm = nullFormCtrl;
};

and create custom directive:

angular.module('myApp').directive('dtIsolatedForm', function () {
    return {
        restrict: 'A',
        require: '?form',
        link: function (scope, element, attrs, formController) {
            if (!formController || !formController.$parentForm) {
                return;
            }

            formController.$resetParent();
        }
    };
});
Yuriy Rozhovetskiy
  • 22,270
  • 4
  • 37
  • 68
0

Also inspired by mbernath, I found a simpler solution. It consists of creating a dummy form-like directive only for isolation. The directive stops propagation from nested elements to outer form but it doesn't have any form functionality. You can nest ngForms inside and have them fully functional.

angular.directive('formIsolator', function () {

            return {
                name: 'form',
                restrict: 'EAC',
                controller: function() {
                    this.$addControl = angular.noop;
                    this.$$renameControl = function(control, name) {
                        control.$name = name;
                    };
                    this.$removeControl = angular.noop;
                    this.$setValidity = angular.noop;
                    this.$setDirty = angular.noop;
                    this.$setPristine = angular.noop;
                    this.$setSubmitted = angular.noop;
                }
            };
        })

The way is to specify the name of controller in directive definition (name: 'form'). This property isn't documented, but is used for creating ngForm directive in angular source.

Community
  • 1
  • 1
montor
  • 1
  • 1
0

i'd like to suggest mbernath's version without downside

angular.module('yourModule').directive('isolatedForm', [function () {
return {
    restrict: 'A',
    require: '?form',
    link: function link(scope, element, iAttrs, formController) {

        if (!formController) return;

        // Remove this form from parent controller
        var parentFormController = element.parent().controller('form');
        parentFormController.$removeControl(formController);
        // override default behavior
        var _handler = formController.$setValidity;
        formController.$setValidity = function (validationErrorKey, isValid, cntrl) {
            _handler(validationErrorKey, isValid, cntrl);
            parentFormController.$setValidity(validationErrorKey, true, this);
        }
    }
};}]);
Anton
  • 2,535
  • 2
  • 25
  • 28
0

From your controller :

Ctrl.isOuterFormValid = function() {
    var outerFormIsValid = true;
    for(var prop in Ctrl.formName) {
        //The form is only inValid if the property is not a new form and it is invalid
        if(pvCtrl.pvForm[prop].constructor.name !== "FormController" &&
           pvCtrl.pvForm[prop].$invalid){
            outerFormIsValid = false;
        }
    }

    alert(outerFormIsValid);
};

FormController is an Object that gives you information about your form state.
Adding a form to a form, with ng-form is adding a FormController property to your original FormController Object.

This has the advantage of not adding a html directive to all your input elements.

gr3g
  • 2,866
  • 5
  • 28
  • 52
0

Basically, the objective is to separate the connection between nested forms and perform your own validation / access the $error objects of the forms independently. This can be did by introducing a modelController in-between two nested forms and allowing that modelController to determine when the parent form controller and the child form controller should be valid / invalid. This can be achieved by increasing the $setValidity(), which determines when the form should go valid / invalid.

Please find my code in the plunker link below. I have introduced a model controller between the parent and child form. Here I have abstracted the $error object of child form from the parent form. Meaning, the parent form won't be able to see what is wrong with the child form but it will be invalidated when some field goes invalid in the child form. Only the intermediate modelController knows what fields have issues in the child form. This logic can be tweaked or extended based on our needs. Please let me know if someone needs more clarification in terms of code.

[plnkr]: https://plnkr.co/edit/5gvctSSqmWiEAUE3YUcZ?p=preview
Tom Tom
  • 3,680
  • 5
  • 35
  • 40
0

Before you check if the form is valid, simply remove the nestled forms! vm.parentForm.$removeControl(vm.nestledForm);

Twoayem
  • 9
  • 1