Angular ng-repeat with ng-form, accessing validation in controller

MyTimeFinder picture MyTimeFinder · Jun 3, 2014 · Viewed 38.5k times · Source

I am trying to generate an editable list using ng-repeat. I want to remind the user to update any edits before moving on, so I am using ng-form to create "nested" forms on the fly because the documentation says I can then use validation on these dynamically created inputs.

While that seems to work within the HTML, I don't see how to access those dynamically created forms and related validation fields in the controller. Specifically, when the user changes the input I use the form $dirty property to bring up a button to tell the user to commit the changes. So far, so good. However, once the changes are committed I want to $setPristine() on the field to indicate that the changes have been set. There may be other ways of ensuring that changes are committed on each input before I allow the main form committed, but this was the best I could come up with.

Unfortunately, even though the documentation says that if I name the ng-form it will be propagated to the $scope object, I can't find a way to access it. $scope.dynamic_form is undefined.

Here is a plunker showing what I mean:

plnk

Thanks!

[EDIT] Just to add to the issue, what does work for this specific example is to add to the ng-click on the dynamically created input:

ng-click="namesForm.name.$setPristine();clean()"

But I still don't have access to the dynamically created form in the controller. I would like, for example, to add a watcher to the namesForm.name.$pristine so that I can set the mainForm.$setValidity(false) whenever the sub-form is $dirty to prevent the user from submitting the main form until all sub-form changes have been committed.

So in a nutshell, the issue is how to access in a parent controller the validation values of a dynamically created nested ngForm?

Answer

Sly_cardinal picture Sly_cardinal · Jun 5, 2014

Updated 2015-01-17:

As pointed out by Leblanc Meneses in the comments Angular 1.3 now supports interpolation with form, ngForm and input directives.

This means that using expressions to name your elements:

<div ng-form="namesForm_{{$index}}" ng-repeat="name in names">
    <input type="text"
           name="input_{{$index}}_0"></input>
    <!-- ... -->
</div>  

will work as expected:

$scope['namesForm_0']
$scope.namesForm_1

// Access nested form elements:
$scope.namesForm_1.input_1_0
...

Original answer for Angular <= 1.2:

Working with forms and the ngFormController can get tricky pretty quickly.

You need to be aware that you can dynamically add form elements and inputs but they can't be dynamically named - interpolation does not work in the ngForm or name directives.

For example, if you tried to name your nested forms dynamically like this:

<div ng-form="namesForm_{{$index}}" ng-repeat="name in names">
    <!-- ... -->
</div>  

Instead of making all the nested forms available on the scope like this: scope['namesForm_0'] you would only have access to the single (last) form with the literal name scope['namesForm_{{$index}}'].

In your situation you need to create a custom directive that will be added along with ngFormto handle setting $pristine$ and $invalid for that form instance.

JavaScript:

This directive will watch the $dirty state of its form to set the $validity to prevent submission when dirty and handle setting the $pristine state when the 'clean' button is pressed.

app.directive('formCleaner', function(){
    return {
        scope: true,
        require: '^form',
        link: function(scope, element, attr){
            scope.clean = function () {
                scope.namesForm.$setPristine();
            };

            scope.$watch('namesForm.$dirty', function(isDirty){
                scope.namesForm.$setValidity('name', !isDirty);
            });
        }
    };
});

HTML:

Then the only change to your HTML is to add the formCleaner directive.

So change your original HTML from this:

<body ng-controller="MainCtrl">
    <form name="mainForm" submit="submit()">
        <h3>My Editable List</h3>
        <div ng-form="namesForm"
             ng-repeat="name in names">
            <!-- ... -->
        </div>
        <button class="btn btn-default" type="submit">Submit</button>
    </form>
</body>

to this, by adding form-cleaner next to ng-form:

<body ng-controller="MainCtrl">
    <form name="mainForm" submit="submit()">
        <h3>My Editable List</h3>

        <!-- Add the `form-cleaner` directive to the element with `ng-form` -->
        <div form-cleaner
             ng-form="namesForm"
             ng-repeat="name in names">
            <!-- ... -->
        </div>
        <button class="btn btn-default" type="submit">Submit</button>
    </form>
</body>

Here is an updated Plunker showing the new behaviour: http://plnkr.co/edit/Lxem5HJXe0UCvslqbJr3?p=preview