Validation dynamique et nom dans un formulaire avec AngularJS

98

J'ai ce formulaire: http://jsfiddle.net/dfJeN/

Comme vous pouvez le voir, la valeur du nom de l'entrée est définie de manière statique:

name="username"

, la validation du formulaire fonctionne correctement (ajoutez quelque chose et supprimez tout le texte de l'entrée, un texte doit apparaître).

Ensuite, j'essaie de définir dynamiquement la valeur du nom: http://jsfiddle.net/jNWB8/

name="{input.name}"

Ensuite, je l'applique à ma validation

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

(ce modèle sera utilisé dans un ng-repeat) mais ma validation de formulaire est interrompue. Il est correctement interprété dans mon navigateur (si j'inspecte l'élément, j'ai vu login.username. $ Error.required).

Une idée ?

EDIT: Après avoir enregistré l'étendue dans la console, il semble que le

{{input.name}}

l'expression n'est pas interpolée. Mon formulaire en tant qu'attribut {{input.name}} mais pas de nom d'utilisateur.

MISE À JOUR: Depuis 1.3.0-rc.3 name = "{{input.name}}" fonctionne comme prévu. S'il vous plaît voir # 1404

IxDay
la source
Après quelques recherches, j'ai trouvé ceci: "Une fois le scénario dans lequel l'utilisation de ngBind est préférable à la liaison {{expression}} est lorsqu'il est souhaitable de mettre des liaisons dans un modèle qui est momentanément affiché par le navigateur dans son état brut avant qu'Angular ne le compile" . Dans cette page docs.angularjs.org/api/ng.directive:ngBind , cela semble être un bon début pour ce que j'essaie de faire. Cet article sera mis à jour si je trouve une solution.
IxDay
Il y a un problème github ouvert github.com/angular/angular.js/issues/1404
Yaroslav
L'une des réponses a résolu votre problème. Si c'est le cas, veuillez la marquer comme réponse en cliquant sur la coche ci-dessous son score.
Ricardo Souza
Voici un article de blog qui sera probablement utile à d'autres personnes qui rencontrent ce problème: thebhwgroup.com/blog/2014/08/angularjs-html-form-design-part-2
PFranchise

Réponses:

176

Vous ne pouvez pas faire ce que vous essayez de faire de cette façon.

En supposant que ce que vous essayez de faire est que vous devez ajouter dynamiquement des éléments à un formulaire, avec quelque chose comme un ng-repeat, vous devez utiliser ng-form imbriqué pour permettre la validation de ces éléments individuels:

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

Malheureusement, ce n'est tout simplement pas une fonctionnalité bien documentée d'Angular.

Ben Lesh
la source
11
comment avez-vous fini par résoudre cela à la fin? Je ne vois toujours pas en quoi cette réponse particulière se rapporte à votre problème - car elle ne montre pas les champs et les noms de formulaire générés dynamiquement?
Oddman
7
Il s'agit d'une solution complète (ou d'une solution de contournement) et de l'approche suggérée par l'équipe angulaire (de docs.angularjs.org/api/ng.directive:form ): "Puisque vous ne pouvez pas générer dynamiquement l'attribut name des éléments d'entrée en utilisant l'interpolation, vous doivent envelopper chaque ensemble d'entrées répétées dans une directive ngForm et les imbriquer dans un élément de formulaire externe. " Chaque formulaire imbriqué a sa propre portée qui permet à cela de fonctionner.
Noremac
2
Cet exemple et cette suggestion n'abordent toujours pas le "nom" dynamique. Il semble qu'ils veulent vous permettre d'imbriquer dynamiquement des ensembles de champs «clonés», mais le nom sous-jacent de chaque champ doit être statique.
thinice
2
@thinice Oui, cela aide. Avec cette solution, le nom n'a pas besoin d'être dynamique. Cela peut être tout ce que vous voulez (comme "foo"). Le point est que le formulaire enfant a sa propre portée, donc les expressions de validation peuvent simplement faire référence à innerForm.foo. $ Error etc. Le ng-model peut alors pointer vers ce que vous voulez dans la portée parent (éventuellement dynamiquement).
Jed Richards
@thinice - Wintamute a raison. Il n'y a pas besoin de noms dynamiques, car vous ne soumettez pas le formulaire directement. L'intention est de modifier un modèle, puis de POST via Ajax. les noms dynamiques ne vont rien de vous à ce stade. Si vous utilisez réellement un formulaire HTML, vous faites quelque chose de bizarre / mal et vous aurez besoin d'une approche différente.
Ben Lesh
44

L'utilisation de ngForm imbriqué vous permet d'accéder au InputController spécifique à partir du modèle HTML. Cependant, si vous souhaitez y accéder à partir d'un autre contrôleur, cela n'aide pas.

par exemple

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

J'utilise cette directive pour aider à résoudre le problème:

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);
    }
  };
});

Maintenant, vous utilisez des noms dynamiques partout où vous avez besoin de l'attribut 'dynamic-name' au lieu de l'attribut 'name'.

par exemple

<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>
Nick Collier
la source
1
J'ai utilisé cette solution à l'exception de l'utilisation à la $interpolateplace de $parse, je me suis senti plus utile
TheRocketSurgeon
je vois que tu fais termial: vrai. Qu'est-ce que ça veut dire? Puis-je également utiliser cette directive sur des formulaires, par exemple <form ng-repeat="item in items" dynamic-name="'item'+item.id"> ... <span ng-show="item{{item.id}}.$invalid">This form is invalid</span></form>?
felixfbecker
16

Le problème devrait être résolu dans AngularJS 1.3, selon cette discussion sur Github .

En attendant, voici une solution temporaire créée par @caitp et @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;
    });
}]);

Démo sur JSFiddle .

Paolo Moretti
la source
1
Pour ceux qui sont bloqués sur ng 1.2, c'est facilement la solution la moins «hacky».
grenade
14

Nice one par @EnISeeK .... mais je l'ai eu pour être plus élégant et moins gênant pour les autres 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]);
        }
    };
}])
srfrnk
la source
1
Je voudrais seulement ajouter ce qui suit. ctrls [0]. $ name = scope. $ eval (attrs.dynamicName) || attrs.dynamicName;
GnrlBzik
7

Juste une petite amélioration par rapport à la solution EnlSeek

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);
    }]

  };
}]);

Voici un essai de plunker . Voici une explication détaillée

Jason Zhang
la source
+1, la directive d'EnlSeek provoquait une boucle infinie dans ma directive; J'ai dû supprimer les éléments `` fx '' de cette réponse pour que cela fonctionne, cependant
John
La priorité peut interférer avec un ensemble de champs qui prendraient le même nom mais qui auraient ng-if. par exemple: <input type = 'text' dynamic-name = 'foo' ng-if = 'field.type == "text" /> <textarea dynamic-name =' foo 'ng-if =' field.type == "textarea"> </textarea> La suppression de la "priorité: 10000" a résolu le problème pour moi et semble toujours fonctionner correctement.
mince
ngIf a la priorité 600. Attribuer une priorité inférieure à 600 pour cette directive devrait la faire fonctionner avec ngIf.
jason zhang
Si aucune priorité n'est définie (par défaut à 0), il peut fonctionner avec ngModel (priorité 0) si cette directive est évaluée avant ngModel. Vous voulez lui donner une priorité afin qu'il soit toujours avant que ngModel ne soit compilé / lié.
jason zhang
5

J'étends un peu la solution @caitp et @Thinkscape, pour autoriser les ng-forms imbriqués créés dynamiquement , comme ceci:

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

Voici ma démo sur JSFiddle .

Gabriel C. Stabel
la source
4

J'ai utilisé la solution de Ben Lesh et cela fonctionne bien pour moi. Mais un problème auquel j'ai été confronté était que lorsque j'ai ajouté un formulaire interne en utilisant ng-form, tous les états du formulaire, par exemple, form.$valid, form.$erroretc., sont devenus indéfinis si j'utilisais la ng-submitdirective.

Donc si j'avais ceci par exemple:

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

Et dans mon contrôleur:

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

J'ai donc dû revenir à l'utilisation d'un événement de clic normal pour soumettre le formulaire, auquel cas il est nécessaire de passer l'objet du formulaire:

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

Et la méthode du contrôleur révisée:

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

Je ne sais pas trop pourquoi mais j'espère que cela aide quelqu'un.

sq1020
la source
3

Ce problème a été résolu dans Angular 1.3+.C'est la syntaxe correcte pour ce que vous essayez de faire:

login[input.name].$invalid
user1261710
la source
0

si nous définissons un nom dynamique pour une entrée comme ci-dessous

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

alors nous avons utilisé la validation d'ensemble pour le nom dynamique comme le code ci-dessous.

<div ng-messages="login.dynamicInputName.$error">
   <div ng-message="required">
   </div>
</div>
Radha Krishna Eedulakanti
la source