Monday, March 4, 2013

Angular JS: Validating Form Elements in a Repeater

How do I use Angular's form validation on these dynamically created elements?


The common scenario is this, you have some ng-repeat creating inputs of some sort, and you need to validate them individually. The knee-jerk reaction is to try to dynamically add names to the input like name="test{{$index}}"... but that won't really work. So now what?


ngForm directive!


The ng-form directive allows for nesting forms that can be used for things like partial validation. The idea is simple, on each repeated element, add an ng-form directive with a name. Then inside that, you can now reference inputs by name on that subform.



<!-- The "main" form directive is the outer form tag. -->
<form name="mainForm" ng-submit="submitAll()">
  <ul>        
        <!-- We add ng-form to the tag ng-repeat is on,
               to create a nested form context. -->
 <li ng-repeat="item in items" ng-form="subForm">
   <input type="text" required name="name" ng-model="item.name"/>
          
          <!-- now we can reference the validated field by name -->
          <span ng-show="subForm.name.$error.required">required</span>
          
          <!-- the nested form context itself can also be checked for validity. -->
          <button type="button" ng-disabled="subForm.$invalid" 
                     ng-click="submitOne(item)">Submit One</button>
 </li>
  </ul>

  <!-- last, but not least, the validation from our 
         subform bubbles up to our main form! -->
  <button type="submit" ng-disabled="mainForm.$invalid">Submit All</button>
</form>



So that's the idea in a nutshell. I'm not going to get too much more verbose with it than that. I've already covered form validation and custom validation elsewhere in my blog. But I will leave you with this plunker to play with that demonstrates this sort of dynamically created form validation, so you can fork it and play with it for yourself:


16 comments:

  1. This is so nice. It would be interesting to see how to perform more advanced validations, for example with jQuery validations. What I mean is fields that are dependent on the value of others. As an example, start and end dates on a user account.
    Anyway, thanks for sharing!

    ReplyDelete
  2. And what about if fields are generated in directive? Now I have problem how to create validation dynamically in directive if form is outside. Here is example: http://plnkr.co/edit/j0xc7iV1Sqid2VK6rMDF?p=preview

    ReplyDelete
    Replies
    1. Well, in the case of form elements inside of a directive, like what you have in your plunker, you'll want to use the ng-form attribute. I've forked and updated your plunker here to show you: http://plnkr.co/edit/Mj359NRavA07ruhTVToz?p=preview

      One critique though, I wouldn't build out my directive's HTML in the manner you are, I would recommend using the templating if at all possible, as it will make it a little easier to maintain, and it brings your directive more into line with an "MVC way of thinking".

      Delete
    2. This doesn't actually seem to work. (Exactly a year old, I know, but I got here via Google.)

      The required attribute gets added, but the visual action (with the required span popping up) doesn't happen.

      Delete
    3. Do you have an example of it not working?

      Delete
  3. Thanks a lot for this! You saved my ass :D

    ReplyDelete
  4. Kodos saved me so much time. AngularJS doc's are missing this trick.

    ReplyDelete
  5. Thanks man, you saved me too.
    One thing I'll add is that I was expecting:
    ng-form="theForm"
    to give the sub form the name "theForm" but it doesn't seem to. In order to set the form controller's $name you still need to specify:
    name="theForm" in the element.

    ReplyDelete
  6. Having each input with a name attribute of 'name' really blows.
    Its totally screws with browser input history. Unfortunately doesn't appear to be a workaround for this at the moment as Angular wont add a variable name into the form object as observed in the book "Mastering Web Application Development with AngularJS"

    Seen on page 26 here = http://www.scribd.com/doc/162170606/9781782161820-Mastering-Web-Application-Development-with-AngularJS-Sample-Chapter

    Do you have any ideas or suggestions on how your would resolve this?

    ReplyDelete
    Replies
    1. I suppose you could create a directive that dynamically added a unique name to your input via direct DOM manipulation. Input history seems like an edge case, but if it's a problem, that's one way you could try to solve it. Then again, input history in a single-page application is going to be really hard to manage anyhow. If it's important you may end up having to go with some sort of localStorage solution, which might get bloated. I guess I don't know that there's a great way to solve that problem in an Angular app. (then again, I find input history obnoxious in apps, so it's nothing I've ever attempted to solve, personally, lol)

      Delete
  7. big thx for the tips!

    ReplyDelete
  8. Thank you so much! You saved my day. Great work!

    ReplyDelete
  9. Great writeup, just what I was looking for!

    ReplyDelete
  10. Woow... I spent too much time trying to figure out this. Finally, this code did the trick. Thank you very much!!!

    ReplyDelete

This form allows some basic HTML. It will only create links if you wrap the URL in an anchor tag (Sorry, it's the Blogger default)