Validación dinámica y nombre en un formulario con AngularJS

98

Tengo este formulario: http://jsfiddle.net/dfJeN/

Como puede ver, el valor del nombre de la entrada está establecido estáticamente:

name="username"

, la validación del formulario funciona bien (agregue algo y elimine todo el texto de la entrada, debe aparecer un texto).

Luego trato de establecer dinámicamente el valor del nombre: http://jsfiddle.net/jNWB8/

name="{input.name}"

Entonces aplico esto a mi validación

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

(este patrón se usará en una repetición ng) pero la validación de mi formulario está rota. Se interpreta correctamente en mi navegador (si inspecciono el elemento vi login.username. $ Error.required).

Alguna idea ?

EDITAR: Después de registrar el alcance en la consola, parece que el

{{input.name}}

la expresión no se interpola. Mi formulario como atributo {{input.name}} pero sin nombre de usuario.

ACTUALIZACIÓN: Desde 1.3.0-rc.3 name = "{{input.name}}" funciona como se esperaba. Consulte el n. ° 1404

IxDay
fuente
Después de investigar un poco, encontré esto: "Una vez que el escenario en el que se prefiere el uso de ngBind sobre el enlace de {{expresión}} es cuando es deseable colocar enlaces en la plantilla que el navegador muestra momentáneamente en su estado sin formato antes de que Angular la compile" . En esta página docs.angularjs.org/api/ng.directive:ngBind , parece ser un buen comienzo para lo que estoy tratando de hacer. Esta publicación se actualizará si encuentro una solución.
IxDay
Hay un problema de github
Yaroslav
Haga que alguna de las respuestas resuelva su problema. Si es así, márquelo como la respuesta haciendo clic en el ckeckmark debajo de su puntuación.
Ricardo Souza
Aquí hay un artículo de blog que probablemente será de alguna ayuda para otras personas que se encuentren con este problema: thebhwgroup.com/blog/2014/08/angularjs-html-form-design-part-2
PFranchise

Respuestas:

176

No puede hacer lo que está tratando de hacer de esa manera.

Suponiendo que lo que está intentando hacer es que necesita agregar elementos dinámicamente a un formulario, con algo como un ng-repeat, debe usar ng-form anidado para permitir la validación de esos elementos individuales:

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

Lamentablemente, no es una característica bien documentada de Angular.

Ben Lesh
fuente
11
¿Cómo terminaste resolviendo esto al final? Todavía no veo cómo esta respuesta en particular se relaciona con su problema, ya que no muestra campos y nombres de formulario generados dinámicamente.
Oddman
7
Esta es una solución completa (o solución alternativa) y el enfoque sugerido por el equipo angular (de docs.angularjs.org/api/ng.directive:form ): "Dado que no puede generar dinámicamente el atributo de nombre de los elementos de entrada mediante interpolación, tiene que envolver cada conjunto de entradas repetidas en una directiva ngForm y anidarlas en un elemento de formulario externo ". Cada formulario anidado tiene su propio alcance que permite que esto funcione.
Noremac
2
Este ejemplo y sugerencia todavía no abordan el "nombre" dinámico. Parece que quieren permitirle anidar conjuntos de campos "clonados" dinámicamente, pero el nombre subyacente de cada campo debe ser estático.
thinice
2
@thinice Sí, ayuda. Con esta solución, el nombre no necesita ser dinámico. Puede ser lo que quieras (como "foo"). El punto es que el formulario hijo tiene su propio alcance, por lo que las expresiones de validación pueden referirse a innerForm.foo. $ Error, etc. El modelo ng puede entonces apuntar a lo que quieras en el alcance principal (posiblemente de forma dinámica).
Jed Richards
@thinice - Wintamute tiene razón. No es necesario utilizar nombres dinámicos, ya que no envía el formulario directamente. La intención es alterar algún modelo, luego PUBLICAR eso a través de Ajax. los nombres dinámicos no te van a dar nada en ese momento. Si realmente está utilizando un envío de formulario HTML, está haciendo algo extraño / incorrecto, y necesitará un enfoque diferente.
Ben Lesh
44

El uso de ngForm anidado le permite acceder al InputController específico desde dentro de la plantilla HTML. Sin embargo, si desea acceder a él desde otro controlador, no ayuda.

p.ej

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

Utilizo esta directiva para ayudar a resolver el problema:

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

Ahora usa nombres dinámicos donde sea necesario, solo el atributo 'nombre dinámico' en lugar del atributo 'nombre'.

p.ej

<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
fuente
1
$interpolate$parse
Usé
Veo que haces termial: cierto. Qué significa eso? ¿Puedo usar esta directiva también en formularios <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

El problema debería solucionarse en AngularJS 1.3, de acuerdo con esta discusión sobre Github .

Mientras tanto, aquí hay una solución temporal creada por @caitp y @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 en JSFiddle .

Paolo Moretti
fuente
1
Para aquellos atascados en ng 1.2, esta es fácilmente la solución menos 'hacky'.
Granada
14

Bonito de @EnISeeK ... pero lo conseguí para ser más elegante y menos molesto para otras directivas:

.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
fuente
1
Solo agregaría lo siguiente. ctrls [0]. $ nombre = alcance. $ eval (attrs.dynamicName) || attrs.dynamicName;
GnrlBzik
7

Solo una pequeña mejora con respecto a la solución 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);
    }]

  };
}]);

Aquí hay una prueba más profunda . Aquí hay una explicación detallada

Jason Zhang
fuente
+1, la directiva de EnlSeek estaba causando un bucle infinito en mi directiva; Sin embargo, tuve que eliminar las piezas 'fx' de esta respuesta para que funcionara
Juan
La prioridad puede interferir con un conjunto de campos que asumirían el mismo nombre pero que tienen ng-if. por ejemplo: <input type = 'text' dynamic-name = 'foo' ng-if = 'field.type == "text" /> <textarea dynamic-name =' foo 'ng-if =' field.type == "textarea"> </textarea> Quitar la 'prioridad: 10000' me solucionó el problema y todavía parece funcionar correctamente.
Thinice
ngIf tiene prioridad 600. Asignar una prioridad inferior a 600 para esta directiva debería hacer que funcione junto con ngIf.
Jason Zhang
Si no se establece ninguna prioridad (el valor predeterminado es 0), puede funcionar con ngModel (prioridad 0) si esta directiva se evalúa antes que ngModel. Desea darle una prioridad para que siempre esté antes de que ngModel sea compilado / vinculado.
Jason Zhang
5

Amplío un poco la solución @caitp y @Thinkscape, para permitir ng-formas anidadas creadas dinámicamente , como esta:

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

Aquí está mi demostración en JSFiddle .

Gabriel C. Stabel
fuente
4

Usé la solución de Ben Lesh y me funciona bien. Pero un problema que enfrenté fue que cuando agregué un formulario interno usando ng-form, todos los estados del formulario, por ejemplo, form.$valid, form.$erroretc., se volvieron indefinidos si estaba usando elng-submit directiva.

Entonces, si tuviera esto, por ejemplo:

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

Y en mi controlador:

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

Así que tuve que volver a usar un evento de clic regular para enviar el formulario, en cuyo caso es necesario pasar el objeto del formulario:

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

Y el método de controlador revisado:

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

No estoy muy seguro de por qué esto es así, pero espero que ayude a alguien.

sq1020
fuente
3

Este problema se ha solucionado en Angular 1.3+ Esta es la sintaxis correcta para lo que está tratando de hacer:

login[input.name].$invalid
usuario1261710
fuente
0

si configuramos un nombre dinámico para una entrada como la siguiente

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

luego usamos la validación de conjunto para el nombre dinámico como el siguiente código.

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