Cómo validar entradas creadas dinámicamente usando ng-repeat, ng-show (angular)

167

Tengo una tabla que se crea usando ng-repeat. Quiero agregar validación a cada elemento en la tabla. El problema es que cada celda de entrada tiene el mismo nombre que la celda arriba y debajo de ella. Intenté usar el {{$index}}valor para nombrar las entradas, pero a pesar de que los literales de cadena en HTML parecen correctos, ahora funciona.

Aquí está mi código a partir de ahora:

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

He intentado eliminar el {{}}índice, pero tampoco funciona. A partir de ahora, la propiedad de validación de la entrada funciona correctamente, pero el mensaje de error no se muestra.

¿Alguien tiene alguna sugerencia?

Editar: además de las excelentes respuestas a continuación, aquí hay un artículo de blog que cubre este tema con más detalle: http://www.thebhwgroup.com/blog/2014/08/angularjs-html-form-design-part-2 / /

Franquicia
fuente
44
Para aquellos que lean esto en 2015 ... la respuesta más votada ya no es la correcta. Mira más abajo. :)
Will Strohl
Esta parece ser la respuesta "para 2015" de la que habla @WillStrohl.
osiris
¿Cuál es la etiqueta SO adecuada aquí? ¿Debo dejar la respuesta aceptada ya que era correcta en ese momento o aceptar la respuesta correcta para hoy? Solo quiero que este hilo aparentemente popular sea útil para los nuevos visitantes.
PFranchise
@PFranchise, no lo sé, pero creo que una nota visible al respecto podría ayudar. Tal vez como una edición de su pregunta, para que la nota permanezca donde más personas puedan verla.
osiris

Respuestas:

197

AngularJS se basa en nombres de entrada para exponer errores de validación.

Desafortunadamente, a partir de hoy, no es posible (sin usar una directiva personalizada) generar dinámicamente el nombre de una entrada. De hecho, al verificar los documentos de entrada podemos ver que el atributo de nombre acepta solo una cadena.

Para resolver el problema del 'nombre dinámico' necesita crear un formulario interno (vea 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>

La otra alternativa sería escribir una directiva personalizada para esto.

Aquí está el jsFiddle que muestra el uso del ngForm: http://jsfiddle.net/pkozlowski_opensource/XK2ZT/2/

pkozlowski.opensource
fuente
2
Eso es genial. ¿Pero es válido html tener múltiples cuadros de texto con el mismo nombre?
Ian Warburton
1
La anidación de formularios no se considera HTML stackoverflow.com/questions/379610/can-you-nest-html-forms ¿Es la planificación angular una solución para esto?
Blowsie
11
@Blowsie que está no anidando forma real aquí, sino más bien ng-formDOM elementos, por lo que el enlace a la otra pregunta por lo que no es relevante aquí.
pkozlowski.opensource
77
Excelente. Debe tenerse en cuenta que si ng-repeatestá vinculado, table trentonces debe usar ng-form="myname"attr.
ivkremer
11
Esta respuesta debería ser editada: el problema github.com/angular/angular.js/issues/1404 se ha resuelto desde AngularJS 1.3.0 (confirmación desde septiembre de 2014)
tanguy_k
228

Desde que se hizo la pregunta, el equipo de Angular ha resuelto este problema haciendo posible la creación dinámica de nombres de entrada.

Con Angular versión 1.3 y posterior , ahora puede hacer esto:

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

Manifestación

Angular 1.3 también introdujo ngMessages, una herramienta más poderosa para la validación de formularios. Puede usar la misma técnica con 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
fuente
2
Esto es perfecto y mucho más fácil que hacer una directiva: puede pasar un formulario a componentes y usar este método. ¡Gracias amigo!
dinkydani
Noté que el nombre de su formulario no puede tener guiones si desea que esto funcione. Alguien sabe por qué es esto?
Patrick Szalapski
@PatrickSzalapski: es porque el nombre del formulario es usado por los nombres angulares y variables con guiones no es una sintaxis válida en Javascript. Solución alternativa: <span ng-show = "vm ['my-form'] ['person_' + $ index]. $ Invalid"> Ingrese un nombre </span>
HoffZ
Me di cuenta de que si elimina un elemento repetido dinámicamente, la $validpropiedad para la entrada se vuelve incorrectamentefalse
jonathanwiesel
¿Qué quiere que se muestren todos sus errores en un lugar, digamos en la parte superior del formulario?
codingbbq
13

Si no desea usar ng-form, puede usar una directiva personalizada que cambiará el atributo de nombre del formulario. Coloque esta directiva como un atributo en el mismo elemento que su modelo ng.

Si está utilizando otras directivas en conjunto, tenga cuidado de que no tengan establecida la propiedad "terminal"; de lo contrario, esta función no podrá ejecutarse (dado que tiene una prioridad de -1).

Por ejemplo, cuando use esta directiva con ng-options, debe ejecutar este parche mono de una línea: 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;

      }
    };
});

A menudo me resulta útil usar ng-init para establecer $ index en un nombre de variable. Por ejemplo:

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

Esto cambia su expresión regular a:

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

Si tiene varias repeticiones ng anidadas, ahora puede usar estos nombres de variables en lugar de $ parent. $ Index.

Definición de "terminal" y "prioridad" de las directivas: https://docs.angularjs.org/api/ng/service/ $ compile # directive-definition-object

Comentario de Github sobre la necesidad de ng-option monkeypatch: https://github.com/angular/angular.js/commit/9ee2cdff44e7d496774b340de816344126c457b3#commitcomment-6832095 https://twitter.com/aljohri/status/48296354152031436

ACTUALIZAR:

También puede hacer que esto funcione con 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
fuente
3
Solo para dejarlo claro, esta respuesta no se seleccionó, no es indicativo de que no sea la mejor respuesta. Acaba de publicarse casi 2 años después de la pregunta original. Consideraría tanto esta respuesta como la de tomGreen además de la respuesta seleccionada si se encuentra con este mismo problema.
PFranchise
11

Use la directiva ng-form dentro de la etiqueta en la que está usando la directiva ng-repeat. Luego puede usar el ámbito creado por la directiva ng-form para hacer referencia a un nombre genérico. Por ejemplo:

    <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édito a: http://www.benlesh.com/2013/03/angular-js-validating-form-elements-in.html


fuente
La respuesta aceptada no funcionó para mí. Este sin embargo lo hizo. (Uso Angular 2.1.14)
Jesper Tejlgaard el
+1 esta respuesta funcionó para mí revise el enlace : solo necesita agregar ng-form="formName"a la etiqueta que tiene ng-repeat ... funcionó de maravilla :)
Abdellah Alaoui
3

Se agregó un ejemplo más complejo con "validación personalizada" en el lado del controlador 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
fuente
1

Al analizar estas soluciones, la que Al Johri proporcionó anteriormente es la más cercana a mis necesidades, pero su directiva era un poco menos programable de lo que quería. Aquí está mi versión de sus soluciones:

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

Esta solución le permite pasar una expresión de generador de nombres a la directiva y evita el bloqueo a la sustitución de patrones que estaba usando.

También tuve problemas inicialmente con esta solución, ya que no mostraba un ejemplo de uso en marcado, así que así es como la usé.

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

Tengo un ejemplo de trabajo más completo en github .

tomgreen98
fuente
1

la validación funciona con ng repeat si uso la siguiente sintaxis scope.step3Form['item[107][quantity]'].$touched No sé si es una mejor práctica o la mejor solución, pero funciona

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

Sobre la base de la respuesta de pkozlowski.opensource , he agregado una forma de tener nombres de entrada dinámicos que también funcionan con ngMessages . Tenga en cuenta la ng-initparte del ng-formelemento y el uso de furryName. furryNamese convierte en el nombre de la variable que contiene el valor de la variable para el input's nameatributo.

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

Es demasiado tarde pero puede ser que pueda ayudar a cualquiera

  1. Crea un nombre único para cada control
  2. Validar usando fromname[uniquname].$error

Código de muestra:

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

Ver demostración de trabajo aquí

Ali Adravi
fuente
1

Si usa ng-repeat $ index funciona así

  name="QTY{{$index}}"

y

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

tenemos que mostrar el ng-show en 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
fuente
0

Es posible y así es como hago lo mismo con una tabla de entradas.

envolver la mesa en una forma así

Entonces solo usa esto

Tengo un formulario con directivas anidadas múltiples que contienen entradas, selecciones, etc. Todos estos elementos están encerrados en ng-repeats y valores de cadena dinámicos.

Así es como se usa la directiva:

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

Nota: puede agregar e indexar a la concatenación de cadenas si necesita serializar tal vez una tabla de entradas; que es lo que hice

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

Esto debería manejar muchas situaciones en las que simplemente no sabes dónde estará el formulario. ¿O quizás tiene formularios anidados, pero por alguna razón desea adjuntar este nombre de entrada a dos formularios? Bueno, simplemente pase el nombre del formulario al que desea adjuntar el nombre de entrada.

Lo que quería era una forma de asignar valores dinámicos a entradas que nunca sabré, y luego simplemente llamar a $ scope.myFormName. $ Valid.

Puede agregar cualquier otra cosa que desee: más tablas, más entradas de formularios, formularios anidados, lo que desee. Simplemente pase el nombre del formulario con el que desea validar las entradas. Luego, en el formulario de envío, pregunte si $ scope.yourFormName. $ Valid

SoEzPz
fuente
0

Esto obtendrá el nombre en ng-repeat para aparecer por separado en la validación del formulario.

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

Pero tuve problemas para buscarlo en su mensaje de validación, así que tuve que usar un ng-init para resolver una variable como clave de objeto.

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

Aquí un ejemplo de cómo hago eso, no sé si es la mejor solución, pero funciona perfectamente.

Primero, el código en HTML. Mira ng-class, está llamando a la función hasError. Mire también la declaración del nombre de la entrada. Yo uso el $ index para crear diferentes nombres de entrada.

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

Y ahora, aquí está la función 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
fuente
0

Mis requisitos eran un poco diferentes a los que se hicieron en la pregunta original, pero espero poder ayudar a alguien que está pasando por el mismo problema que yo ...

Tenía que definir si un campo era obligatorio o no en función de una variable de alcance. Así que básicamente tuve que establecer ng-required="myScopeVariable"(que es una variable booleana).

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