web-dev-qa-db-fra.com

Formulaires réactifs - marquer les champs comme touchés

J'ai du mal à savoir comment marquer tous les champs de formulaire comme étant touchés. Le problème principal est que si je ne touche pas aux champs et que je tente de soumettre le formulaire, une erreur de validation n’est pas affichée. J'ai un espace réservé pour ce morceau de code dans mon contrôleur.
Mon idée est simple:

  1. l'utilisateur clique sur le bouton d'envoi
  2. tous les champs marqués comme touché
  3. error formateur relance et affiche les erreurs de validation

Si quelqu'un a une autre idée sur la manière d'afficher les erreurs lors de la soumission, sans implémenter de nouvelle méthode, merci de les partager. Merci!


Mon formulaire simplifié:

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

Et mon contrôleur:

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] + ' ';
        }
      }
    }
  }
}
44
Giedrius Kiršys

La fonction suivante revient à travers des contrôles dans un groupe de formulaires et les touche doucement. Le champ de contrôles étant un objet, le code appelle Object.values ​​() dans le champ de contrôle du groupe de formulaires.

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

Dans Angular 8 + vous pouvez simplement utiliser

this.form.markAllAsTouched();

pour marquer un contrôle et ses contrôles descendants comme touché.

10
hovado

En ce qui concerne la réponse de @ masterwork. J'ai essayé cette solution, mais une erreur s'est produite lorsque la fonction a essayé de creuser, de manière récursive, à l'intérieur d'un groupe de formulaires, car un argument FormControl était transmis à cette ligne à la place d'un groupe de formulaires:

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

Voici ma solution

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

Faire une boucle à travers les contrôles de formulaire et les marquer comme touchés toucherait également:

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

De Angular v8, vous l'avez intégré à l'aide de la méthode markAllAsTouched.

Par exemple, vous pourriez l'utiliser comme

form.markAllAsTouched();

Voir la documentation officielle: https://angular.io/api/forms/AbstractControl#markallastouched

3
ylerjen

C'est ma solution

      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);
      }
3
Aladdin Mhemed

J'ai eu ce problème, mais j'ai trouvé la façon "correcte" de le faire, même si ce tutoriel ne figure dans aucun tutoriel Angular que j'ai trouvé auparavant.

Dans votre code HTML, sur la balise form, ajoutez la même variable de référence de modèle #myVariable='ngForm' (Variable 'hashtag') utilisée par les exemples de formulaires basés sur des modèles, en plus de celle utilisée par les exemples de formulaires réactifs :

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

Vous avez maintenant accès à myForm.submitted Dans le modèle que vous pouvez utiliser à la place de (ou en plus de) 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>

Sachez que myForm.form === myFormGroup Est vrai ... à condition de ne pas oublier la partie ="ngForm". Si vous utilisez #myForm Seul, cela ne fonctionnera pas car la variable var sera définie sur HtmlElement au lieu de la directive qui commande cet élément.

Sachez que myFormGroup est visible dans le code TypeScript de votre composant conformément aux didacticiels de Reactive Forms, mais que myForm ne l’est pas, à moins que vous ne le transmettiez via un appel de méthode, comme submit(myForm) à submit(myForm: NgForm): void {...}. (Notez que NgForm est en majuscule dans le TypeScript, mais le camel en HTML.)

2
Ron Newcomb

Voici comment je le fais. Je ne veux pas que les champs d'erreur s'affichent tant que le bouton d'envoi n'a pas été appuyé (ou que le formulaire n'a pas été touché).

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>
1
brando
onSubmit(form: any): void {
  if (!this.form) {
    this.form.markAsTouched();
    // this.form.markAsDirty(); <-- this can be useful 
  }
}
1
Vlado Tesanovic

Ce code fonctionne pour moi:

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

J'ai rencontré le même problème, mais je ne veux pas "polluer" mes composants avec un code qui gère cela. Surtout que j'en ai besoin sous de nombreuses formes et que je ne veux pas répéter le code à plusieurs reprises.

J'ai donc créé une directive (en utilisant les réponses affichées jusqu'à présent). La directive décore la méthode onSubmit de NgForm: si le formulaire est invalide, tous les champs sont marqués et la soumission est annulée. Sinon, la méthode habituelle onSubmit s'exécute normalement.

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

Usage:

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

C'est le code que j'utilise réellement.

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

Une solution sans récursion

Pour ceux qui s'inquiètent des performances, j'ai proposé une solution qui n'utilise pas de récursivité, bien qu'elle répète tous les contrôles à tous les niveaux.

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

Cette solution fonctionne à la fois avec FormGroup et également avec FormArray.

Vous pouvez jouer avec cela ici: angular-mark-as-touched

1
Arthur Silva

Voir cette gemme . Jusqu'à présent, la solution la plus élégante que j'ai vue.

Code complet

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

Je comprends tout à fait la frustration du PO. J'utilise les éléments suivants:

fonction utilitaire:

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

sage:

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

Notez que cette fonction ne prend pas encore en charge les contrôles imbriqués.

0
Stephen Paul