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:
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] + ' ';
}
}
}
}
}
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);
}
});
}
Dans Angular 8 + vous pouvez simplement utiliser
this.form.markAllAsTouched();
pour marquer un contrôle et ses contrôles descendants comme touché.
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();
}
});
}
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();
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
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);
}
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.)
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>
onSubmit(form: any): void {
if (!this.form) {
this.form.markAsTouched();
// this.form.markAsDirty(); <-- this can be useful
}
}
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();
});
}
}
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>
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);
}
});
}
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
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);
}
}
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.