Реактивные формы - отметьте поля как затронутые


88

Мне не удается узнать, как отметить все поля формы как затронутые. Основная проблема в том, что если я не касаюсь полей и не пытаюсь отправить форму - ошибка проверки не отображается. У меня есть заполнитель для этого фрагмента кода в моем контроллере.
Моя идея проста:

  1. пользователь нажимает кнопку отправки
  2. все поля отмечены как затронутые
  3. программа форматирования ошибок повторно запускается и отображает ошибки проверки

Если у кого-то есть другие идеи, как показывать ошибки при отправке без реализации нового метода - поделитесь ими. Благодаря!


Моя упрощенная форма:

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

Ответы:


147

Следующая функция рекурсивно проходит через элементы управления в группе форм и мягко касается их. Поскольку поле элементов управления является объектом, код вызывает 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);
      }
    });
  }

19
это, к сожалению, не работает в Internet Explorer :( просто измените (<any>Object).values(formGroup.controls)на Object.keys(formGroup.controls).map(x => formGroup.controls[x])(from stackoverflow.com/questions/42830257/… )
moi_meme

1
Это было огромным подспорьем для меня, когда я использовал FormGroup и FormControl и задавался вопросом, как показать пользователю, что они не коснулись обязательного поля. Спасибо.
NAMS

@NAMS без проблем! Я рад, что это помогло:]
masterwok

4
+1 Только одна небольшая проблема в рекурсивной части. Вы уже выполняете итерацию controlsв начале функции, поэтому вместо этого должно быть следующее:if (control.controls) { markFormGroupTouched(control); }
zurfyx 01

3
touchedпросто означает, что ввод был размыт один раз. Чтобы появились ошибки, мне также пришлось вызвать updateValueAndValidity()свои элементы управления.
adamdport 09

108

Из Angular 8/9 вы можете просто использовать

this.form.markAllAsTouched();

Чтобы пометить элемент управления и его дочерние элементы управления как затронутые.

AbstractControl doc


2
Это должен быть принятый ответ для тех, кто использует Angular 8.
Джейкоб Робертс

1
Это более простое и понятное решение.
HDJEMAI

1
это рекомендованное решение для angular 8 и выше, отлично!
Дюк Нгуен

1
Если это не работает для некоторых элементов управления, вероятно, они не находятся в этой FormGroup.
Noumenon

12

Что касается ответа @ masterwork. Я попробовал это решение, но у меня возникла ошибка, когда функция попыталась рекурсивно копать внутри FormGroup, потому что в этой строке передается аргумент FormControl вместо FormGroup:

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


8

Также будет работать цикл по элементам управления формы и их пометка как затронутые:

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

1
Спасибо, приятель, ваше решение неплохое, единственное, что я хотел бы добавить, потому что tslint жалуется, так это: for (const i in this.form.controls) {if (this.form.controls [i]) {this.form.controls [i ] .markAsTouched (); }}
Аврам Вирджил

1
Это не работает, если у вас formGroupесть другие formGroups
adamdport

3

Это мое решение

      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

У меня была эта проблема, но я нашел «правильный» способ сделать это, несмотря на то, что его нет ни в одном учебнике по Angular, который я когда-либо находил.

В своем HTML- formкоде добавьте в тег ту же переменную ссылки на шаблон #myVariable='ngForm'( переменную 'hashtag'), которую используют примеры шаблонных форм, в дополнение к тем, что используются в примерах реактивных форм:

<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один, это не сработает, потому что для переменной будет задан HtmlElement вместо директивы, управляющей этим элементом.

Знайте , что myFormGroupвидно в машинописном коде компонента в реакционно - Forms учебники, но myFormэто не так , если вы не передать его через вызов метода, как submit(myForm)в submit(myForm: NgForm): void {...}. (Обратите внимание NgFormна заглавные буквы в машинописном тексте, но в верблюжьем регистре в HTML.)


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

Просто попробовал это, и почему-то он не касается дочерних элементов формы. Пришлось написать цикл, который помечает все дочерние элементы вручную. Знаете ли вы, почему markAsTouched()не затрагиваются дочерние элементы?
Giedrius Kiršys

Какие угловые версии вы используете?
Владо Тесанович

Угловая версия - 2.1.0
Гедрюс Киршис

1
Похоже, я нашел, почему markAsTouched()не отмечают дочерние элементы - github.com/angular/angular/issues/11774 . TL; DR: Это не ошибка.
Giedrius Kiršys

1
Ага, теперь я вспомнил. Вы можете отключить кнопку отправки, если форма недействительна, <button [disable] = "! This.form"> Отправить </button>
Владо Тесанович

1

Я столкнулся с той же проблемой, но я не хочу «загрязнять» мои компоненты кодом, который это обрабатывает. Тем более, что мне это нужно во многих формах, и я не хочу повторять код в разных случаях.

Таким образом, я создал директиву (используя опубликованные ответы). Директива украшает -Метод onSubmitNgForm: если форма недействительна, она помечает все поля как затронутые и прерывает отправку. В противном случае обычный 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>

1

Это код, который я на самом деле использую.

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

У меня работает этот код:

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

Решение без рекурсии

Для тех, кто беспокоится о производительности, я предложил решение, которое не использует рекурсию, хотя по-прежнему выполняет итерацию по всем элементам управления на всех уровнях.

 /**
  * 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-touched


@VladimirPrudnikov Проблема в том, что при рекурсивном вызове функции обычно связаны дополнительные накладные расходы. Из-за этого ЦП будет тратить больше времени на обработку стека вызовов. При использовании циклов процессор будет тратить большую часть времени на выполнение самого алгоритма. Преимущество рекурсии в том, что код обычно более читабелен. Итак, если производительность не является проблемой, я бы сказал, что вы можете придерживаться рекурсии.
Артур Сильва

«Преждевременная оптимизация - корень всех зол».
Dem

@DemPilafian Я согласен с цитатой. Однако здесь это не применяется, потому что, если кто-то обратится к этой теме, он сможет бесплатно получить оптимизированное решение (не тратя на это время). И, кстати, в моем случае у меня действительно были причины его оптимизировать =)
Артур Сильва

1

согласно @masterwork

машинописный код для угловой версии 8

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

0

Вот как я это делаю. Я не хочу, чтобы поля ошибок отображались до тех пор, пока не будет нажата кнопка отправки (или не будет нажата форма).

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>


Похоже, что со временем он может стать тяжелым при добавлении новых правил проверки. Но я понял.
Giedrius Kiršys

0

Я полностью понимаю разочарование ОП. Я использую следующее:

Полезная функция :

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

Обратите внимание, что эта функция еще не поддерживает вложенные элементы управления.


0

Смотрите этот драгоценный камень . Пока что это самое элегантное решение, которое я видел.

Полный код

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

Посмотреть:

<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

Я сделал версию с некоторыми изменениями в представленных ответах, для тех, кто использует версии более ранние, чем версия 8 angular, я хотел бы поделиться ею с теми, кто будет полезен.

Вспомогательная функция:

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

Применение:

onSubmit() {
 if (this.isValidForm()) {
   // ... TODO: logic if form is valid
 }
}
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.