Skip to content
This repository has been archived by the owner on Apr 12, 2024. It is now read-only.

Nested Forms validation #5858

Closed
aleshchenko opened this issue Jan 17, 2014 · 26 comments
Closed

Nested Forms validation #5858

aleshchenko opened this issue Jan 17, 2014 · 26 comments

Comments

@aleshchenko
Copy link

It would be great to add some optional attribute to form element or div with ng-form attribute which will say that validation errors of current form shouldn't affect validation of parent form.
Here is what I mean:

<div ng-form="parentForm">
  <input...>some valid input</input>
  <div ng-form="childForm" omit-parent-validation="true">
    <input...>some INVALID input</input>
  </div>
</div>

Result:
parentForm.$valid -> true, childForm.$valid -> false
Same logic may be applied for
parentForm.$pristine -> true, childForm.$pristine -> false

@caitp
Copy link
Contributor

caitp commented Jan 23, 2014

This seems like it should be pretty trivial to implement. Care to submit a patch?

@sriram-dev
Copy link

What would be an appropriate name for this attribute ? ng-propagate ? ng-isolate ? Any suggestions ?

@ghost
Copy link

ghost commented Feb 3, 2014

Probably we would want to prevent the registration of the form in its parent form altogether. I'm thinking: a parent attribute on ngForm, that defaults to the ngForm that is the parent in the DOM (formElement.parent().controller('form') in the current implementation). Then, if one defines the attribute but with a falsy value, it wouldn't register itself in another form (this will prevent all propagation of validation, dirtiness and so on). Could then be used the other way around as well; telling ngForm to register itself in a form that is not the actual parent in the DOM.

@plentz
Copy link
Contributor

plentz commented May 28, 2014

related #5037

@91K00
Copy link

91K00 commented Jul 24, 2014

I wrote a temporary solution, waiting the fix from angular.
I use an attribute directive on the nested form that isolates the interaction ($pristine, $dirty) and validity ($valid, $invalid) from its parent form.

Here is the http://jsfiddle.net/gikoo/qNrFX/

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

You can use it like that:

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

@tjwallace
Copy link

Another proposed solution: #8917

@ruifortes
Copy link

👍

1 similar comment
@martinmicunda
Copy link

+1

gonzaloruizdevilla pushed a commit to gonzaloruizdevilla/angular.js that referenced this issue Nov 22, 2014
Child forms propagate always their state to its parent form. Following the pattern of ngModelOptions
a new optional attribute is defined for forms, ngFormOptions that will allow to define now if the form
is isolated from its parent. In the future, if more options are needed this new attribute may be
the place to define them.

Options are exposed in the controller, but the isolated property is read only when the NgFormController
is executed, so the behavior won't change if its value is changed later.

It maybe used like this:

<ng:form name="parent">
  <ng:form name="child" ng-form-options="{isolated:true}">
     <input ng:model="modelA" name="inputA">
     <input ng:model="modelB" name="inputB">
   </ng:form>
</ng:form>

Closes: angular#5858
@rodrigopedra
Copy link

+1

also: #8917

@rodrigopedra
Copy link

Implementation done by this guy in SO (link below), seems ideal (in functionality) to me, as it preserves most of the isolated forms functionality. In my project I am using this code as a base for a custom directive, actually the only change I made was add $setPristine.

http://stackoverflow.com/a/24936234/1211472

@martinmicunda
Copy link

@rodrigopedra is that solution working for you in AngularJS 1.3?

@rodrigopedra
Copy link

yep. I did a small change, but just for my use case.

Here is the directive I am using to isolate the form:

/**
 * @ref http://stackoverflow.com/a/23541054/1211472
 * @ref http://stackoverflow.com/a/24936234/1211472
 */
app.directive(
    'rrIsolatedForm', [
        function () {
            'use strict';
            return {
                restrict : 'A',
                require  : '?form',
                link     : function link ( scope, element, iAttrs, formController ) {
                    element.addClass( 'rr-isolated-form' );

                    if ( !formController )
                    {
                        return;
                    }

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

                    if ( !parentFormController )
                    {
                        return; // root form, no need to isolate
                    }

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

                    // Replace form controller with a "null-controller"
                    var nullFormCtrl = {
                        // $addControl    : angular.noop,
                        // $removeControl : angular.noop,
                        $setValidity   : function ( validationToken, isValid, control ) {
                            originalCtrl.$setValidity( validationToken, isValid, control );
                            parentFormController.$setValidity( validationToken, true, formController );
                        },
                        $setDirty      : function () {
                            element.removeClass( 'ng-pristine' ).addClass( 'ng-dirty' );
                            formController.$dirty = true;
                            formController.$pristine = false;
                        },
                        $setPristine   : function () {
                            element.addClass( 'ng-pristine' ).removeClass( 'ng-dirty' );
                            formController.$dirty = false;
                            formController.$pristine = true;
                        }
                    };

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

If you want I can send you the snippet for the component I am using isolated, it is a list of phones/emails that the user can add/remove in an internal form. The component syncs an array with the outer form through ngModel only when the collection (array) changes.

gonzaloruizdevilla pushed a commit to gonzaloruizdevilla/angular.js that referenced this issue Dec 1, 2014
Child forms propagate always their state to its parent form. Following the pattern of ngModelOptions
a new optional attribute is defined for forms, ngFormOptions that will allow to define now if the form
should be considered as 'root', therefore preventing the propagation of its state to its parent.
In the future, if more options are needed this new attribute ngFormOptions may be the place to define
them.

Options are exposed in the controller, but the isolated property is read only when the NgFormController
is executed, so the behavior won't change if its value is changed later.

It maybe used like this:

<ng:form name="parent">
  <ng:form name="child" ng-form-options="{root:true}">
     <input ng:model="modelA" name="inputA">
     <input ng:model="modelB" name="inputB">
   </ng:form>
</ng:form>

Closes: angular#5858
gonzaloruizdevilla pushed a commit to gonzaloruizdevilla/angular.js that referenced this issue Dec 1, 2014
Child forms propagate always their state to its parent form. Following the pattern of ngModelOptions
a new optional attribute is defined for forms, ngFormOptions that will allow to define now if the form
should be considered as 'root', therefore preventing the propagation of its state to its parent.
In the future, if more options are needed this new attribute ngFormOptions may be the place to define
them.

Options are exposed in the controller, but the isolated property is read only when the NgFormController
is executed, so the behavior won't change if its value is changed later.

It maybe used like this:

<ng:form name="parent">
  <ng:form name="child" ng-form-options="{root:true}">
     <input ng:model="modelA" name="inputA">
     <input ng:model="modelB" name="inputB">
   </ng:form>
</ng:form>

Closes: angular#5858
gonzaloruizdevilla pushed a commit to gonzaloruizdevilla/angular.js that referenced this issue Dec 1, 2014
Child forms propagate always their state to its parent form. Following the pattern of ngModelOptions
a new optional attribute is defined for forms, ngFormOptions that will allow to define now if the form
should be considered as 'root', therefore preventing the propagation of its state to its parent.
In the future, if more options are needed this new attribute ngFormOptions may be the place to define
them.

Options are exposed in the controller, but the isolated property is read only when the NgFormController
is executed, so the behavior won't change if its value is changed later.

It maybe used like this:

<ng:form name="parent">
  <ng:form name="child" ng-form-options="{root:true}">
     <input ng:model="modelA" name="inputA">
     <input ng:model="modelB" name="inputB">
   </ng:form>
</ng:form>

Closes: angular#5858
@alessiodm
Copy link

I wanted to reach a slightly different result from the one described by this issue: i.e., I just wanted to detach some controls from the containing form. This directive does the job, simply removing the $formController from the inheritedData chain and causing the ngModelControllers to register a nullFormCtrl for their parentForm:

.directive('evictForm', function() {
    return {
        restrict : 'A',
        link: {
            pre: function (scope, iElement) {
                iElement.data('$formController', null);
            }
        }
    };
})

I think it might apply to subforms as well, so I'm just posting it here.
Thanks to @chrisirhc for the advice and the discussion.

gonzaloruizdevilla pushed a commit to gonzaloruizdevilla/angular.js that referenced this issue Apr 15, 2015
Child forms propagate always their state to its parent form. A new optional attribute ngFormTopLevel
is defined for forms that will allow to define now if the form should be considered as 'top leve', therefore
preventing the propagation of its state to its parent. I

It maybe used like this:

<ng:form name="parent">
  <ng:form name="child" ng-form-top-level="true">
     <input ng:model="modelA" name="inputA">
     <input ng:model="modelB" name="inputB">
   </ng:form>
</ng:form>

Closes: angular#5858
gonzaloruizdevilla pushed a commit to gonzaloruizdevilla/angular.js that referenced this issue Apr 16, 2015
Child forms propagate always their state to its parent form. A new optional attribute ngFormTopLevel
is defined for forms that will allow to define now if the form should be considered as 'top leve', therefore
preventing the propagation of its state to its parent. I

It maybe used like this:

<ng:form name="parent">
  <ng:form name="child" ng-form-top-level="true">
     <input ng:model="modelA" name="inputA">
     <input ng:model="modelB" name="inputB">
   </ng:form>
</ng:form>

Closes: angular#5858
gonzaloruizdevilla pushed a commit to gonzaloruizdevilla/angular.js that referenced this issue Apr 16, 2015
Child forms propagate always their state to its parent form. A new optional attribute ngFormTopLevel
is defined for forms that will allow to define now if the form should be considered as 'top leve', therefore
preventing the propagation of its state to its parent. I

It maybe used like this:

<ng:form name="parent">
  <ng:form name="child" ng-form-top-level="true">
     <input ng:model="modelA" name="inputA">
     <input ng:model="modelB" name="inputB">
   </ng:form>
</ng:form>

Closes: angular#5858
gonzaloruizdevilla pushed a commit to gonzaloruizdevilla/angular.js that referenced this issue Apr 16, 2015
Child forms propagate always their state to its parent form. A new optional attribute ngFormTopLevel
is defined for forms that will allow to define now if the form should be considered as 'top leve', therefore
preventing the propagation of its state to its parent. I

It maybe used like this:

<ng:form name="parent">
  <ng:form name="child" ng-form-top-level="true">
     <input ng:model="modelA" name="inputA">
     <input ng:model="modelB" name="inputB">
   </ng:form>
</ng:form>

Closes: angular#5858
gonzaloruizdevilla pushed a commit to gonzaloruizdevilla/angular.js that referenced this issue Apr 16, 2015
Child forms propagate always their state to its parent form. A new optional attribute ngFormTopLevel
is defined for forms that will allow to define now if the form should be considered as 'top leve', therefore
preventing the propagation of its state to its parent. I

It maybe used like this:

<ng:form name="parent">
  <ng:form name="child" ng-form-top-level="true">
     <input ng:model="modelA" name="inputA">
     <input ng:model="modelB" name="inputB">
   </ng:form>
</ng:form>

Closes: angular#5858
gonzaloruizdevilla pushed a commit to gonzaloruizdevilla/angular.js that referenced this issue Apr 22, 2015
Child forms propagate always their state to its parent form. A new optional attribute ngFormTopLevel
is defined for forms that will allow to define now if the form should be considered as 'top leve', therefore
preventing the propagation of its state to its parent. I

It maybe used like this:

<ng:form name="parent">
  <ng:form name="child" ng-form-top-level="true">
     <input ng:model="modelA" name="inputA">
     <input ng:model="modelB" name="inputB">
   </ng:form>
</ng:form>

Closes: angular#5858
@iyeldinov
Copy link

@alessiodm your solution breaks form from updating itself.
Is angular team going to solve this issue?

@williamweckl
Copy link

+1
Using @91K00 solution.

evilaliv3 added a commit to globaleaks/GlobaLeaks that referenced this issue Apr 24, 2016
@evilaliv3
Copy link

+1

Using @91K00 solution also!

evilaliv3 added a commit to globaleaks/GlobaLeaks that referenced this issue Apr 24, 2016
evilaliv3 added a commit to globaleaks/GlobaLeaks that referenced this issue Apr 24, 2016
evilaliv3 added a commit to globaleaks/GlobaLeaks that referenced this issue Apr 24, 2016
evilaliv3 added a commit to globaleaks/GlobaLeaks that referenced this issue May 2, 2016
evilaliv3 added a commit to globaleaks/GlobaLeaks that referenced this issue May 2, 2016
evilaliv3 added a commit to globaleaks/GlobaLeaks that referenced this issue May 2, 2016
evilaliv3 added a commit to globaleaks/GlobaLeaks that referenced this issue May 2, 2016
evilaliv3 added a commit to globaleaks/GlobaLeaks that referenced this issue May 3, 2016
evilaliv3 added a commit to globaleaks/GlobaLeaks that referenced this issue May 3, 2016
evilaliv3 added a commit to globaleaks/GlobaLeaks that referenced this issue May 10, 2016
gonzaloruizdevilla pushed a commit to gonzaloruizdevilla/angular.js that referenced this issue Jun 7, 2016
Child forms propagate always their state to its parent form. A new optional attribute ngFormTopLevel
is defined for forms that will allow to define now if the form should be considered as 'top level', therefore
preventing the propagation of its state to its parent. I

It maybe used like this:

<ng:form name="parent">
  <ng:form name="child" ng-form-top-level="true">
     <input ng:model="modelA" name="inputA">
     <input ng:model="modelB" name="inputB">
   </ng:form>
</ng:form>

Closes: angular#5858
@MRezaSafari
Copy link

MRezaSafari commented Jun 20, 2016

First of all, thank you to @91K00 and @gonzaloruizdevilla for trying to fix the problem.

lets assume this structure:

<ng-form name="X1" novalidate>

    <ng-form name="X2" novalidate isolate-form>

        <input name="Input01" ng-model="input1" required />
        <p ng-show="X2.Input01.$touched && X2.Input01.$invalid">input is not valid</p>

        <input name="Input02" ng-model="input2" required />

        <input type="button" id="ButtonX2" value="Submit Nested Form" ng-disabled="X2.$invalid" />

    </ng-form>

<input name="Input03" ng-model="input3" required ng-minlength="5" />

<input type="button" id="ButtonX1" value="Submit Nested Form" ng-disabled="X1.$invalid" />

</ng-form> 

neither the ngFormTopLevel or @91K00 solution can handle this.

tl;dr :
ButtonX1 is dependent to nested form validation and it shouldn't !

Test case 1:
Step 1: Fill input3 with any text and more than 5 character.
Expected: ButtonX1 should be enable.
Result: ButtonX1 still disabled.

Test case 2:
Step 1: Fill input1 with any text.
Step 2: Fill input2 with any text.
Expected: ButtonX2 should be enable.
Result: ButtonX2 is enabled.

Test case 3:
Step 1: Fill input3 with any text and more than 5 character.
Step 2: Fill input1 with any text.
Step 2: Fill input2 with any text.
Expected: ButtonX1 and ButtonX2 should be enable.
Result: ButtonX1 and ButtonX2 is enabled.

and the other problem is the P tag inside the nested form does not show when the Input01 is invalid.
i tried both the isolateForm and the ngFormTopLevel but both of them have this problem.

@91K00
Copy link

91K00 commented Jun 21, 2016

Hi rSafari,
If you add "ng-model" attribute on each input text, it works.
For the message inside the P, I don't know why the $touched control is not working... You can use $dirty or ng-message module instead.

@MRezaSafari
Copy link

MRezaSafari commented Jun 21, 2016

Thank you @91K00 for your answer.

of course i have ng-model for my inputs. i just forgot to add it in this structure ... ( i updated my post)

the main problem is that i have to validate the nested form before i can submit the parent form. it means i cant submit the parent form until i add something valid to the nested form inputs.

by the way, the {{X2.Input01.$error}} is empty and don't have $dirty or $invalid object in it.

@MRezaSafari
Copy link

@penfold
Copy link

penfold commented Feb 21, 2017

This solution no longer works (tested in v1.6.2) due to an exception when copying the original form controller.

Error: [ng:cpws] Can't copy! Making copies of Window or Scope instances is not supported.

Any suggestions for a workaround?

@Erffun
Copy link

Erffun commented Mar 12, 2017

also not working on v1.6.3

@Narretz Narretz removed the PRs plz! label Apr 21, 2017
@Narretz Narretz modified the milestones: Ice Box, 1.5.x Apr 21, 2017
@Narretz
Copy link
Contributor

Narretz commented Nov 14, 2017

why do need to copy the form Controller anyway? It should be enough to simply call parent.$removeControl(self) in the directive. If the form does not have a parent form controller, it doesn't propagate change upwards.

@momega
Copy link

momega commented Nov 17, 2017

Here is working code for angular >=1.6.2, based recent comment. https://github.com/momega/isolate-form

@Narretz
Copy link
Contributor

Narretz commented Jun 12, 2018

Isolate forms aren't in scope for core because the logic required is too heavy. See #10193 (comment) You can detach form controls, however.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests