How to validate inputs dynamically created using ng-repeat, ng-show (angular)

161,512

Solution 1

AngularJS relies on input names to expose validation errors.

Unfortunately, as of today, it is not possible (without using a custom directive) to dynamically generate a name of an input. Indeed, checking input docs we can see that the name attribute accepts a string only.

To solve the 'dynamic name' problem you need to create an inner form (see ng-form):

<div ng-repeat="social in formData.socials">
      <ng-form name="urlForm">
            <input type="url" name="socialUrl" ng-model="social.url">
            <span class="alert error" ng-show="urlForm.socialUrl.$error.url">URL error</span>
      </ng-form>
  </div>

The other alternative would be to write a custom directive for this.

Here is the jsFiddle showing the usage of the ngForm: http://jsfiddle.net/pkozlowski_opensource/XK2ZT/2/

Solution 2

Since the question was asked the Angular team has solved this issue by making it possible to dynamically create input names.

With Angular version 1.3 and later you can now do this:

<form name="vm.myForm" novalidate>
  <div ng-repeat="p in vm.persons">
    <input type="text" name="person_{{$index}}" ng-model="p" required>
    <span ng-show="vm.myForm['person_' + $index].$invalid">Enter a name</span>
  </div>
</form>

Demo

Angular 1.3 also introduced ngMessages, a more powerful tool for form validation. You can use the same technique with ngMessages:

<form name="vm.myFormNgMsg" novalidate>
    <div ng-repeat="p in vm.persons">
      <input type="text" name="person_{{$index}}" ng-model="p" required>
      <span ng-messages="vm.myFormNgMsg['person_' + $index].$error">
        <span ng-message="required">Enter a name</span>
      </span>
    </div>
  </form>

Solution 3

If you don't want to use ng-form you can use a custom directive that will change the form's name attribute. Place this directive as an attribute on the same element as your ng-model.

If you're using other directives in conjunction, be careful that they don't have the "terminal" property set otherwise this function won't be able to run (given that it has a priority of -1).

For example, when using this directive with ng-options, you must run this one line monkeypatch: https://github.com/AlJohri/bower-angular/commit/eb17a967b7973eb7fc1124b024aa8b3ca540a155

angular.module('app').directive('fieldNameHack', function() {
    return {
      restrict: 'A',
      priority: -1,
      require: ['ngModel'],
      // the ngModelDirective has a priority of 0.
      // priority is run in reverse order for postLink functions.
      link: function (scope, iElement, iAttrs, ctrls) {

        var name = iElement[0].name;
        name = name.replace(/\{\{\$index\}\}/g, scope.$index);

        var modelCtrl = ctrls[0];
        modelCtrl.$name = name;

      }
    };
});

I often find it useful to use ng-init to set the $index to a variable name. For example:

<fieldset class='inputs' ng-repeat="question questions" ng-init="qIndex = $index">

This changes your regular expression to:

name = name.replace(/\{\{qIndex\}\}/g, scope.qIndex);

If you have multiple nested ng-repeats, you can now use these variable names instead of $parent.$index.

Definition of "terminal" and "priority" for directives: https://docs.angularjs.org/api/ng/service/$compile#directive-definition-object

Github Comment regarding need for ng-option monkeypatch: https://github.com/angular/angular.js/commit/9ee2cdff44e7d496774b340de816344126c457b3#commitcomment-6832095 https://twitter.com/aljohri/status/482963541520314369

UPDATE:

You can also make this work with ng-form.

angular.module('app').directive('formNameHack', function() {
    return {
      restrict: 'A',
      priority: 0,
      require: ['form'],
      compile: function() {
        return {
          pre: function(scope, iElement, iAttrs, ctrls) {
            var parentForm = $(iElement).parent().controller('form');
            if (parentForm) {
                var formCtrl = ctrls[0];
                delete parentForm[formCtrl.$name];
                formCtrl.$name = formCtrl.$name.replace(/\{\{\$index\}\}/g, scope.$index);
                parentForm[formCtrl.$name] = formCtrl;
            }
          }
        }
      }
    };
});

Solution 4

Use the ng-form directive inside of the tag in which you are using the ng-repeat directive. You can then use the scope created by the ng-form directive to reference a generic name. For example:

    <div class="form-group col-sm-6" data-ng-form="subForm" data-ng-repeat="field in justificationInfo.justifications"">

        <label for="{{field.label}}"><h3>{{field.label}}</h3></label>
        <i class="icon-valid" data-ng-show="subForm.input.$dirty && subForm.input.$valid"></i>
        <i class="icon-invalid" data-ng-show="subForm.input.$dirty && subForm.input.$invalid"></i>
        <textarea placeholder="{{field.placeholder}}" class="form-control" id="{{field.label}}" name="input" type="text" rows="3" data-ng-model="field.value" required>{{field.value}}</textarea>

    </div>

Credit to: http://www.benlesh.com/2013/03/angular-js-validating-form-elements-in.html

Solution 5

Added more complex example with "custom validation" on the side of controller http://jsfiddle.net/82PX4/3/

<div class='line' ng-repeat='line in ranges' ng-form='lineForm'>
    low: <input type='text' 
                name='low'
                ng-pattern='/^\d+$/' 
                ng-change="lowChanged(this, $index)" ng-model='line.low' />
    up: <input type='text' 
                name='up'
                ng-pattern='/^\d+$/'
                ng-change="upChanged(this, $index)" 
                ng-model='line.up' />
    <a href ng-if='!$first' ng-click='removeRange($index)'>Delete</a>
    <div class='error' ng-show='lineForm.$error.pattern'>
        Must be a number.
    </div>
    <div class='error' ng-show='lineForm.$error.range'>
        Low must be less the Up.
    </div>
</div>
Share:
161,512

Related videos on Youtube

PFranchise
Author by

PFranchise

Web and mobile developer.

Updated on July 08, 2022

Comments

  • PFranchise
    PFranchise almost 2 years

    I have a table that is created using ng-repeat. I want to add validation to each element in the table. The problem is that each input cell has the same name as the cell above and below it. I attempted to use the {{$index}} value to name the inputs, but despite the string literals in HTML appearing correct, it is now working.

    Here is my code as of now:

    <tr ng-repeat="r in model.BSM ">
       <td>
          <input ng-model="r.QTY" class="span1" name="QTY{{$index}}" ng-pattern="/^[\d]*\.?[\d]*$/" required/>
          <span class="alert-error" ng-show="form.QTY{{$index}}.$error.pattern"><strong>Requires a number.</strong></span>
          <span class="alert-error" ng-show="form.QTY{{$index}}.$error.required"><strong>*Required</strong></span>
       </td>
    </tr>
    

    I have tried removing the {{}} from index, but that does not work either. As of now, the validation property of the input is working correctly, but the error message is not displayed.

    Anyone have any suggestions?

    Edit: In addition to the great answers below, here is a blog article that covers this issue in more detail: http://www.thebhwgroup.com/blog/2014/08/angularjs-html-form-design-part-2/

    • Will Strohl
      Will Strohl over 8 years
      For those reading this in 2015... the top voted answer is NOT the correct one any longer. Look lower. :)
    • osiris
      osiris over 8 years
      This seems the be the "for 2015" answer @WillStrohl talks about.
    • PFranchise
      PFranchise over 8 years
      What is proper SO etiquette here? Should I leave the accepted answer since it was correct at the time or accept the correct answer for today? Just want this seemingly popular thread to helpful to new visitors.
    • osiris
      osiris over 6 years
      @PFranchise, I don't know but i think a visible note about it could help. Maybe as an edit to your question, so the note stays where more people can see it.
  • PFranchise
    PFranchise over 11 years
    Thanks for the reply. I am still having an issue after using forms and was wondering if you knew off hand if using tables r ng-patter validation might cause this solution to break.
  • pkozlowski.opensource
    pkozlowski.opensource over 11 years
    AngularJS uses DOM traversal to compile its templates so your HTML must be a valid one. If you could prepare a jsFiddle with your template it would be easier to help.
  • PFranchise
    PFranchise over 11 years
    I figured out the issue. Was simple after looking over your links. Thanks a ton for quickly providing a solution that i will be using often!
  • Felipe Castro
    Felipe Castro over 11 years
    Great solution! Didn't realize at first glance that the key is in how ng-form directive allows you to have nestable forms. Thanks!
  • Ian Warburton
    Ian Warburton almost 11 years
    That's great. But is it valid html to have multiple text boxes with the same name?
  • Blowsie
    Blowsie over 10 years
    As far as I know its not possible to use a dynamic name even with a custom directive, as I just found out after 2 hours of hacking
  • Blowsie
    Blowsie over 10 years
    @IanWarburton its valid but it sucks when you have different fields using the same name as it drowns the browsers user input history
  • rasmusvhansen
    rasmusvhansen over 10 years
    Also there seems to be an issue when having radio buttons inside the nested repeated ng-form. See my question here: stackoverflow.com/questions/19114478/…
  • Blowsie
    Blowsie over 10 years
    Nesting forms isnt considered to be valid HTML stackoverflow.com/questions/379610/can-you-nest-html-forms Is angular planning a fix for this?
  • pkozlowski.opensource
    pkozlowski.opensource over 10 years
    @Blowsie you are not nesting real form here, but rather ng-form DOM elements, so the link to the other SO question is not relevant here.
  • Blowsie
    Blowsie over 10 years
    Im aiming to keep polyfills / additional fixes for IE8 to a mininum, so I am using the data-ng- approach throughout my entire application. Is it possible to achieve a similar results without using <ng-form> ?
  • Blowsie
    Blowsie over 10 years
    @pkozlowski.opensource this issue is over a year old now, I was wondering if you know of a github thread where I can track it?
  • ivkremer
    ivkremer about 10 years
    Great. It should be noticed that if your ng-repeat is bound on table tr then you have to use ng-form="myname" attr.
  • Marc M.
    Marc M. about 10 years
    This solution is awesome cause it solves the problem in an incredibly AngularJS way. Create a new scope! You keep the name the same and have a different scope for each repeat. That's what the ng-form is doing and this is not a bug that needs to be fixed, but rather the nature of AngularJS.
  • PFranchise
    PFranchise almost 10 years
    Just to make it clear, this answer not being selected, is not indicative of it not being the best answer. It was just posted almost 2 years after the question was originally asked. I would consider both this answer and tomGreen's in addition to the selected answer if you run into this same issue.
  • Jesper Tejlgaard
    Jesper Tejlgaard about 9 years
    The accepted answer did not work for me. This one however did. (I use Angular 2.1.14)
  • tanguy_k
    tanguy_k about 9 years
    This answer should be edited: the issue github.com/angular/angular.js/issues/1404 has been solved since AngularJS 1.3.0 (commit from september 2014)
  • ABCD.ca
    ABCD.ca over 8 years
    If you're interested in this working with ngMessages see my answer (which builds on this great answer)
  • Abdellah Alaoui
    Abdellah Alaoui over 8 years
    +1 this answer worked for me check the link: you just need to add ng-form="formName" to the tag that has ng-repeat ... it worked like a charm :)
  • PavanSandeep
    PavanSandeep over 8 years
    So, lets say we add pagination to this ng-repeat, then the controls on the second/nth page would not get validated. How do we solve for that ?
  • dinkydani
    dinkydani almost 8 years
    This is perfect and much easier than doing a directive - can pass a form into components and use this method. Thanks mate!
  • Patrick Szalapski
    Patrick Szalapski almost 8 years
    I noticed that your form name can't have hyphens if you want this to work. Anyone know why this is?
  • HoffZ
    HoffZ almost 8 years
    @PatrickSzalapski: it's because the form name is used by Angular and variable names with hyphens is not valid syntax in Javascript. Workaround: <span ng-show="vm['my-form']['person_' + $index].$invalid">Enter a name</span>
  • Ashit Vora
    Ashit Vora almost 8 years
    Is it possible to validate fields of other forms inside ng-repeat? I've a situation where I want value of all dynamically create fields be different. So if the value of any field is duplicate, it will show an error in only one field and not both. So I wish to revalidate all the fields. How can this be achieved?
  • jonathanwiesel
    jonathanwiesel over 7 years
    I noticed that if you remove a repeated item dynamically, the $valid property for the input gets incorrectly false
  • RGS
    RGS over 7 years
    Is it possible to validate textbox by using id attribute instead of name attribute using ng-messages?
  • codingbbq
    codingbbq over 6 years
    what is you want all your errors to display at one place say at the top of the form?
  • NeverGiveUp161
    NeverGiveUp161 about 6 years
    @HoffZ : can you please take a look at my case? am using ng-options Please help