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


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.


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>

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 .


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

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


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


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.


3

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

login[input.name].$invalid

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>
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.