Реактивные формы - пометить поля как затронутые
У меня возникли проблемы с поиском того, как пометить все поля формы как затронутые. Основная проблема заключается в том, что если я не касаюсь полей и пытаюсь отправить форму - ошибка проверки не отображается. У меня есть место для этой части кода в моем контроллере.
Моя идея проста:
- кнопка отправки кликов пользователем
- все поля отмечены как тронутые
- ошибка форматирования ошибок и отображает ошибки проверки
Если у кого-то есть другая идея, как показывать ошибки в submit, не применяя новый метод - пожалуйста, поделитесь ими. Благодарю!
Моя упрощенная форма:
<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>
И мой контроллер:
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] + ' ';
}
}
}
}
}
Ответы
Ответ 1
Следующая функция проходит через элементы управления в группе форм и осторожно касается их. Поскольку поле элементов управления является объектом, код вызывает Object.values() в поле элемента управления группы форм.
/**
* 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);
}
});
}
Ответ 2
В Angular 8+ вы можете просто использовать
this.form.markAllAsTouched();
пометить элемент управления и его дочерние элементы управления как прикосновенные.
Ответ 3
Относительно ответа @masterwork. Я пробовал это решение, но у меня возникла ошибка, когда функция пыталась копать, рекурсивно, внутри FormGroup, потому что в этой строке вместо FormGroup передается аргумент FormControl:
control.controls.forEach(c => this.markFormGroupTouched(c));
Вот мое решение
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();
}
});
}
Ответ 4
Также будет работать цикл с помощью элементов управления формы и их маркировка как затронутая:
for(let i in this.form.controls)
this.form.controls[i].markAsTouched();
Ответ 5
В Angular v8 это встроено с помощью метода markAllAsTouched
.
Например, вы можете использовать его как
form.markAllAsTouched();
См. Официальный документ: https://angular.io/api/forms/AbstractControl#markallastouched
Ответ 6
Это мое решение
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);
}
Ответ 7
У меня была эта проблема, но я нашел "правильный" способ сделать это, несмотря на то, что она не была в любом уральском учебнике, который я когда-либо находил.
В своем HTML в теге form
добавьте ту же #myVariable='ngForm'
переменную шаблона #myVariable='ngForm'
(переменная hashtag), которую используют примеры шаблонов Driven Forms, в дополнение к тем, что используют примеры Reactive Forms:
<form [formGroup]="myFormGroup" #myForm="ngForm" (ngSubmit)="submit()">
Теперь у вас есть доступ к myForm.submitted
в шаблоне, который вы можете использовать вместо (или в дополнение к) 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>
Знайте, что myForm.form === myFormGroup
верен... до тех пор, пока вы не забудете часть ="ngForm"
. Если вы используете только #myForm
, это не сработает, потому что var будет установлен в HtmlElement вместо Директивы, управляющей этим элементом.
Знайте, что myFormGroup
видна в вашем тексте кода компонента в учебниках Reactive Forms, но myForm
не является, если вы не передаете его через вызов метода, например submit(myForm)
для submit(myForm: NgForm): void {...}
. (Обратите внимание, что NgForm
находится в заглавных NgForm
в машинописном тексте, но в верблюжьем случае в HTML.)
Ответ 8
onSubmit(form: any): void {
if (!this.form) {
this.form.markAsTouched();
// this.form.markAsDirty(); <-- this can be useful
}
}
Ответ 9
Вот как я это делаю. Я не хочу, чтобы поля ошибок отображались до тех пор, пока не будет нажата кнопка отправки (или коснуться формы).
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);
};
}
}
Ответ 10
Я столкнулся с той же проблемой, но я не хочу "загрязнять" мои компоненты кодом, который обрабатывает это. Тем более, что мне это нужно во многих формах, и я не хочу повторять код в разных случаях.
Таким образом, я создал директиву (используя ответы, опубликованные до сих пор). Директива украшает NgForm onSubmit
-Method: если форма недействительна, она помечает все поля как коснувшиеся и прерывает отправку. В противном случае обычный onSubmit -Method выполняется нормально.
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);
};
}
}
Применение:
<form (ngSubmit)="submit()" appValidateOnSubmit>
<!-- ... form controls ... -->
</form>
Ответ 11
Это код, который я фактически использую.
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);
}
});
}
Ответ 12
Этот код работает для меня:
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();
});
}
}
Ответ 13
Решение без рекурсии
Для тех, кто беспокоится о производительности, я придумала решение, которое не использует рекурсию, хотя оно все еще перебирает все элементы управления на всех уровнях.
/**
* 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();
}
});
}
}
Это решение работает как с FormGroup, так и с FormArray.
Вы можете поиграть с этим здесь: angular-mark-as-touch
Ответ 14
Я полностью понимаю разочарование ФП. Я использую следующее:
Полезная функция:
/**
* 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;
}
Использование:
onSubmit() {
if (!formValid(this.formGroup)) {
return;
}
// ... TODO: logic if form is valid.
}
Обратите внимание, что эта функция еще не поддерживает вложенные элементы управления.
Ответ 15
Смотрите этот драгоценный камень. Пока что самое элегантное решение, которое я видел.
Полный код
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);
}
}
Ответ 16
Вид:
<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
Ответ 17
/**
* 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);
}
});
}