Reaktive Formen - Markieren Sie Felder als berührt


83

Ich habe Probleme herauszufinden, wie alle Felder des Formulars als berührt markiert werden können. Das Hauptproblem ist, dass, wenn ich keine Felder berühre und versuche, einen Formularüberprüfungsfehler einzureichen, dieser nicht angezeigt wird. Ich habe einen Platzhalter für diesen Code in meinem Controller.
Meine Idee ist einfach:

  1. Benutzer klickt auf die Schaltfläche "Senden"
  2. Alle Felder werden als berührt markiert
  3. Fehlerformatierer führt erneut aus und zeigt Validierungsfehler an

Wenn jemand eine andere Idee hat, wie Fehler beim Senden angezeigt werden können, ohne eine neue Methode zu implementieren, teilen Sie diese bitte mit. Vielen Dank!


Meine vereinfachte Form:

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

Und mein Controller:

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] + ' ';
        }
      }
    }
  }
}

Antworten:


145

Die folgende Funktion wird durch Steuerelemente in einer Formulargruppe wiederholt und berührt sie sanft. Da das Steuerelementfeld ein Objekt ist, ruft der Code Object.values ​​() im Steuerfeld der Formulargruppe auf.

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

18
Dies funktioniert leider nicht in Internet Explorer :( Wechseln Sie einfach (<any>Object).values(formGroup.controls)zu Object.keys(formGroup.controls).map(x => formGroup.controls[x])(von stackoverflow.com/questions/42830257/… )
moi_meme

1
Dies war eine große Hilfe für mich, FormGroup und FormControl zu verwenden und mich zu fragen, wie ich dem Benutzer zeigen kann, dass er ein erforderliches Feld nicht berührt hat. Vielen Dank.
NAMS

@NAMS kein Problem! Ich bin froh, dass es geholfen hat:]
Masterwok

4
+1 Nur ein kleines Problem im rekursiven Teil. Sie iterieren bereits controlszu Beginn der Funktion, daher sollte es stattdessen Folgendes sein:if (control.controls) { markFormGroupTouched(control); }
zurfyx

3
touchedbedeutet nur, dass die Eingabe einmal unscharf war. Um Fehler anzuzeigen, musste ich auch updateValueAndValidity()meine Steuerelemente aufrufen .
Adamdport

102

Ab Angular 8/9 können Sie einfach verwenden

this.form.markAllAsTouched();

Markieren eines Steuerelements und seiner untergeordneten Steuerelemente als berührt.

AbstractControl doc


2
Dies sollte die akzeptierte Antwort für diejenigen sein, die Angular 8 verwenden.
Jacob Roberts

1
Dies ist eine einfachere und sauberere Lösung.
HDJEMAI

1
Dies ist eine empfohlene Lösung für Winkel 8 und höher, großartig!
Duc Nguyen

1
Wenn dies bei einigen Steuerelementen nicht funktioniert, befinden sie sich wahrscheinlich nicht in dieser FormGroup.
Noumenon

11

In Bezug auf die Antwort von @ masterwork. Ich habe diese Lösung ausprobiert, aber es ist ein Fehler aufgetreten, als die Funktion versucht hat, rekursiv in einer FormGroup zu graben, da in dieser Zeile ein FormControl-Argument anstelle einer FormGroup übergeben wird:

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

Hier ist meine Lösung

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


8

Das Durchlaufen der Formularsteuerelemente und das Markieren als berührt würde ebenfalls funktionieren:

for(let i in this.form.controls)
    this.form.controls[i].markAsTouched();

1
Vielen Dank, Kumpel, deine Lösung ist ziemlich gut. Das einzige, was ich hinzufügen würde, weil tslint sich beschwert, ist Folgendes: for (const i in this.form.controls) {if (this.form.controls [i]) {this.form.controls [i ] .markAsTouched (); }}
Avram Virgil

1
Dies funktioniert nicht, wenn Ihre formGroupandere formGroups
adamdport

3

Das ist meine Lösung

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

2

Ich hatte dieses Problem, fand aber den "richtigen" Weg, obwohl es in keinem Angular-Tutorial enthalten war, das ich jemals gefunden habe.

Fügen Sie in Ihrem HTML-Code auf dem formTag dieselbe Vorlagenreferenzvariable #myVariable='ngForm'('Hashtag'-Variable) hinzu, die in den Beispielen für vorlagengesteuerte Formulare verwendet wird, zusätzlich zu den Beispielen für reaktive Formulare:

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

Jetzt haben Sie Zugriff auf myForm.submitteddie Vorlage, die Sie anstelle (oder zusätzlich zu) verwenden können 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>

Wisse, dass myForm.form === myFormGroupdas wahr ist ... solange du den ="ngForm"Teil nicht vergisst . Wenn Sie es #myFormalleine verwenden, funktioniert es nicht, da die Variable auf das HTML-Element gesetzt wird, anstatt auf die Direktive, die dieses Element steuert.

Wissen Sie, dass myFormGroupin Ihrer Komponente Typoskript Code gemäß dem Reactive Forms Tutorials sichtbar ist, aber myFormnicht, wenn Sie es in durch einen Methodenaufruf übergeben, wie submit(myForm)zu submit(myForm: NgForm): void {...}. (Hinweis NgFormist im Typoskript in Titelkappen, in HTML jedoch in Kamelbuchstaben.)


1
onSubmit(form: any): void {
  if (!this.form) {
    this.form.markAsTouched();
    // this.form.markAsDirty(); <-- this can be useful 
  }
}

Ich habe es gerade versucht und irgendwie berührt es keine untergeordneten Formelemente. Musste eine Schleife schreiben, die alle untergeordneten Elemente manuell markiert. Haben Sie eine Ahnung, warum markAsTouched()untergeordnete Elemente nicht berührt werden?
Giedrius Kiršys

Welche Winkelversionen verwenden Sie?
Vlado Tesanovic

Winkelversion ist 2.1.0
Giedrius Kiršys

1
Sieht so aus, als hätte ich herausgefunden, warum untergeordnetemarkAsTouched() Elemente nicht markiert werden - github.com/angular/angular/issues/11774 . TL; DR: Es ist kein Fehler.
Giedrius Kiršys

1
Ja, ich erinnere mich jetzt. Sie können die Schaltfläche "Senden" deaktivieren, wenn das Formular nicht gültig ist. <Button [disable] = "! This.form"> Senden </ button>
Vlado Tesanovic

1

Ich bin auf dasselbe Problem gestoßen, möchte meine Komponenten jedoch nicht mit Code "verschmutzen", der dies behandelt. Zumal ich dies in vielen Formen brauche und den Code nicht bei verschiedenen Gelegenheiten wiederholen möchte.

Daher habe ich eine Richtlinie erstellt (unter Verwendung der bisher veröffentlichten Antworten). Die Direktive schmückt die Methode von onSubmitNgForm: Wenn das Formular ungültig ist, werden alle Felder als berührt markiert und die Übermittlung abgebrochen. Ansonsten wird die übliche onSubmit-Methode normal ausgeführt.

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

Verwendung:

<form (ngSubmit)="submit()" appValidateOnSubmit>
    <!-- ... form controls ... -->
</form>

1

Dies ist der Code, den ich tatsächlich verwende.

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


1

Dieser Code funktioniert für mich:

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

1

Eine Lösung ohne Rekursion

Für diejenigen, die sich Sorgen um die Leistung machen, habe ich eine Lösung gefunden, die keine Rekursion verwendet, obwohl sie immer noch alle Steuerelemente in allen Ebenen durchläuft.

 /**
  * 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();
        }
      });
    }
  }

Diese Lösung funktioniert sowohl mit FormGroup als auch mit FormArray.

Hier können Sie damit herumspielen: Winkelmarkierung als berührt


@VladimirPrudnikov Das Problem ist, dass beim rekursiven Aufrufen einer Funktion normalerweise mehr Overhead verbunden ist. Aus diesem Grund verbringt die CPU mehr Zeit mit der Bearbeitung des Aufrufstapels. Bei Verwendung von Schleifen verbringt die CPU die meiste Zeit damit, den Algorithmus selbst auszuführen. Der Vorteil der Rekursion besteht darin, dass der Code normalerweise besser lesbar ist. Wenn also die Leistung kein Problem darstellt, würde ich sagen, dass Sie bei der Rekursion bleiben könnten.
Arthur Silva

"Vorzeitige Optimierung ist die Wurzel allen Übels."
Dem Pilafian

@ DemPilafian Ich stimme dem Zitat zu. Dies gilt hier jedoch nicht, da jemand, der auf diesen Thread zugreift, kostenlos eine optimierte Lösung erhalten kann (ohne Zeitaufwand). Und übrigens, in meinem Fall hatte ich wirklich Gründe, es zu optimieren =)
Arthur Silva

1

gemäß @masterwork

Typoskript-Code für die Winkelversion 8

private markFormGroupTouched(formGroup: FormGroup) {
    (Object as any).values(formGroup.controls).forEach(control => {
      control.markAsTouched();
      if (control.controls) {
        this.markFormGroupTouched(control);
      }
    });   }

0

Hier ist, wie ich es mache. Ich möchte nicht, dass die Fehlerfelder angezeigt werden, bis die Senden-Schaltfläche gedrückt wird (oder das Formular berührt wird).

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>


Dieser scheint mit der Zeit sehr schwer zu werden, wenn neue Validierungsregeln hinzugefügt werden. Aber ich habe verstanden.
Giedrius Kiršys

0

Ich verstehe die Frustration des OP vollkommen. Ich benutze folgendes:

Dienstprogrammfunktion :

/**
 * 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;
}

Verwendung :

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

Beachten Sie, dass diese Funktion noch nicht für verschachtelte Steuerelemente geeignet ist.


0

Sehen Sie dieses Juwel . Bisher die eleganteste Lösung, die ich je gesehen habe.

Vollständiger Code

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

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

0

Aussicht:

<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 

0

Ich habe eine Version mit einigen Änderungen in den vorgestellten Antworten erstellt. Für diejenigen, die Versionen verwenden, die älter als Version 8 des Winkels sind, möchte ich sie mit denen teilen, die nützlich sind.

Dienstprogrammfunktion:

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

Verwendung:

onSubmit() {
 if (this.isValidForm()) {
   // ... TODO: logic if form is valid
 }
}
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.