Como validar entradas criadas dinamicamente usando ng-repeat, ng-show (angular)

167

Eu tenho uma tabela que é criada usando ng-repeat. Desejo adicionar validação a cada elemento da tabela. O problema é que cada célula de entrada tem o mesmo nome que a célula acima e abaixo dela. Tentei usar o {{$index}}valor para nomear as entradas, mas, apesar de os literais de string em HTML parecerem corretos, ele está funcionando agora.

Aqui está o meu código a partir de agora:

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

Eu tentei remover o {{}}índice, mas isso também não funciona. A partir de agora, a propriedade de validação da entrada está funcionando corretamente, mas a mensagem de erro não é exibida.

Alguém tem alguma sugestão?

Editar: além das ótimas respostas abaixo, aqui está um artigo de blog que aborda esse problema com mais detalhes: http://www.thebhwgroup.com/blog/2014/08/angularjs-html-form-design-part-2 /

PFranchise
fonte
4
Para aqueles que estão lendo isso em 2015 ... a resposta mais votada NÃO é mais a resposta correta. Olhe mais baixo. :)
Will Strohl
Essa parece ser a resposta "para 2015" sobre a qual @WillStrohl fala.
Osiris
O que é a etiqueta SO apropriada aqui? Devo deixar a resposta aceita, pois estava correta na época ou aceitar a resposta correta para hoje? Só quero que esse tópico aparentemente popular seja útil para novos visitantes.
PFranchise
@PFranchise, eu não sei, mas acho que uma observação visível sobre isso poderia ajudar. Talvez como uma edição da sua pergunta, para que a nota fique onde mais pessoas possam vê-la.
Osíris

Respostas:

197

O AngularJS depende de nomes de entrada para expor erros de validação.

Infelizmente, a partir de hoje, não é possível (sem o uso de uma diretiva personalizada) gerar dinamicamente o nome de uma entrada. De fato, verificando os documentos de entrada , podemos ver que o atributo name aceita apenas uma string.

Para resolver o problema do 'nome dinâmico', você precisa criar um formulário interno (consulte 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>

A outra alternativa seria escrever uma diretiva personalizada para isso.

Aqui está o jsFiddle mostrando o uso do ngForm: http://jsfiddle.net/pkozlowski_opensource/XK2ZT/2/

pkozlowski.opensource
fonte
2
Isso é ótimo. Mas é válido html ter várias caixas de texto com o mesmo nome?
11133 Ian Warburton
1
Aninhar formulários não é considerado HTML stackoverflow.com/questions/379610/can-you-nest-html-forms válido O planejamento angular é uma correção para isso?
Blowsie
11
@ Blowsie, você não está aninhando a forma real aqui, mas os ng-formelementos DOM, portanto o link para a outra pergunta SO não é relevante aqui.
usar o seguinte código
7
Ótimo. Deve-se notar que, se você ng-repeatestiver ligado table tr, precisará usar o ng-form="myname"attr.
ivkremer
11
Essa resposta deve ser editado: a questão github.com/angular/angular.js/issues/1404 foi resolvido desde AngularJS 1.3.0 (commit a partir de setembro de 2014)
tanguy_k
228

Desde que a pergunta foi feita, a equipe Angular resolveu esse problema, possibilitando a criação dinâmica de nomes de entrada.

Com a versão Angular 1.3 e posterior, agora você pode fazer isso:

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

Demo

O Angular 1.3 também introduziu o ngMessages, uma ferramenta mais poderosa para validação de formulários. Você pode usar a mesma técnica com 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
fonte
2
Isso é perfeito e muito mais fácil do que fazer uma diretiva - pode passar um formulário para os componentes e usar esse método. Obrigado companheiro!
dinkydani
Notei que o nome do seu formulário não pode ter hífens se você quiser que isso funcione. Alguém sabe por que isso é?
Patrick Szalapski
@PatrickSzalapski: é porque o nome do formulário é usado pelo Angular e os nomes das variáveis ​​com hífens não são uma sintaxe válida no Javascript. Solução alternativa: <span ng-show = "vm ['my-form'] ['person_' + $ index]. $ Invalid"> Digite um nome </span>
HoffZ
Notei que, se você remover um item de repetidos de forma dinâmica, a $validpropriedade para a entrada fica incorretamentefalse
jonathanwiesel
como você deseja que todos os seus erros sejam exibidos em um único local, na parte superior do formulário?
codingbbq
13

Se você não quiser usar o ng-form, poderá usar uma diretiva personalizada que alterará o atributo name do formulário. Coloque esta diretiva como um atributo no mesmo elemento que o seu ng-model.

Se você estiver usando outras diretivas em conjunto, tenha cuidado para que elas não possuam a propriedade "terminal" definida, caso contrário, essa função não poderá ser executada (desde que tenha a prioridade -1).

Por exemplo, ao usar esta diretiva com ng-options, você deve executar este monkeypatch de uma linha: 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;

      }
    };
});

Costumo achar útil usar o ng-init para definir o $ index como um nome de variável. Por exemplo:

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

Isso altera sua expressão regular para:

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

Se você tiver várias repetições ng aninhadas, agora poderá usar esses nomes de variáveis ​​em vez de $ parent. $ Index.

Definição de "terminal" e "prioridade" das diretivas: https://docs.angularjs.org/api/ng/service/ $ compile # directory-definition-object

Comentário do Github sobre a necessidade do monkeypatch da opção ng: https://github.com/angular/angular.js/commit/9ee2cdff44e7d496774b340de816344126c457b3#commitcomment-6832095 https://twitter.com/aljohri/status/482963541520314369

ATUALIZAR:

Você também pode fazer isso funcionar com o 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
fonte
3
Apenas para esclarecer, esta resposta não está sendo selecionada, não é indicativa de não ser a melhor resposta. Foi postado quase 2 anos após a pergunta ser feita originalmente. Eu consideraria essa resposta e a tomGreen, além da resposta selecionada, se você encontrar esse mesmo problema.
PFranchise
11

Use a diretiva ng-form dentro da tag na qual você está usando a diretiva ng-repeat. Você pode usar o escopo criado pela diretiva ng-form para fazer referência a um nome genérico. Por exemplo:

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


fonte
A resposta aceita não funcionou para mim. Este porém fez. (Eu uso angular 2.1.14)
Jesper Tejlgaard
+1 esta resposta funcionou para mim confira o link : você só precisa adicionar ng-form="formName"a tag que tem ng-repeat ... ele trabalhou como um encanto :)
Abdellah Alaoui
3

Exemplo mais complexo adicionado com "validação personalizada" na lateral do 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
fonte
1

Examinando essas soluções, a fornecida por Al Johri acima é a mais próxima das minhas necessidades, mas sua diretiva era um pouco menos programável do que eu queria. Aqui está minha versão de suas soluções:

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

Essa solução permite passar apenas uma expressão de gerador de nome para a diretiva e evitar o bloqueio da substituição de padrão que ele estava usando.

Inicialmente, também tive problemas com essa solução, pois ela não mostrava um exemplo de uso na marcação, então aqui está como eu a usei.

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

Eu tenho um exemplo de trabalho mais completo no github .

tomgreen98
fonte
1

a validação está funcionando com ng repeat se eu usar a seguinte sintaxe scope.step3Form['item[107][quantity]'].$touched Não sei se é uma prática recomendada ou a melhor solução, mas 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
fonte
1

Com base na resposta de pkozlowski.opensource , adicionei uma maneira de ter nomes de entrada dinâmicos que também funcionam com ngMessages . Observe a ng-initparte no ng-formelemento e o uso de furryName. furryNametorna-se o nome da variável que contém o valor da variável para o 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
fonte
1

É tarde demais, mas pode ajudar alguém

  1. Crie um nome exclusivo para cada controle
  2. Valide usando fromname[uniquname].$error

Código de amostra:

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

Veja a demonstração de trabalho aqui

Ali Adravi
fonte
1

Se o uso do índice ng-repeat $ funcionar assim

  name="QTY{{$index}}"

e

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

temos que mostrar o ng-show no ng-padrão

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

É possível e aqui está como eu faço a mesma coisa com uma tabela de entradas.

enrole a mesa em uma forma como essa

Então é só usar isso

Eu tenho um formulário com diretivas multi-aninhadas que todos contêm entradas, seleções, etc.

Isto é como usar a diretiva:

<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: você pode adicionar e indexar à concatenação de cadeias se precisar serializar talvez uma tabela de entradas; Foi o que eu fiz.

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

Isso deve lidar com muitas situações em que você simplesmente não sabe onde estará o formulário. Ou talvez você tenha formulários aninhados, mas por algum motivo você deseja anexar esse nome de entrada a dois formulários acima? Bem, basta passar o nome do formulário ao qual você deseja anexar o nome da entrada.

O que eu queria era uma maneira de atribuir valores dinâmicos a entradas que eu nunca conhecerei e, em seguida, basta chamar $ scope.myFormName. $ Valid.

Você pode adicionar qualquer outra coisa que desejar: mais tabelas, mais entradas de formulário, formulários aninhados, o que quiser. Basta passar o nome do formulário no qual você deseja validar as entradas. Em seguida, no formulário, envie se o $ scope.yourFormName. $ Valid

SoEzPz
fonte
0

Isso fará com que o nome no ng-repeat seja separado separadamente na validação do formulário.

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

Mas tive problemas para procurar na mensagem de validação, então tive que usar um ng-init para resolver uma variável como chave do 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
fonte
0

Aqui está um exemplo de como faço isso, não sei se é a melhor solução, mas funciona perfeitamente.

Primeiro, codifique em HTML. Veja a classe ng, está chamando a função hasError. Veja também a declaração do nome da entrada. Eu uso o $ index para criar diferentes nomes 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}}"/>

E agora, aqui está a função 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
fonte
0

Minhas exigências eram um pouco diferentes das solicitadas na pergunta original, mas espero poder ajudar alguém que está passando pelo mesmo problema que eu.

Eu tive que definir se um campo era obrigatório ou não com base em uma variável de escopo. Então, basicamente, tive que definir ng-required="myScopeVariable" (que é uma variável booleana).

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