Formularze reaktywne - zaznacz pola jako dotknięte

88

Mam problem ze znalezieniem sposobu oznaczenia wszystkich pól formularza jako dotkniętych. Główny problem polega na tym, że jeśli nie dotykam pól i nie próbuję wysłać formularza - błąd walidacji nie pojawia się. Mam symbol zastępczy dla tego fragmentu kodu w moim kontrolerze.
Mój pomysł jest prosty:

  1. użytkownik klika przycisk przesyłania
  2. wszystkie pola są zaznaczone jako dotknięte
  3. formater błędów uruchamia się ponownie i wyświetla błędy walidacji

Jeśli ktoś ma inny pomysł, jak pokazać błędy przy przesyłaniu, bez implementacji nowej metody - podziel się nimi. Dzięki!


Mój uproszczony formularz:

<form class="form-horizontal" [formGroup]="form" (ngSubmit)="onSubmit(form.value)">
    <input type="text" id="title" class="form-control" formControlName="title">
    <span class="help-block" *ngIf="formErrors.title">{{ formErrors.title }}</span>
    <button>Submit</button>
</form>

I mój kontroler:

import {Component, OnInit} from '@angular/core';
import {FormGroup, FormBuilder, Validators} from '@angular/forms';

@Component({
  selector   : 'pastebin-root',
  templateUrl: './app.component.html',
  styleUrls  : ['./app.component.css']
})
export class AppComponent implements OnInit {
  form: FormGroup;
  formErrors = {
    'title': ''
  };
  validationMessages = {
    'title': {
      'required': 'Title is required.'
    }
  };

  constructor(private fb: FormBuilder) {
  }

  ngOnInit(): void {
    this.buildForm();
  }

  onSubmit(form: any): void {
    // somehow touch all elements so onValueChanged will generate correct error messages

    this.onValueChanged();
    if (this.form.valid) {
      console.log(form);
    }
  }

  buildForm(): void {
    this.form = this.fb.group({
      'title': ['', Validators.required]
    });
    this.form.valueChanges
      .subscribe(data => this.onValueChanged(data));
  }

  onValueChanged(data?: any) {
    if (!this.form) {
      return;
    }

    const form = this.form;

    for (const field in this.formErrors) {
      if (!this.formErrors.hasOwnProperty(field)) {
        continue;
      }

      // clear previous error message (if any)
      this.formErrors[field] = '';
      const control = form.get(field);
      if (control && control.touched && !control.valid) {
        const messages = this.validationMessages[field];
        for (const key in control.errors) {
          if (!control.errors.hasOwnProperty(key)) {
            continue;
          }
          this.formErrors[field] += messages[key] + ' ';
        }
      }
    }
  }
}
Giedrius Kiršys
źródło

Odpowiedzi:

147

Następująca funkcja powtarza się za pomocą kontrolek w grupie formularzy i delikatnie je dotyka. Ponieważ pole kontrolne jest obiektem, kod wywołuje Object.values ​​() w polu kontrolnym grupy formularzy.

  /**
   * Marks all controls in a form group as touched
   * @param formGroup - The form group to touch
   */
  private markFormGroupTouched(formGroup: FormGroup) {
    (<any>Object).values(formGroup.controls).forEach(control => {
      control.markAsTouched();

      if (control.controls) {
        this.markFormGroupTouched(control);
      }
    });
  }
masterwok
źródło
19
to niestety nie działa w Internet Explorerze :( po prostu zmień (<any>Object).values(formGroup.controls)na Object.keys(formGroup.controls).map(x => formGroup.controls[x])(ze stackoverflow.com/questions/42830257/… )
moi_meme
1
To była dla mnie ogromna pomoc przy użyciu FormGroup i FormControl i zastanawiałem się, jak pokazać użytkownikowi, że nie dotknął wymaganego pola. Dziękuję Ci.
NAMS
@NAMS nie ma problemu! Cieszę się, że pomogło:]
masterwok
4
+1 Tylko jeden drobny problem w części rekurencyjnej. Już wykonujesz iterację controlsna początku funkcji, więc zamiast tego powinno wyglądać następująco:if (control.controls) { markFormGroupTouched(control); }
zurfyx
3
touchedoznacza po prostu, że wejście zostało raz zamazane. Aby pojawiły się błędy, musiałem również wywołać updateValueAndValidity()moje kontrolki.
adamdport
108

Od Angular 8/9 możesz po prostu użyć

this.form.markAllAsTouched();

Aby oznaczyć kontrolkę i jej elementy podrzędne jako dotknięte.

AbstractControl doc

hovado
źródło
2
To powinna być akceptowana odpowiedź dla tych, którzy używają Angular 8.
Jacob Roberts
1
To prostsze i bardziej przejrzyste rozwiązanie.
HDJEMAI
1
jest to zalecane rozwiązanie dla kątowników 8 i wyższych, świetnie!
Duc Nguyen
1
Jeśli wydaje się, że to nie działa dla niektórych kontrolek, prawdopodobnie nie ma ich w tej FormGroup.
Noumenon
12

Odnośnie odpowiedzi @ masterwork. Wypróbowałem to rozwiązanie, ale wystąpił błąd, gdy funkcja próbowała rekursywnie kopać wewnątrz FormGroup, ponieważ w tym wierszu jest przekazywany argument FormControl zamiast FormGroup:

control.controls.forEach(c => this.markFormGroupTouched(c));

Oto moje rozwiązanie

markFormGroupTouched(formGroup: FormGroup) {
 (<any>Object).values(formGroup.controls).forEach(control => {
   if (control.controls) { // control is a FormGroup
     markFormGroupTouched(control);
   } else { // control is a FormControl
     control.markAsTouched();
   }
 });
}
GarryOne
źródło
8

Zapętlenie przez kontrolki formularza i oznaczenie ich jako dotkniętych również zadziała:

for(let i in this.form.controls)
    this.form.controls[i].markAsTouched();
jsertx
źródło
1
Dzięki kolego, twoje rozwiązanie jest całkiem dobre, jedyne, co dodałbym, ponieważ tslint narzeka, to: for (const i in this.form.controls) {if (this.form.controls [i]) {this.form.controls [i ] .markAsTouched (); }}
Avram Virgil
1
To nie działa jeśli formGroupzawiera inne formGroups
adamdport
3

To jest moje rozwiązanie

      static markFormGroupTouched (FormControls: { [key: string]: AbstractControl } | AbstractControl[]): void {
        const markFormGroupTouchedRecursive = (controls: { [key: string]: AbstractControl } | AbstractControl[]): void => {
          _.forOwn(controls, (c, controlKey) => {
            if (c instanceof FormGroup || c instanceof FormArray) {
              markFormGroupTouchedRecursive(c.controls);
            } else {
              c.markAsTouched();
            }
          });
        };
        markFormGroupTouchedRecursive(FormControls);
      }
Aladdin Mhemed
źródło
2

Miałem ten problem, ale znalazłem „właściwy” sposób, aby to zrobić, mimo że nie ma go w żadnym samouczku dotyczącym Angulara, jaki kiedykolwiek znalazłem.

W formtagu HTML dodaj tę samą zmienną odniesienia szablonu #myVariable='ngForm'( zmienną „hashtag”), której używają przykłady formularzy sterowanych szablonami, oprócz tego, czego używają przykłady formularzy reaktywnych:

<form [formGroup]="myFormGroup" #myForm="ngForm" (ngSubmit)="submit()">

Teraz masz dostęp myForm.submittedw szablonie, do którego możesz użyć zamiast (lub dodatkowo) myFormGroup.controls.X.touched:

<div *ngIf="myForm.submitted" class="text-error"> <span *ngIf="myFormGroup.controls.myFieldX.errors?.badDate">invalid date format</span> <span *ngIf="myFormGroup.controls.myFieldX.errors?.isPastDate">date cannot be in the past.</span> </div>

Wiedz, że myForm.form === myFormGroupto prawda ... o ile nie zapomnisz ="ngForm"części. Jeśli użyjesz #myFormsamego, nie zadziała, ponieważ var zostanie ustawiony na HtmlElement zamiast dyrektywy sterującej tym elementem.

Wiedz o tym myFormGroup jest widoczny na maszynie kodu komponentu w jednej z reaktywnych form ćwiczeń, ale myFormnie jest, chyba że przechodzą go poprzez wywołanie metody, jak submit(myForm)się submit(myForm: NgForm): void {...}. (Uwaga NgFormw maszynopisie jest zapisana wielkimi literami, a w HTML - wielbłądem).

Ron Newcomb
źródło
1
onSubmit(form: any): void {
  if (!this.form) {
    this.form.markAsTouched();
    // this.form.markAsDirty(); <-- this can be useful 
  }
}
Vlado Tesanovic
źródło
Właśnie tego próbowałem i jakoś nie dotyka to elementów formularzy potomnych. Musiałem napisać pętlę, która ręcznie oznacza wszystkie elementy podrzędne. Czy masz jakąś wskazówkę, dlaczego markAsTouched()nie dotknął elementów potomnych?
Giedrius Kiršys
Jakich wersji kątowych używasz?
Vlado Tesanovic
Wersja Angular to 2.1.0
Giedrius Kiršys
1
Wygląda na to, że znalazłem dlaczego markAsTouched()nie oznaczać elementów potomnych - github.com/angular/angular/issues/11774 . TL; DR: To nie jest błąd.
Giedrius Kiršys
1
Tak, teraz pamiętam. Możesz wyłączyć przycisk przesyłania, jeśli formularz jest nieprawidłowy, <button [disable] = "! This.form"> Prześlij </button>
Vlado Tesanovic
1

Napotkałem ten sam problem, ale nie chcę „zanieczyszczać” moich komponentów kodem, który to obsługuje. Zwłaszcza, że ​​potrzebuję tego w wielu formach i nie chcę przy różnych okazjach powtarzać kodu.

W ten sposób stworzyłem dyrektywę (korzystając z dotychczas zamieszczonych odpowiedzi). Dyrektywa ozdabia -Method onSubmitNgForm: Jeśli formularz jest nieprawidłowy, oznacza wszystkie pola jako dotknięte i przerywa przesyłanie. W przeciwnym razie zwykła metoda onSubmit jest wykonywana normalnie.

import {Directive, Host} from '@angular/core';
import {NgForm} from '@angular/forms';

@Directive({
    selector: '[appValidateOnSubmit]'
})
export class ValidateOnSubmitDirective {

    constructor(@Host() form: NgForm) {
        const oldSubmit = form.onSubmit;

        form.onSubmit = function (): boolean {
            if (form.invalid) {
                const controls = form.controls;
                Object.keys(controls).forEach(controlName => controls[controlName].markAsTouched());
                return false;
            }
            return oldSubmit.apply(form, arguments);
        };
    }
}

Stosowanie:

<form (ngSubmit)="submit()" appValidateOnSubmit>
    <!-- ... form controls ... -->
</form>
Jankes
źródło
1

To jest kod, którego faktycznie używam.

validateAllFormFields(formGroup: any) {
    // This code also works in IE 11
    Object.keys(formGroup.controls).forEach(field => {
        const control = formGroup.get(field);

        if (control instanceof FormControl) {
            control.markAsTouched({ onlySelf: true });
        } else if (control instanceof FormGroup) {               
            this.validateAllFormFields(control);
        } else if (control instanceof FormArray) {  
            this.validateAllFormFields(control);
        }
    });
}    

Leonardo Moreira
źródło
1

Ten kod działa dla mnie:

markAsRequired(formGroup: FormGroup) {
  if (Reflect.getOwnPropertyDescriptor(formGroup, 'controls')) {
    (<any>Object).values(formGroup.controls).forEach(control => {
      if (control instanceof FormGroup) {
        // FormGroup
        markAsRequired(control);
      }
      // FormControl
      control.markAsTouched();
    });
  }
}
Dionis Oros
źródło
1

Rozwiązanie bez rekursji

Dla tych, którzy martwią się wydajnością, wymyśliłem rozwiązanie, które nie używa rekursji, chociaż nadal iteruje po wszystkich kontrolkach na wszystkich poziomach.

 /**
  * Iterates over a FormGroup or FormArray and mark all controls as
  * touched, including its children.
  *
  * @param {(FormGroup | FormArray)} rootControl - Root form
  * group or form array
  * @param {boolean} [visitChildren=true] - Specify whether it should
  * iterate over nested controls
  */
  public markControlsAsTouched(rootControl: FormGroup | FormArray,
    visitChildren: boolean = true) {

    let stack: (FormGroup | FormArray)[] = [];

    // Stack the root FormGroup or FormArray
    if (rootControl &&
      (rootControl instanceof FormGroup || rootControl instanceof FormArray)) {
      stack.push(rootControl);
    }

    while (stack.length > 0) {
      let currentControl = stack.pop();
      (<any>Object).values(currentControl.controls).forEach((control) => {
        // If there are nested forms or formArrays, stack them to visit later
        if (visitChildren &&
            (control instanceof FormGroup || control instanceof FormArray)
           ) {
           stack.push(control);
        } else {
           control.markAsTouched();
        }
      });
    }
  }

To rozwiązanie działa zarówno z FormGroup, jak i FormArray.

Możesz się nim bawić tutaj: kątowy znak jako dotknięty

Arthur Silva
źródło
@VladimirPrudnikov Problem polega na tym, że rekurencyjne wywołanie funkcji wiąże się zwykle z większym narzutem. Z tego powodu procesor poświęci więcej czasu na obsługę stosu wywołań. Podczas korzystania z pętli procesor będzie spędzał większość czasu na wykonywaniu samego algorytmu. Zaletą rekurencji jest to, że kod jest zwykle bardziej czytelny. Więc jeśli wydajność nie jest problemem, powiedziałbym, że możesz trzymać się rekursji.
Arthur Silva
„Przedwczesna optymalizacja jest źródłem wszelkiego zła”.
Dem Pilafian
@DemPilafian Zgadzam się z wyceną. Jednak tutaj nie ma to zastosowania, ponieważ jeśli ktoś podejdzie do tego wątku, będzie mógł bezpłatnie uzyskać zoptymalizowane rozwiązanie (bez czasu na to). I przy okazji, w moim przypadku naprawdę miałem powody, aby to zoptymalizować =)
Arthur Silva
1

zgodnie z @masterwork

kod maszynowy dla wersji kątowej 8

private markFormGroupTouched(formGroup: FormGroup) {
    (Object as any).values(formGroup.controls).forEach(control => {
      control.markAsTouched();
      if (control.controls) {
        this.markFormGroupTouched(control);
      }
    });   }
Aupr
źródło
0

Oto jak to robię. Nie chcę, aby pola błędów były wyświetlane, dopóki nie zostanie naciśnięty przycisk przesyłania (lub formularz).

import {FormBuilder, FormGroup, Validators} from "@angular/forms";

import {OnInit} from "@angular/core";

export class MyFormComponent implements OnInit {
  doValidation = false;
  form: FormGroup;


  constructor(fb: FormBuilder) {
    this.form = fb.group({
      title: ["", Validators.required]
    });

  }

  ngOnInit() {

  }
  clickSubmitForm() {
    this.doValidation = true;
    if (this.form.valid) {
      console.log(this.form.value);
    };
  }
}

<form class="form-horizontal" [formGroup]="form" >
  <input type="text" class="form-control" formControlName="title">
  <div *ngIf="form.get('title').hasError('required') && doValidation" class="alert alert-danger">
            title is required
        </div>
  <button (click)="clickSubmitForm()">Submit</button>
</form>

brando
źródło
Wygląda na to, że z czasem może stać się ciężki podczas dodawania nowych reguł walidacji. Ale zrozumiałem.
Giedrius Kiršys
0

Całkowicie rozumiem frustrację OP. Używam następujących:

Funkcja użytkowa :

/**
 * Determines if the given form is valid by touching its controls 
 * and updating their validity.
 * @param formGroup the container of the controls to be checked
 * @returns {boolean} whether or not the form was invalid.
 */
export function formValid(formGroup: FormGroup): boolean {
  return !Object.keys(formGroup.controls)
    .map(controlName => formGroup.controls[controlName])
    .filter(control => {
      control.markAsTouched();
      control.updateValueAndValidity();
      return !control.valid;
    }).length;
}

Użycie :

onSubmit() {
  if (!formValid(this.formGroup)) {
    return;
  }
  // ... TODO: logic if form is valid.
}

Zauważ, że ta funkcja nie obsługuje jeszcze zagnieżdżonych kontrolek.

Stephen Paul
źródło
0

Zobacz ten klejnot . Jak dotąd najbardziej eleganckie rozwiązanie, jakie widziałem.

Pełny kod

import { Injectable } from '@angular/core';
import { FormGroup } from '@angular/forms';

const TOUCHED = 'markAsTouched';
const UNTOUCHED = 'markAsUntouched';
const DIRTY = 'markAsDirty';
const PENDING = 'markAsPending';
const PRISTINE = 'markAsPristine';

const FORM_CONTROL_STATES: Array<string> = [TOUCHED, UNTOUCHED, DIRTY, PENDING, PRISTINE];

@Injectable({
  providedIn: 'root'
})
export class FormStateService {

  markAs (form: FormGroup, state: string): FormGroup {
    if (FORM_CONTROL_STATES.indexOf(state) === -1) {
      return form;
    }

    const controls: Array<string> = Object.keys(form.controls);

    for (const control of controls) {
      form.controls[control][state]();
    }

    return form;
  }

  markAsTouched (form: FormGroup): FormGroup {
    return this.markAs(form, TOUCHED);
  }

  markAsUntouched (form: FormGroup): FormGroup {
    return this.markAs(form, UNTOUCHED);
  }

  markAsDirty (form: FormGroup): FormGroup {
    return this.markAs(form, DIRTY);
  }

  markAsPending (form: FormGroup): FormGroup {
    return this.markAs(form, PENDING);
  }

  markAsPristine (form: FormGroup): FormGroup {
    return this.markAs(form, PRISTINE);
  }
}
David Votrubec
źródło
0
    /**
    * Marks as a touched
    * @param { FormGroup } formGroup
    *
    * @return {void}
    */
    markFormGroupTouched(formGroup: FormGroup) {
        Object.values(formGroup.controls).forEach((control: any) => {

            if (control instanceof FormControl) {
                control.markAsTouched();
                control.updateValueAndValidity();

            } else if (control instanceof FormGroup) {
                this.markFormGroupTouched(control);
            }
        });
    }
Rahul Rathore
źródło
0

Widok:

<button (click)="Submit(yourFormGroup)">Submit</button>   

API

Submit(form: any) {
  if (form.status === 'INVALID') {
      for (let inner in details.controls) {
           details.get(inner).markAsTouched();
       }
       return false; 
     } 
     // as it return false it breaks js execution and return 
panatoni
źródło
0

Zrobiłem wersję z pewnymi zmianami w przedstawionych odpowiedziach, dla tych, którzy używają wersji starszej niż wersja 8 angulara, chciałbym się nią podzielić z tymi, którzy są przydatni.

Funkcja użytkowa:

import {FormControl, FormGroup} from "@angular/forms";

function getAllControls(formGroup: FormGroup): FormControl[] {
  const controls: FormControl[] = [];
  (<any>Object).values(formGroup.controls).forEach(control => {
    if (control.controls) { // control is a FormGroup
      const allControls = getAllControls(control);
      controls.push(...allControls);
    } else { // control is a FormControl
      controls.push(control);
    }
  });
  return controls;
}

export function isValidForm(formGroup: FormGroup): boolean {
  return getAllControls(formGroup)
    .filter(control => {
      control.markAsTouched();
      return !control.valid;
    }).length === 0;
}

Stosowanie:

onSubmit() {
 if (this.isValidForm()) {
   // ... TODO: logic if form is valid
 }
}
Pedro Bacchini
źródło