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

angularjs, angularjs-ng-form, angularjs-ng-repeat, validation

Solution

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 `ngForm`to 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

Problem

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?

Original source