Validação dinâmica e nome em um formulário com AngularJS

98

Eu tenho este formulário: http://jsfiddle.net/dfJeN/

Como você pode ver, o valor do nome para a entrada é definido estaticamente:

name="username"

, a validação do formulário funciona bem (adicione algo e remova todo o texto da entrada, um texto deve aparecer).

Em seguida, tento definir dinamicamente o valor do nome: http://jsfiddle.net/jNWB8/

name="{input.name}"

Então eu aplico isso à minha validação

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

(este padrão será usado em uma repetição de ng), mas a validação do meu formulário está quebrada. Ele foi interpretado corretamente no meu navegador (se eu inspecionar o elemento, vi login.username. $ Error.required).

Qualquer ideia ?

EDITAR: Após registrar o escopo no console, parece que o

{{input.name}}

expressão não é interpolar. Meu formulário como um atributo {{input.name}}, mas sem nome de usuário.

ATUALIZAÇÃO: Desde 1.3.0-rc.3 name = "{{input.name}}" funciona como esperado. Por favor, veja # 1404

IxDay
fonte
Depois de alguma pesquisa, descobri o seguinte: "Uma vez que o cenário em que o uso de ngBind é preferido em vez de vinculação de {{expressão}} é quando é desejável colocar vinculações no modelo que é momentaneamente exibido pelo navegador em seu estado bruto antes de o Angular compilá-lo" . Nesta página docs.angularjs.org/api/ng.directive:ngBind , parece ser um bom começo para o que estou tentando fazer. Este post será atualizado se eu encontrar uma solução.
IxDay
Há um problema de github aberto github.com/angular/angular.js/issues/1404
Yaroslav
Alguma das respostas resolveu o seu problema. Em caso afirmativo, marque-a como a resposta clicando na marca de seleção abaixo de sua pontuação.
Ricardo Souza
Aqui está um artigo do blog que provavelmente será de alguma ajuda para outras pessoas que se deparam com esse problema: thebhwgroup.com/blog/2014/08/angularjs-html-form-design-part-2
PFranchise

Respostas:

176

Você não pode fazer o que está tentando fazer dessa maneira.

Assumindo que o que você está tentando fazer é adicionar elementos dinamicamente a um formulário, com algo como uma repetição de ng, você precisa usar o formulário de ng aninhado para permitir a validação desses itens individuais:

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

Infelizmente, não é apenas um recurso bem documentado do Angular.

Ben Lesh
fonte
11
como você acabou resolvendo isso no final? Ainda não vejo como essa resposta específica se relaciona ao seu problema - já que ela não mostra campos e nomes de formulário gerados dinamicamente?
Oddman
7
Esta é uma solução completa (ou alternativa) e a abordagem sugerida pela equipe angular (de docs.angularjs.org/api/ng.directive:form ): "Visto que você não pode gerar dinamicamente o atributo de nome de elementos de entrada usando interpolação, você tem que envolver cada conjunto de entradas repetidas em uma diretiva ngForm e aninhá-los em um elemento de formulário externo. " Cada formulário aninhado tem seu próprio escopo, o que permite que isso funcione.
Noremac
2
Este exemplo e sugestão ainda não abordam o "nome" dinâmico. Parece que eles desejam permitir que você aninhe conjuntos de campos 'clonados' dinamicamente, mas o nome subjacente de cada campo deve ser estático.
thinice
2
@fino Sim, ajuda. Com esta solução, o nome não precisa ser dinâmico. Pode ser o que você quiser (como "foo"). O ponto é que o formulário filho tem seu próprio escopo, então as expressões de validação podem apenas referir-se a innerForm.foo. $ Error etc. O ng-model pode então apontar para o que você quiser no escopo pai (possivelmente dinamicamente).
Jed Richards
@thinice - Wintamute está certo. Não há necessidade de nomes dinâmicos, pois você não está enviando o formulário diretamente. A intenção é alterar algum modelo, então POST-lo via Ajax. nomes dinâmicos não vão deixar você em nada nesse ponto. Se você está realmente usando um envio de formulário HTML, está fazendo algo estranho / errado e precisará de uma abordagem diferente.
Ben Lesh
44

O uso de ngForm aninhado permite acessar o InputController específico de dentro do modelo HTML. No entanto, se você deseja acessá-lo de outro controlador, isso não ajuda.

por exemplo

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

Eu uso esta diretiva para ajudar a resolver o 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);
    }
  };
});

Agora você usa nomes dinâmicos sempre que necessário, apenas o atributo 'nome-dinâmico' em vez do atributo 'nome'.

por exemplo

<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
fonte
1
Eu usei essa solução com a exceção de usar em $interpolatevez de $parse, me senti mais útil
TheRocketSurgeon
vejo você fazer termial: true. O que isso significa? Posso usar essa diretiva em formulários também, como <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

O problema deve ser corrigido no AngularJS 1.3, de acordo com esta discussão no Github .

Enquanto isso, aqui está uma solução temporária criada por @caitp e @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 no JSFiddle .

Paolo Moretti
fonte
1
Para aqueles presos no ng 1.2, esta é facilmente a correção menos 'hacky'.
granada de
14

Legal do @EnISeeK .... mas eu fiz para ser mais elegante e menos intrusivo para outras diretivas:

.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
fonte
1
Eu apenas adicionaria o seguinte. ctrls [0]. $ name = scope. $ eval (attrs.dynamicName) || attrs.dynamicName;
GnrlBzik
7

Apenas uma pequena melhoria em relação à solução 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);
    }]

  };
}]);

Aqui está um teste de êmbolo . Aqui está a explicação detalhada

Jason Zhang
fonte
+1, a diretiva de EnlSeek estava causando um loop infinito em minha diretiva; Eu tive que remover as partes 'fx' desta resposta para fazê-la funcionar, no entanto
João
A prioridade pode interferir com um conjunto de campos que assumiriam o mesmo nome, mas teriam ng-if. por exemplo: <input type = 'text' dynamic-name = 'foo' ng-if = 'field.type == "text" /> <textarea dynamic-name =' foo 'ng-if =' field.type == "textarea"> </textarea> Remover a 'prioridade: 10000' resolveu o problema para mim e ainda parece funcionar corretamente.
thinice
ngIf tem prioridade 600. Atribuir uma prioridade inferior a 600 para esta diretiva deve fazê-la funcionar junto com ngIf.
jason zhang
Se nenhuma prioridade for definida (padrão para 0), pode funcionar com ngModel (prioridade 0) se esta diretiva for avaliada antes de ngModel. Você deseja dar a ele uma prioridade para que sempre seja antes de o ngModel ser compilado / vinculado.
jason zhang
5

Eu expandi a solução @caitp e @Thinkscape um pouco, para permitir formulários ng aninhados criados dinamicamente , como este:

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

Aqui está minha demonstração no JSFiddle .

Gabriel C. Stabel
fonte
4

Usei a solução de Ben Lesh e funcionou bem para mim. Mas um problema que enfrentei foi que quando adicionei um formulário interno usando ng-form, todos os estados do formulário, por exemplo, form.$valid, form.$erroretc, tornaram-se indefinidos se eu estivesse usando ong-submit diretiva.

Então, se eu tivesse isso, por exemplo:

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

E no meu controlador:

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

Então, tive que voltar a usar um evento de clique regular para enviar o formulário, caso em que é necessário passar o objeto do formulário:

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

E o método do controlador revisado:

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

Não tenho certeza do porquê, mas espero que ajude alguém.

sq1020
fonte
3

Este problema foi corrigido no Angular 1.3+. Esta é a sintaxe correta para o que você está tentando fazer:

login[input.name].$invalid
user1261710
fonte
0

se definirmos o nome dinâmico para uma entrada como a abaixo

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

então usamos a validação de conjunto para o nome dinâmico como o código abaixo.

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