Jak sprawdzić dane wejściowe utworzone dynamicznie za pomocą ng-repeat, ng-show (angular)

167

Mam tabelę utworzoną za pomocą ng-repeat. Chcę dodać walidację do każdego elementu w tabeli. Problem polega na tym, że każda komórka wejściowa ma taką samą nazwę jak komórka nad i pod nią. Próbowałem użyć {{$index}}wartości do nazwania danych wejściowych, ale pomimo tego, że literały tekstowe w HTML wydają się poprawne, teraz działa.

Oto mój kod na teraz:

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

Próbowałem usunąć {{}}z indeksu, ale to też nie działa. W tej chwili właściwość walidacji danych wejściowych działa poprawnie, ale komunikat o błędzie nie jest wyświetlany.

Czy ktoś ma jakieś sugestie?

Edycja: Oprócz poniższych świetnych odpowiedzi, tutaj jest artykuł na blogu, który bardziej szczegółowo omawia ten problem: http://www.thebhwgroup.com/blog/2014/08/angularjs-html-form-design-part-2 /

PFranczyza
źródło
4
Dla tych, którzy czytają to w 2015 roku ... odpowiedź, która została wybrana najczęściej, NIE jest już poprawna. Spójrz niżej. :)
Will Strohl
To wydaje się być „do 2015” opowiada o odpowiedź @WillStrohl.
osiris
Jaka jest tutaj właściwa etykieta SO? Czy powinienem zostawić zaakceptowaną odpowiedź, ponieważ była poprawna w tamtym czasie, czy przyjąć poprawną odpowiedź na dziś? Po prostu chcę, aby ten pozornie popularny wątek był pomocny dla nowych odwiedzających.
PFranchise
@PFranchise, nie wiem, ale myślę, że widoczna uwaga na ten temat mogłaby pomóc. Może jako zmiana w Twoim pytaniu, aby notatka pozostała tam, gdzie może ją zobaczyć więcej osób.
osiris

Odpowiedzi:

197

AngularJS polega na nazwach danych wejściowych w celu ujawnienia błędów walidacji.

Niestety, na dzień dzisiejszy nie jest możliwe (bez użycia niestandardowej dyrektywy) dynamiczne generowanie nazwy wejścia. Rzeczywiście, sprawdzając dokumenty wejściowe , widzimy, że atrybut name przyjmuje tylko ciąg.

Aby rozwiązać problem `` dynamicznej nazwy '' , musisz utworzyć wewnętrzną formę (patrz 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>

Inną alternatywą byłoby napisanie w tym celu niestandardowej dyrektywy.

Oto jsFiddle pokazujący użycie ngForm: http://jsfiddle.net/pkozlowski_opensource/XK2ZT/2/

pkozlowski.opensource
źródło
2
To wspaniale. Ale czy poprawnym HTML jest posiadanie wielu pól tekstowych o tej samej nazwie?
Ian Warburton,
1
Zagnieżdżanie formularzy nie jest uważane za poprawne HTML stackoverflow.com/questions/379610/can-you-nest-html-forms Czy planowanie kątowe jest rozwiązaniem tego problemu?
Blowsie
11
@Blowsie, nie zagnieżdżasz tutaj prawdziwej formy, ale raczej ng-formelementy DOM, więc link do drugiego pytania SO nie ma tutaj znaczenia.
pkozlowski.opensource
7
Wspaniały. Należy zauważyć, że jeśli jesteś ng-repeatzwiązany table tr, musisz użyć ng-form="myname"attr.
ivkremer
11
Tę odpowiedź należy zredagować: problem github.com/angular/angular.js/issues/1404 został rozwiązany od czasu AngularJS 1.3.0 (zatwierdzenie od września 2014)
tanguy_k
228

Odkąd zadano to pytanie, zespół Angular rozwiązał ten problem, umożliwiając dynamiczne tworzenie nazw wejść.

W Angular w wersji 1.3 i nowszych możesz teraz zrobić to:

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

Próbny

Angular 1.3 wprowadził również ngMessages, potężniejsze narzędzie do walidacji formularzy. Możesz użyć tej samej techniki z 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
źródło
2
Jest to doskonałe i znacznie łatwiejsze niż wykonanie dyrektywy - można przekazać formularz do komponentów i użyć tej metody. Dzięki stary!
dinkydani,
Zauważyłem, że nazwa twojego formularza nie może zawierać łączników, jeśli chcesz, aby to zadziałało. Czy ktoś wie, dlaczego tak jest?
Patrick Szalapski
@PatrickSzalapski: to dlatego, że nazwa formularza jest używana przez Angular, a nazwy zmiennych z łącznikami nie są poprawną składnią w JavaScript. Obejście: <span ng-show = "vm ['my-form'] ['person_' + $ index]. $ Invalid"> Wpisz imię i nazwisko </span>
HoffZ,
Zauważyłem, że jeśli dynamicznie usuniesz powtarzający się element, $validwłaściwość dla wejścia jest niepoprawnafalse
jonathanwiesel
co chcesz, aby wszystkie błędy były wyświetlane w jednym miejscu, powiedz na górze formularza?
kodowaniebbq
13

Jeśli nie chcesz używać ng-form, możesz użyć niestandardowej dyrektywy, która zmieni atrybut name formularza. Umieść tę dyrektywę jako atrybut w tym samym elemencie co twój model ng.

Jeśli używasz innych dyrektyw w połączeniu, uważaj, aby nie miały ustawionej właściwości „terminal”, w przeciwnym razie ta funkcja nie będzie mogła działać (biorąc pod uwagę, że ma priorytet -1).

Na przykład, gdy używasz tej dyrektywy z ng-options, musisz uruchomić ten jeden wiersz monkeypatch: 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;

      }
    };
});

Często uważam, że przydatne jest użycie ng-init do ustawienia indeksu $ na nazwę zmiennej. Na przykład:

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

Spowoduje to zmianę wyrażenia regularnego na:

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

Jeśli masz wiele zagnieżdżonych powtórzeń ng, możesz teraz używać tych nazw zmiennych zamiast $ parent. $ Index.

Definicja „terminala” i „priorytetu” dla dyrektyw: https://docs.angularjs.org/api/ng/service/ $ compile # Directive-definition-object

Komentarz Github dotyczący potrzeby ng-option monkeypatch: https://github.com/angular/angular.js/commit/9ee2cdff44e7d496774b340de816344126c457b3#commitcomment-6832095 https://twitter.com/aljohri/status/482963541520314

AKTUALIZACJA:

Możesz również sprawić, by działało to z 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
źródło
3
Aby było jasne, że ta odpowiedź nie została wybrana, nie oznacza, że ​​nie jest to najlepsza odpowiedź. Został on opublikowany prawie 2 lata po pierwszym zadaniu pytania. Rozważyłbym zarówno tę odpowiedź, jak i tomGreen jako dodatek do wybranej odpowiedzi, jeśli napotkasz ten sam problem.
PFranchise
11

Użyj dyrektywy ng-form wewnątrz tagu, w którym używasz dyrektywy ng-repeat. Następnie możesz użyć zakresu utworzonego przez dyrektywę ng-form, aby odwołać się do nazwy ogólnej. Na przykład:

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

Kredyt dla: http://www.benlesh.com/2013/03/angular-js-validating-form-elements-in.html


źródło
Przyjęta odpowiedź nie zadziałała dla mnie. Ten jednak to zrobił. (Używam Angular 2.1.14)
Jesper Tejlgaard
+1 ta odpowiedź zadziałała dla mnie sprawdź link : wystarczy dodać ng-form="formName"do tagu, który ma powtórzenie ng ... zadziałało jak urok :)
Abdellah Alaoui
3

Dodano bardziej złożony przykład z „niestandardową walidacją” po stronie kontrolera 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
źródło
1

Patrząc na te rozwiązania, to, które przedstawił Al Johri powyżej, jest najbliższe moim potrzebom, ale jego dyrektywa była trochę mniej programowalna niż chciałem. Oto moja wersja jego rozwiązań:

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

To rozwiązanie pozwala po prostu przekazać wyrażenie generatora nazw do dyrektywy i pozwala uniknąć blokowania podstawiania wzorca, którego używał.

Miałem też początkowo problemy z tym rozwiązaniem, ponieważ nie pokazało ono przykładu użycia go w znacznikach, więc oto jak go użyłem.

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

Mam bardziej kompletny przykład roboczy na githubie .

tomgreen98
źródło
1

walidacja działa z powtórzeniem ng, jeśli używam następującej składni scope.step3Form['item[107][quantity]'].$touched Nie wiem, czy jest to najlepsza praktyka lub najlepsze rozwiązanie, ale działa

<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
źródło
1

Opierając się na odpowiedzi pkozlowski.opensource , dodałem sposób na uzyskanie dynamicznych nazw wejść, które działają również z ngMessages . Zwróć uwagę na ng-initczęść na ng-formelemencie i użycie furryName. furryNamestaje nazwę zmiennej, która zawiera wartość zmiennej dla input„s nameatrybut.

<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
źródło
1

Jest już za późno, ale może pomoże każdemu

  1. Utwórz unikalną nazwę dla każdej kontrolki
  2. Sprawdź, używając fromname[uniquname].$error

Przykładowy kod:

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

Zobacz działające demo tutaj

Ali Adravi
źródło
1

Jeśli używasz ng-repeat $ index działa w ten sposób

  name="QTY{{$index}}"

i

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

musimy pokazać ng-show we wzorze ng

   <span class="alert-error" ng-show="form['QTY' + $index].$error.pattern">
   <span class="alert-error" ng-show="form['QTY' + $index].$error.required">
Kondal
źródło
0

Jest to możliwe i oto jak robię to samo z tabelą wejść.

owinąć stół w taką formę

Następnie po prostu użyj tego

Mam formularz z wieloma zagnieżdżonymi dyrektywami, z których wszystkie zawierają dane wejściowe, zaznaczenia, itp ... Te elementy są zawarte w ng-powtórzeń i dynamicznych wartościach ciągów.

Oto jak korzystać z dyrektywy:

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

Uwaga: jeśli chcesz serializować tabelę danych wejściowych, możesz dodawać i indeksować do konkatenacji ciągów; co właśnie zrobiłem.

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

Powinno to rozwiązać wiele sytuacji, w których po prostu nie wiesz, gdzie będzie formularz. A może masz zagnieżdżone formularze, ale z jakiegoś powodu chcesz dołączyć tę nazwę wejściową do dwóch formularzy wyżej? Cóż, po prostu podaj nazwę formularza, do którego chcesz dołączyć nazwę wejściową.

To, czego chciałem, to sposób na przypisanie wartości dynamicznych do danych wejściowych, których nigdy nie poznam, a następnie wywołanie po prostu $ scope.myFormName. $ Valid.

Możesz dodać wszystko, co chcesz: więcej tabel, więcej danych wejściowych, zagnieżdżone formularze, cokolwiek chcesz. Po prostu podaj nazwę formularza, dla którego chcesz sprawdzić poprawność danych wejściowych. Następnie podczas przesyłania formularza zapytaj, czy $ scope.yourFormName. $ Jest prawidłowe

SoEzPz
źródło
0

Dzięki temu nazwa w powtórzeniu ng pojawi się osobno w walidacji formularza.

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

Ale miałem problem z wyszukaniem go w komunikacie walidacji, więc musiałem użyć ng-init, aby rozwiązać zmienną jako klucz obiektu.

<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
źródło
0

Oto przykład jak to robię, nie wiem, czy to najlepsze rozwiązanie, ale działa idealnie.

Najpierw kod w HTML. Spójrz na ng-class, wywołuje funkcję hasError. Spójrz także na deklarację nazwy wejścia. Używam indeksu $ do tworzenia różnych nazw wejściowych.

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

A teraz mamy funkcję 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
źródło
0

Moje wymagania były nieco inne niż te zadane w pierwotnym pytaniu, ale mam nadzieję, że mógłbym pomóc komuś, kto ma ten sam problem co ja.

Musiałem zdefiniować, czy pole było wymagane, czy nie na podstawie zmiennej zakresu .. Więc w zasadzie musiałem ustawić ng-required="myScopeVariable"(która jest zmienną boolowską).

<div class="align-left" ng-repeat="schema in schemas">
    <input type="text" ng-required="schema.Required" />
</div>
Bartho Bernsmann
źródło