Comment valider les entrées créées dynamiquement à l'aide de ng-repeat, ng-show (angular)

167

J'ai une table créée à l'aide de ng-repeat. Je souhaite ajouter une validation à chaque élément du tableau. Le problème est que chaque cellule d'entrée a le même nom que la cellule au-dessus et en dessous. J'ai essayé d'utiliser la {{$index}}valeur pour nommer les entrées, mais bien que les littéraux de chaîne en HTML semblent corrects, cela fonctionne maintenant.

Voici mon code pour le moment:

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

J'ai essayé de supprimer l' {{}}index, mais cela ne fonctionne pas non plus. À partir de maintenant, la propriété de validation de l'entrée fonctionne correctement, mais le message d'erreur ne s'affiche pas.

Quelqu'un a des suggestions?

Edit: En plus des excellentes réponses ci-dessous, voici un article de blog qui couvre ce problème plus en détail: http://www.thebhwgroup.com/blog/2014/08/angularjs-html-form-design-part-2 /

PFranchise
la source
4
Pour ceux qui liront ceci en 2015 ... la réponse la plus votée n'est plus la bonne. Regardez plus bas. :)
Will Strohl
Cela semble être la réponse «pour 2015» dont parle @WillStrohl.
osiris
Quelle est la bonne étiquette SO ici? Dois-je laisser la réponse acceptée puisqu'elle était correcte à l'époque ou accepter la bonne réponse pour aujourd'hui? Je veux juste que ce fil apparemment populaire soit utile aux nouveaux visiteurs.
PFranchise
@PFranchise, je ne sais pas mais je pense qu'une note visible à ce sujet pourrait aider. Peut-être en tant que modification de votre question, afin que la note reste là où plus de gens peuvent la voir.
osiris

Réponses:

197

AngularJS s'appuie sur les noms d'entrée pour exposer les erreurs de validation.

Malheureusement, à ce jour, il n'est pas possible (sans utiliser une directive personnalisée) de générer dynamiquement le nom d'une entrée. En effet, en vérifiant les documents d'entrée, nous pouvons voir que l'attribut name n'accepte qu'une chaîne.

Pour résoudre le problème du 'nom dynamique', vous devez créer un formulaire interne (voir 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>

L'autre alternative serait d'écrire une directive personnalisée pour cela.

Voici le jsFiddle montrant l'utilisation du ngForm: http://jsfiddle.net/pkozlowski_opensource/XK2ZT/2/

pkozlowski.opensource
la source
2
C'est génial. Mais est-il valide html d'avoir plusieurs zones de texte avec le même nom?
Ian Warburton
1
L'imbrication des formulaires n'est pas considérée comme un HTML valide stackoverflow.com/questions/379610/can-you-nest-html-forms La planification angulaire est-elle un correctif pour cela?
Blowsie
11
@Blowsie vous n'emboitez pas ici une forme réelle, mais plutôt ng-formdes éléments DOM, donc le lien vers l'autre question SO n'est pas pertinent ici.
pkozlowski.opensource
7
Génial. Il convient de noter que si vous êtes ng-repeatlié, table trvous devez utiliser ng-form="myname"attr.
ivkremer
11
Cette réponse doit être modifiée: le problème github.com/angular/angular.js/issues/1404 a été résolu depuis AngularJS 1.3.0 (commit à partir de septembre 2014)
tanguy_k
228

Depuis que la question a été posée, l'équipe Angular a résolu ce problème en permettant de créer dynamiquement des noms d'entrée.

Avec Angular version 1.3 et ultérieure, vous pouvez maintenant faire ceci:

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

Démo

Angular 1.3 a également introduit ngMessages, un outil plus puissant pour la validation de formulaire. Vous pouvez utiliser la même technique avec 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>
HoffZ
la source
2
C'est parfait et beaucoup plus facile que de faire une directive - peut passer un formulaire en composants et utiliser cette méthode. Merci mon pote!
dinkydani
J'ai remarqué que le nom de votre formulaire ne peut pas avoir de traits d'union si vous voulez que cela fonctionne. Quelqu'un sait pourquoi c'est?
Patrick Szalapski
@PatrickSzalapski: c'est parce que le nom du formulaire est utilisé par les noms angulaires et les variables avec des traits d'union n'est pas une syntaxe valide en Javascript. Solution de contournement: <span ng-show = "vm ['my-form'] ['person_' + $ index]. $ Invalid"> Entrez un nom </span>
HoffZ
J'ai remarqué que si vous supprimez un élément répété de manière dynamique, la $validpropriété de l'entrée est incorrectefalse
jonathanwiesel
que voulez-vous que toutes vos erreurs s'affichent à un seul endroit, disons en haut du formulaire?
codingbbq
13

Si vous ne souhaitez pas utiliser ng-form, vous pouvez utiliser une directive personnalisée qui modifiera l'attribut de nom du formulaire. Placez cette directive en tant qu'attribut sur le même élément que votre ng-model.

Si vous utilisez d'autres directives en conjonction, veillez à ce qu'elles ne disposent pas de la propriété "terminal", sinon cette fonction ne pourra pas s'exécuter (étant donné qu'elle a une priorité de -1).

Par exemple, lorsque vous utilisez cette directive avec ng-options, vous devez exécuter ce monkeypatch d'une ligne: 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;

      }
    };
});

Je trouve souvent utile d'utiliser ng-init pour définir $ index sur un nom de variable. Par exemple:

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

Cela change votre expression régulière en:

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

Si vous avez plusieurs ng-répétitions imbriquées, vous pouvez désormais utiliser ces noms de variables au lieu de $ parent. $ Index.

Définition du "terminal" et de la "priorité" pour les directives: https://docs.angularjs.org/api/ng/service/ $ compile # directive-definition-object

Commentaire de Github concernant le besoin de ng-option monkeypatch: https://github.com/angular/angular.js/commit/9ee2cdff44e7d496774b340de816344126c457b3#commitcomment-6832095 https://twitter.com/aljohri/status/4829146354

METTRE À JOUR:

Vous pouvez également faire fonctionner cela avec 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;
            }
          }
        }
      }
    };
});
Al Johri
la source
3
Juste pour être clair, cette réponse n'étant pas sélectionnée, ne signifie pas qu'elle n'est pas la meilleure réponse. Il vient d'être publié près de 2 ans après la question initiale. Je considérerais à la fois cette réponse et celle de tomGreen en plus de la réponse sélectionnée si vous rencontrez ce même problème.
PFranchise
11

Utilisez la directive ng-form à l'intérieur de la balise dans laquelle vous utilisez la directive ng-repeat. Vous pouvez ensuite utiliser la portée créée par la directive ng-form pour référencer un nom générique. Par exemple:

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

Crédit à: http://www.benlesh.com/2013/03/angular-js-validating-form-elements-in.html


la source
La réponse acceptée n'a pas fonctionné pour moi. Celui-ci l'a cependant fait. (J'utilise Angular 2.1.14)
Jesper Tejlgaard
+1 cette réponse a fonctionné pour moi vérifier le lien : il vous suffit d'ajouter ng-form="formName"au tag qui a ng-repeat ... cela a fonctionné comme un charme :)
Abdellah Alaoui
3

Ajout d'un exemple plus complexe avec "validation personnalisée" du côté du contrôleur 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>
Mikita Manko
la source
1

En examinant ces solutions, celle fournie par Al Johri ci-dessus est la plus proche de mes besoins, mais sa directive était un peu moins programmable que je ne le souhaitais. Voici ma version de ses 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;
                        }
                    }
                 }
            }
        };
   });

Cette solution vous permet simplement de passer une expression de générateur de nom à la directive et évite le verrouillage de la substitution de modèle qu'il utilisait.

J'ai également eu des problèmes au départ avec cette solution car elle ne montrait pas d'exemple d'utilisation dans le balisage, alors voici comment je l'ai utilisée.

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

J'ai un exemple de travail plus complet sur github .

tomgreen98
la source
1

la validation fonctionne avec ng repeat si j'utilise la syntaxe suivante, scope.step3Form['item[107][quantity]'].$touched je ne sais pas que c'est une meilleure pratique ou la meilleure solution, mais cela fonctionne

<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>
Vlad Vinnikov
la source
1

En me basant sur la réponse de pkozlowski.opensource , j'ai ajouté un moyen d'avoir des noms d'entrée dynamiques qui fonctionnent également avec ngMessages . Notez la ng-initpartie sur l' ng-formélément et l'utilisation de furryName. furryNamedevient le nom de la variable contenant la valeur de la variable pour l' attribut inputs name.

<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>
ABCD.ca
la source
1

Il est trop tard mais peut-être que cela peut aider n'importe qui

  1. Créez un nom unique pour chaque contrôle
  2. Validez en utilisant fromname[uniquname].$error

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

Voir la démo de travail ici

Ali Adravi
la source
1

Si votre utilisation de ng-repeat $ index fonctionne comme ceci

  name="QTY{{$index}}"

et

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

nous devons montrer le ng-show dans ng-pattern

   <span class="alert-error" ng-show="form['QTY' + $index].$error.pattern">
   <span class="alert-error" ng-show="form['QTY' + $index].$error.required">
Kondal
la source
0

C'est possible et voici comment je fais la même chose avec un tableau d'entrées.

envelopper la table dans une forme comme celle-ci

Alors utilisez simplement ceci

J'ai un formulaire avec des directives multi-imbriquées qui contiennent toutes des entrées, des sélections, etc. Ces éléments sont tous entourés de ng-repeats et de valeurs de chaîne dynamiques.

Voici comment utiliser la 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>

Remarque: vous pouvez ajouter et indexer la concaténation de chaînes si vous avez besoin de sérialiser peut-être une table d'entrées; c'est ce que j'ai fait.

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

Cela devrait gérer de nombreuses situations où vous ne savez tout simplement pas où le formulaire sera. Ou peut-être avez-vous des formulaires imbriqués, mais pour une raison quelconque, vous souhaitez attacher ce nom d'entrée à deux formulaires? Eh bien, transmettez simplement le nom du formulaire auquel vous souhaitez attacher le nom d'entrée.

Ce que je voulais, c'était un moyen d'attribuer des valeurs dynamiques à des entrées que je ne saurais jamais, puis d'appeler simplement $ scope.myFormName. $ Valid.

Vous pouvez ajouter tout ce que vous souhaitez: plus de tables, plus d'entrées de formulaire, des formulaires imbriqués, tout ce que vous voulez. Passez simplement le nom du formulaire sur lequel vous souhaitez valider les entrées. Ensuite, lors de l'envoi du formulaire, demandez si le $ scope.yourFormName. $ Valid

SoEzPz
la source
0

Cela obtiendra le nom dans le ng-repeat pour apparaître séparément dans la validation du formulaire.

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

Mais j'ai eu du mal à le faire rechercher dans son message de validation, j'ai donc dû utiliser un ng-init pour lui faire résoudre une variable en tant que clé d'objet.

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

Andrew Clavin
la source
0

Voici un exemple de comment je fais cela, je ne sais pas si c'est la meilleure solution, mais fonctionne parfaitement.

Tout d'abord, du code en HTML. Regardez ng-class, il appelle la fonction hasError. Regardez également la déclaration de nom de l'entrée. J'utilise $ index pour créer différents noms d'entrée.

<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}}"/>

Et maintenant, voici la fonction hasError:

$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);
       };
David Martin
la source
0

Mes exigences étaient un peu différentes de celles posées sur la question initiale, mais j'espère que je pourrais aider quelqu'un qui traverse le même problème que moi.

Je devais définir si un champ était requis ou non en fonction d'une variable de portée. Donc, je devais essentiellement définir ng-required="myScopeVariable"(qui est une variable booléenne).

<div class="align-left" ng-repeat="schema in schemas">
    <input type="text" ng-required="schema.Required" />
</div>
Bartho Bernsmann
la source