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

14

171

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/

Spectacles answered 20/8, 2012 at 19:58 Comment(4)
For those reading this in 2015... the top voted answer is NOT the correct one any longer. Look lower. :)Minor
This seems the be the "for 2015" answer @WillStrohl talks about.Eschar
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.Spectacles
@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.Eschar
S
197

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/

Seng answered 20/8, 2012 at 20:24 Comment(19)
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.Spectacles
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.Seng
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!Spectacles
Great solution! Didn't realize at first glance that the key is in how ng-form directive allows you to have nestable forms. Thanks!Postfix
That's great. But is it valid html to have multiple text boxes with the same name?Perutz
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 hackingCleon
@IanWarburton its valid but it sucks when you have different fields using the same name as it drowns the browsers user input historyCleon
Also there seems to be an issue when having radio buttons inside the nested repeated ng-form. See my question here: #19114978Novobiocin
Nesting forms isnt considered to be valid HTML https://mcmap.net/q/37312/-can-you-nest-html-forms Is angular planning a fix for this?Cleon
@Cleon 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.Seng
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> ?Cleon
@Seng this issue is over a year old now, I was wondering if you know of a github thread where I can track it?Cleon
Great. It should be noticed that if your ng-repeat is bound on table tr then you have to use ng-form="myname" attr.Orphanage
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.Betony
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)Fourflush
If you're interested in this working with ngMessages see my answer (which builds on this great answer)Screening
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 ?Diagram
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?Translative
Is it possible to validate textbox by using id attribute instead of name attribute using ng-messages?Brahmanism
N
231

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>
Naughton answered 2/10, 2015 at 12:27 Comment(6)
This is perfect and much easier than doing a directive - can pass a form into components and use this method. Thanks mate!Solitta
I noticed that your form name can't have hyphens if you want this to work. Anyone know why this is?Manvil
@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>Naughton
I noticed that if you remove a repeated item dynamically, the $valid property for the input gets incorrectly falseOlathe
what is you want all your errors to display at one place say at the top of the form?Benison
@Naughton : can you please take a look at my case? am using ng-options Please helpAirsickness
S
197

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/

Seng answered 20/8, 2012 at 20:24 Comment(19)
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.Spectacles
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.Seng
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!Spectacles
Great solution! Didn't realize at first glance that the key is in how ng-form directive allows you to have nestable forms. Thanks!Postfix
That's great. But is it valid html to have multiple text boxes with the same name?Perutz
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 hackingCleon
@IanWarburton its valid but it sucks when you have different fields using the same name as it drowns the browsers user input historyCleon
Also there seems to be an issue when having radio buttons inside the nested repeated ng-form. See my question here: #19114978Novobiocin
Nesting forms isnt considered to be valid HTML https://mcmap.net/q/37312/-can-you-nest-html-forms Is angular planning a fix for this?Cleon
@Cleon 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.Seng
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> ?Cleon
@Seng this issue is over a year old now, I was wondering if you know of a github thread where I can track it?Cleon
Great. It should be noticed that if your ng-repeat is bound on table tr then you have to use ng-form="myname" attr.Orphanage
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.Betony
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)Fourflush
If you're interested in this working with ngMessages see my answer (which builds on this great answer)Screening
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 ?Diagram
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?Translative
Is it possible to validate textbox by using id attribute instead of name attribute using ng-messages?Brahmanism
H
13

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;
            }
          }
        }
      }
    };
});
Hainan answered 28/6, 2014 at 20:0 Comment(1)
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.Spectacles
K
11

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

Kraigkrait answered 20/6, 2014 at 19:59 Comment(2)
The accepted answer did not work for me. This one however did. (I use Angular 2.1.14)Gymnasium
+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 :)Sandiesandifer
S
3

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>
Smoulder answered 11/2, 2014 at 1:5 Comment(0)
P
1

Looking over these solutions, the one provided by Al Johri above is the closest to my needs, but his directive was a little less programmable then I wanted. Here is my version of his solutions:

angular.module("app", [])
    .directive("dynamicFormName", function() {
        return {
            restrict: "A",
            priority: 0,
            require: ["form"],
            compile: function() {
                return {
                    pre: function preLink(scope, iElement, iAttrs, ctrls) {
                        var name = "field" + scope.$index;

                        if (iAttrs.dnfnNameExpression) {
                            name = scope.$eval(iAttrs.dnfnNameExpression);
                        }

                        var parentForm = iElement.parent().controller("form");
                        if (parentForm) {
                            var formCtrl = ctrls[0];
                            delete parentForm[formCtrl.$name];
                            formCtrl.$name = name;
                            parentForm[formCtrl.$name] = formCtrl;
                        }
                    }
                 }
            }
        };
   });

This solution lets you just pass a name generator expression to the directive and avoids the lock down to pattern substitution he was using.

I also had trouble initially with this solution since it didn't show an example of using it in markup, so here is how I used it.

<form name="theForm">
    <div ng-repeat="field in fields">
        <input type="number" ng-form name="theInput{{field.id}}" ng-model="field.value" dynamic-form-name dnfn-name-expression="'theInput' + field.id">        
    </div>
</form>

I have a more complete working example on github.

Pym answered 4/7, 2014 at 17:57 Comment(0)
L
1

validation is working with ng repeat if I use the following syntax scope.step3Form['item[107][quantity]'].$touched I don't know it's a best practice or the best solution, but it works

<tr ng-repeat="item in items">
   <td>
        <div class="form-group">
            <input type="text" ng-model="item.quantity" name="item[<% item.id%>][quantity]" required="" class="form-control" placeholder = "# of Units" />
            <span ng-show="step3Form.$submitted || step3Form['item[<% item.id %>][quantity]'].$touched">
                <span class="help-block" ng-show="step3Form['item[<% item.id %>][quantity]'].$error.required"> # of Units is required.</span>
            </span>
        </div>
    </td>
</tr>
Lockout answered 7/7, 2015 at 18:55 Comment(0)
S
1

Building on pkozlowski.opensource's answer, I've added a way to have dynamic input names that also work with ngMessages. Note the ng-init part on the ng-form element and the use of furryName. furryName becomes the variable name that contains the variable value for the input's name attribute.

<ion-item ng-repeat="animal in creatures track by $index">
<ng-form name="animalsForm" ng-init="furryName = 'furry' + $index">
        <!-- animal is furry toggle buttons -->
        <input id="furryRadio{{$index}}"
               type="radio"
               name="{{furryName}}"
               ng-model="animal.isFurry"
               ng-value="radioBoolValues.boolTrue"
               required
                >
        <label for="furryRadio{{$index}}">Furry</label>

        <input id="hairlessRadio{{$index}}"
               name="{{furryName}}"
               type="radio"
               ng-model="animal.isFurry"
               ng-value="radioBoolValues.boolFalse"
               required
               >
        <label for="hairlessRadio{{$index}}">Hairless</label>

        <div ng-messages="animalsForm[furryName].$error"
             class="form-errors"
             ng-show="animalsForm[furryName].$invalid && sectionForm.$submitted">
            <div ng-messages-include="client/views/partials/form-errors.ng.html"></div>
        </div>
</ng-form>
</ion-item>
Screening answered 28/8, 2015 at 3:41 Comment(0)
L
1

Here an example of how I do that, I don't know if it is the best solution, but works perfectly.

First, code in HTML. Look at ng-class, it's calling hasError function. Look also to the input's name declaration. I use the $index to create different input names.

<div data-ng-repeat="tipo in currentObject.Tipo"
    ng-class="{'has-error': hasError(planForm, 'TipoM', 'required', $index) || hasError(planForm, 'TipoM', 'maxlength', $index)}">
    <input ng-model="tipo.Nombre" maxlength="100" required
        name="{{'TipoM' + $index}}"/>

And now, here is the hasError function:

$scope.hasError = function (form, elementName, errorType, index) {
           if (form == undefined
               || elementName == undefined
               || errorType == undefined
               || index == undefined)
               return false;

           var element = form[elementName + index];
           return (element != null && element.$error[errorType] && element.$touched);
       };
Laquitalar answered 1/10, 2015 at 7:13 Comment(0)
S
1

It is too late but might be it can help anyone

  1. Create unique name for every control
  2. Validate by using fromname[uniquname].$error

Sample code:

<input 
    ng-model="r.QTY" 
    class="span1" 
    name="QTY{{$index}}" 
    ng-pattern="/^[\d]*\.?[\d]*$/" required/>
<div ng-messages="formName['QTY' +$index].$error"
     ng-show="formName['QTY' +$index].$dirty || formName.$submitted">
   <div ng-message="required" class='error'>Required</div>
   <div ng-message="pattern" class='error'>Invalid Pattern</div>
</div>

See working demo here

Subminiaturize answered 24/1, 2017 at 15:38 Comment(0)
C
1

If your using ng-repeat $index works like this

  name="QTY{{$index}}"

and

   <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>

we have to show the ng-show in ng-pattern

   <span class="alert-error" ng-show="form['QTY' + $index].$error.pattern">
   <span class="alert-error" ng-show="form['QTY' + $index].$error.required">
Chinookan answered 16/6, 2017 at 11:41 Comment(0)
P
0

It is possible and here is how I do the same thing with a table of inputs.

wrap the table in a form like so

Then just use this

I have a form with multi-nested directives that all contain input(s), select(s), etc... These elements are all enclosed in ng-repeats, and dynamic string values.

This is how to use the directive:

<form name="myFormName">
  <nested directives of many levels>
    <your table here>
    <perhaps a td here>
    ex: <input ng-repeat=(index, variable) in variables" type="text"
               my-name="{{ variable.name + '/' + 'myFormName' }}"
               ng-model="variable.name" required />
    ex: <select ng-model="variable.name" ng-options="label in label in {{ variable.options }}"
                my-name="{{ variable.name + index + '/' + 'myFormName' }}"
        </select>
</form>

Note: you can add and index to the string concatenation if you need to serialize perhaps a table of inputs; which is what I did.

app.directive('myName', function(){

  var myNameError = "myName directive error: "

  return {
    restrict:'A', // Declares an Attributes Directive.
    require: 'ngModel', // ngModelController.

    link: function( scope, elem, attrs, ngModel ){
      if( !ngModel ){ return } // no ngModel exists for this element

      // check myName input for proper formatting ex. something/something
      checkInputFormat(attrs);

      var inputName = attrs.myName.match('^\\w+').pop(); // match upto '/'
      assignInputNameToInputModel(inputName, ngModel);

      var formName = attrs.myName.match('\\w+$').pop(); // match after '/'
      findForm(formName, ngModel, scope);
    } // end link
  } // end return

  function checkInputFormat(attrs){
    if( !/\w\/\w/.test(attrs.rsName )){
      throw myNameError + "Formatting should be \"inputName/formName\" but is " + attrs.rsName
    }
  }

  function assignInputNameToInputModel(inputName, ngModel){
    ngModel.$name = inputName
  }

  function addInputNameToForm(formName, ngModel, scope){
    scope[formName][ngModel.$name] = ngModel; return
  }

  function findForm(formName, ngModel, scope){
    if( !scope ){ // ran out of scope before finding scope[formName]
      throw myNameError + "<Form> element named " + formName + " could not be found."
    }

    if( formName in scope){ // found scope[formName]
      addInputNameToForm(formName, ngModel, scope)
      return
    }
    findForm(formName, ngModel, scope.$parent) // recursively search through $parent scopes
  }
});

This should handle many situations where you just don't know where the form will be. Or perhaps you have nested forms, but for some reason you want to attach this input name to two forms up? Well, just pass in the form name you want to attach the input name to.

What I wanted, was a way to assign dynamic values to inputs that I will never know, and then just call $scope.myFormName.$valid.

You can add anything else you wish: more tables more form inputs, nested forms, whatever you want. Just pass the form name you want to validate the inputs against. Then on form submit ask if the $scope.yourFormName.$valid

Parenteau answered 22/11, 2014 at 1:20 Comment(0)
O
0

This will get the name in the ng-repeat to come up seperate in the form validation.

<td>
    <input ng-model="r.QTY" class="span1" name="{{'QTY' + $index}}" ng-pattern="/^[\d]*\.?[\d]*$/" required/>
</td>

But I had trouble getting it to look up in its validation message so I had to use an ng-init to get it to resolve a variable as the object key.

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

Ogbomosho answered 4/5, 2015 at 13:38 Comment(0)
B
0

My requirements were a bit different than the ones asked on the original question, but hopefully I might help someone who is going through the same problem that I was..

I had to define if a field was required or not based on a scope variable.. So I basically had to set ng-required="myScopeVariable" (which is a boolean variable).

<div class="align-left" ng-repeat="schema in schemas">
    <input type="text" ng-required="schema.Required" />
</div>
Baillieu answered 28/10, 2016 at 1:57 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.