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


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.