99

I have this form : http://jsfiddle.net/dfJeN/

As you can see the name value for the input is statically set :

name="username"

, the form validation works fine (add something and remove all text from the input, a text must appears).

Then I try to dynamically set the name value : http://jsfiddle.net/jNWB8/

name="{input.name}"

Then I apply this to my validation

login.{{input.name}}.$error.required

(this pattern will be used in an ng-repeat) but my form validation is broken. It is correctly interpreted in my browser (if I inspect the element I saw login.username.$error.required).

Any Idea ?

EDIT: After logging the scope in the console it appears that the

{{input.name}}

expression is not interpolate. My form as an {{input.name}} attribute but no username.

UPDATE: Since 1.3.0-rc.3 name="{{input.name}}" works as expected. Please see #1404

F.D.Castel
  • 2,284
  • 1
  • 16
  • 15
IxDay
  • 3,667
  • 2
  • 22
  • 27
  • After some research I found this : "Once scenario in which the use of ngBind is prefered over {{ expression }} binding is when it's desirable to put bindings into template that is momentarily displayed by the browser in its raw state before Angular compiles it". In this page http://docs.angularjs.org/api/ng.directive:ngBind, it seems to be a good start for what I am trying to do. This post will be updated if I find a solution. – IxDay Jan 17 '13 at 13:15
  • There is an opened github issue https://github.com/angular/angular.js/issues/1404 – Yaroslav Apr 16 '13 at 06:22
  • Have any of the answers solved your problem. If so, please mark it as the answer by clicking on the ckeckmark bellow its score. – Ricardo Souza Sep 30 '14 at 19:15
  • Here is a blog article that will likely be of some help to others who come across this issue: http://www.thebhwgroup.com/blog/2014/08/angularjs-html-form-design-part-2/ – PFranchise Nov 03 '14 at 16:18

9 Answers9

177

You can't do what you're trying to do that way.

Assuming what you're trying to do is you need to dynamically add elements to a form, with something like an ng-repeat, you need to use nested ng-form to allow validation of those individual items:

<form name="outerForm">
<div ng-repeat="item in items">
   <ng-form name="innerForm">
      <input type="text" name="foo" ng-model="item.foo" />
      <span ng-show="innerForm.foo.$error.required">required</span>
   </ng-form>
</div>
<input type="submit" ng-disabled="outerForm.$invalid" />
</form>

Sadly, it's just not a well-documented feature of Angular.

Ben Lesh
  • 107,825
  • 47
  • 247
  • 232
  • 12
    how did you end up solving this in the end? I still don't see how this particular answer relates to your problem - as it doesn't show dynamically generated form fields and names? – Oddman Jun 19 '13 at 22:40
  • 7
    This is a complete solution (or workaround) and the suggested approach by the angular team (from http://docs.angularjs.org/api/ng.directive:form): "Since you cannot dynamically generate the name attribute of input elements using interpolation, you have to wrap each set of repeated inputs in an ngForm directive and nest these in an outer form element." Each nested form has its own scope which allows this to work. – Noremac Dec 05 '13 at 18:36
  • 2
    This example and suggestion still don't address the dynamic "name". It looks like they want to allow you to nest dynamically 'cloned' sets of fields but the underlying name of each field must be static. – thinice Mar 20 '14 at 20:08
  • 2
    @thinice Yes it does help. With this solution the name doesn't need to be dynamic. It can be anything you like (like "foo"). The point is the child form has it's own scope, so validation expressions can just refer to innerForm.foo.$error etc. The ng-model can then point to whatever you want it to in the parent scope (possibly dynamically). – Jed Richards Jul 08 '14 at 18:19
  • 1
    @thinice - Wintamute is right. There is no need for dynamic names, since you're not submitting the form directly. The intention is to alter some model, then POST that via Ajax. dynamic names aren't going to by you anything at that point. If you're actually using an HTML form submit, you're doing something weird/wrong, and you'll need a different approach. – Ben Lesh Jul 08 '14 at 19:52
  • @Ben How do you handle checkbox or radio lists if you cannot have dynamic names? The names are required for tying the form inputs together (e.g. one radio button click does not affect radio buttons in a different list). – Christopher Parsons Sep 17 '14 at 22:25
  • 1
    @Whitelaw you'd use the same approach, making sure that each radiobutton or checkbox is in it's own ng-form directive. This way they can all have the same name, without trampling each others' validation. – Ben Lesh Sep 18 '14 at 17:21
  • This solution work in this case but it is not perfect. I am doing a application with very generic form described by Json data. My form can be nested (a form inside another form) in this case a field defined on a sub ng-form will be present on the parent ng-form too ! I think I am going to try @EnISeeK solution. – Luc DUZAN Jul 06 '15 at 16:12
  • After a long time trying to fix my issue with validation, I finally found this post. This needs to go in to the new documentation section of SO! Thanks a lot – bwright Aug 08 '16 at 15:41
44

Using nested ngForm allows you to access the specific InputController from within the HTML template. However, if you wish to access it from another controller it does not help.

e.g.

<script>
  function OuterController($scope) {
    $scope.inputName = 'dynamicName';

    $scope.doStuff = function() {
      console.log($scope.formName.dynamicName); // undefined
      console.log($scope.formName.staticName); // InputController
    }
  }
</script>

<div controller='OuterController'>
  <form name='myForm'>
    <input name='{{ inputName }}' />
    <input name='staticName' />
  </form>
  <a ng-click='doStuff()'>Click</a>
</div>

I use this directive to help solve the problem:

angular.module('test').directive('dynamicName', function($compile, $parse) {
  return {
    restrict: 'A',
    terminal: true,
    priority: 100000,
    link: function(scope, elem) {
      var name = $parse(elem.attr('dynamic-name'))(scope);
      // $interpolate() will support things like 'skill'+skill.id where parse will not
      elem.removeAttr('dynamic-name');
      elem.attr('name', name);
      $compile(elem)(scope);
    }
  };
});

Now you use dynamic names wherever is needed just the 'dynamic-name' attribute instead of the 'name' attribute.

e.g.

<script>
  function OuterController($scope) {
    $scope.inputName = 'dynamicName';

    $scope.doStuff = function() {
      console.log($scope.formName.dynamicName); // InputController
      console.log($scope.formName.staticName); // InputController
    }
  }
</script>

<div controller='OuterController'>
  <form name='myForm'>
    <input dynamic-name='inputName' />
    <input name='staticName' />
  </form>
  <a ng-click='doStuff()'>Click</a>
</div>
boatcoder
  • 17,525
  • 18
  • 114
  • 178
Nick Collier
  • 741
  • 1
  • 7
  • 9
16

The problem should be fixed in AngularJS 1.3, according to this discussion on Github.

Meanwhile, here's a temporary solution created by @caitp and @Thinkscape:

// Workaround for bug #1404
// https://github.com/angular/angular.js/issues/1404
// Source: http://plnkr.co/edit/hSMzWC?p=preview
app.config(['$provide', function($provide) {
    $provide.decorator('ngModelDirective', function($delegate) {
        var ngModel = $delegate[0], controller = ngModel.controller;
        ngModel.controller = ['$scope', '$element', '$attrs', '$injector', function(scope, element, attrs, $injector) {
            var $interpolate = $injector.get('$interpolate');
            attrs.$set('name', $interpolate(attrs.name || '')(scope));
            $injector.invoke(controller, this, {
                '$scope': scope,
                '$element': element,
                '$attrs': attrs
            });
        }];
        return $delegate;
    });
    $provide.decorator('formDirective', function($delegate) {
        var form = $delegate[0], controller = form.controller;
        form.controller = ['$scope', '$element', '$attrs', '$injector', function(scope, element, attrs, $injector) {
            var $interpolate = $injector.get('$interpolate');
            attrs.$set('name', $interpolate(attrs.name || attrs.ngForm || '')(scope));
            $injector.invoke(controller, this, {
                '$scope': scope,
                '$element': element,
                '$attrs': attrs
            });
        }];
        return $delegate;
    });
}]);

Demo on JSFiddle.

Paolo Moretti
  • 54,162
  • 23
  • 101
  • 92
14

Nice one by @EnISeeK.... but i got it to be more elegant and less obtrusive to other directives:

.directive("dynamicName",[function(){
    return {
        restrict:"A",
        require: ['ngModel', '^form'],
        link:function(scope,element,attrs,ctrls){
            ctrls[0].$name = scope.$eval(attrs.dynamicName) || attrs.dynamicName;
            ctrls[1].$addControl(ctrls[0]);
        }
    };
}])
GnrlBzik
  • 3,358
  • 5
  • 27
  • 37
srfrnk
  • 2,459
  • 1
  • 17
  • 32
  • 1
    I would only add following. ctrls[0].$name = scope.$eval(attrs.dynamicName) || attrs.dynamicName; – GnrlBzik Aug 26 '14 at 19:30
7

Just a little improvement over EnlSeek solution

angular.module('test').directive('dynamicName', ["$parse", function($parse) {
 return {
    restrict: 'A',
    priority: 10000, 
    controller : ["$scope", "$element", "$attrs", 
           function($scope, $element, $attrs){
         var name = $parse($attrs.dynamicName)($scope);
         delete($attrs['dynamicName']);
         $element.removeAttr('data-dynamic-name');
         $element.removeAttr('dynamic-name');
          $attrs.$set("name", name);
    }]

  };
}]);

Here is a plunker trial. Here is detailed explantion

gae123
  • 8,589
  • 3
  • 35
  • 40
jason zhang
  • 633
  • 6
  • 13
  • +1, EnlSeek's directive was causing an infinite loop in my directive; I had to remove the 'fx' pieces of this answer to get it to work, though – John Feb 25 '14 at 17:15
  • The priority can interfere with a set of fields that would assume the same name but have ng-if. e.g.: – thinice Mar 20 '14 at 20:16
  • ngIf has priority 600. Assign a priority less than 600 for this directive should make it work together with ngIf. – jason zhang Mar 20 '14 at 22:53
  • If no priority is set(default to 0), it may work with ngModel (priority 0) if this directive is evaluated before ngModel. You want to give it a priority so that it is always before ngModel is compiled/linked. – jason zhang Mar 20 '14 at 22:55
5

I expand the @caitp and @Thinkscape solution a bit, to allow dynamically created nested ng-forms, like this:

<div ng-controller="ctrl">
    <ng-form name="form">
        <input type="text" ng-model="static" name="static"/>

        <div ng-repeat="df in dynamicForms">
            <ng-form name="form{{df.id}}">
                <input type="text" ng-model="df.sub" name="sub"/>
                <div>Dirty: <span ng-bind="form{{df.id}}.$dirty"></span></div>
            </ng-form>
        </div>

        <div><button ng-click="consoleLog()">Console Log</button></div>
        <div>Dirty: <span ng-bind="form.$dirty"></span></div>
    </ng-form>      
</div>

Here is my demo on JSFiddle.

4

I used Ben Lesh's solution and it works well for me. But one problem I faced was that when I added an inner form using ng-form, all of the form states e.g. form.$valid, form.$error etc became undefined if I was using the ng-submit directive.

So if I had this for example:

<form novalidate ng-submit="saveRecord()" name="outerForm">
    <!--parts of the outer form-->
    <ng-form name="inner-form">
      <input name="someInput">
    </ng-form>
    <button type="submit">Submit</button>
</form>

And in the my controller:

$scope.saveRecord = function() {
    outerForm.$valid // this is undefined
}

So I had to go back to using a regular click event for submitting the form in which case it's necessary to pass the form object:

<form novalidate name="outerForm">  <!--remove the ng-submit directive-->
    <!--parts of the outer form-->
    <ng-form name="inner-form">
      <input name="someInput">
    </ng-form>
    <button type="submit" ng-click="saveRecord(outerForm)">Submit</button>
</form>

And the revised controller method:

$scope.saveRecord = function(outerForm) {
    outerForm.$valid // this works
}

I'm not quite sure why this is but hopefully it helps someone.

sq1020
  • 1,000
  • 2
  • 9
  • 15
3

This issue has been fixed in Angular 1.3+ This is the correct syntax for what you are trying to do:

login[input.name].$invalid
user1261710
  • 2,539
  • 5
  • 41
  • 72
0

if we set dynamic name for a input like the below

<input name="{{dynamicInputName}}" />

then we have use set validation for dynamic name like the below code.

<div ng-messages="login.dynamicInputName.$error">
   <div ng-message="required">
   </div>
</div>
rkmsnc
  • 116
  • 5