Angular.js: Nested Forms validation

Created on 17 Jan 2014  路  26Comments  路  Source: angular/angular.js

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

forms low won't fix feature

Most helpful comment

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>

All 26 comments

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

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

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.

related #5037

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>

Another proposed solution: #8917

:+1:

+1

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

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

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.

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.

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

There is a PR for this at #10193

+1
Using @91K00 solution.

+1

Using @91K00 solution also!

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.

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.

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.

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?

also not working on v1.6.3

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.

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

Isolate forms aren't in scope for core because the logic required is too heavy. See https://github.com/angular/angular.js/pull/10193#discussion_r66080985 You can detach form controls, however.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

brijesh1ec picture brijesh1ec  路  3Comments

piu picture piu  路  3Comments

butchpeters picture butchpeters  路  3Comments

WesleyKapow picture WesleyKapow  路  3Comments

jetta20162 picture jetta20162  路  3Comments