Реактивні форми - позначте поля як доторкнуті


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])(від stackoverflow.com/questions/42830257 / ... )
moi_meme

1
Це було величезною допомогою для мене, використовуючи FormGroup та FormControl, і роздумував, як показати користувачеві, що він не торкався необхідного поля. Дякую.
NAMS

@NAMS не проблема! Я радий, що це допомогло:]
masterwok

4
+1 Лише одна незначна проблема в рекурсивній частині. Ви вже виконуєте ітерацію controlsна початку функції, тому замість цього вона повинна бути такою:if (control.controls) { markFormGroupTouched(control); }
zurfyx

3
touchedпросто означає, що введені дані були розмиті один раз. Щоб з’являлися помилки, мені також довелося зателефонувати updateValueAndValidity()на моє управління.
adamdport

109

З Angular 8/9 ви можете просто використовувати

this.form.markAllAsTouched();

Щоб позначити елемент керування та його нащадкові елементи керування як доторкнуті.

AbstractControl doc


2
Це має бути прийнятою відповіддю для тих, хто використовує Angular 8.
Джейкоб Робертс

1
Це більш просте і чисте рішення.
HDJEMAI

1
це рекомендоване рішення для кутових 8 і вище, чудово!
Duc Nguyen

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'( змінну `` хештег ''), яку використовують приклади, керовані шаблонами, на додаток до того, що використовують приклади реактивних форм:

<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видно в друкованому коді компонента в реакційно - 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()це не стосується дочірніх елементів?
Гедріус Кіршис

Які кутові версії ви використовуєте?
Владо Тесанович

Кутова версія - 2.1.0
Гедріус Кіршис,

1
Схоже, я знайшов, чому markAsTouched()не позначати дочірні елементи - github.com/angular/angular/issues/11774 . TL; DR: Це не помилка.
Гедріус Кіршис

1
Так, я зараз пам’ятаю. Ви можете відключити кнопку подання, якщо форма недійсна, <button [disable] = "! This.form"> Надіслати </button>
Владо Тесанович

1

Я зіткнувся з тією ж проблемою, але я не хочу "забруднювати" свої компоненти кодом, який це обробляє. Тим більше, що мені це потрібно у багатьох формах, і я не хочу повторювати код з різних приводів.

Таким чином я створив директиву (використовуючи відповіді, опубліковані дотепер). Директива прикрашає NgForm's onSubmit-Method: Якщо форма недійсна, вона позначає всі поля як торкнуті та скасовує подання. В іншому випадку звичайний метод onSubmit виконується нормально.

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.

Ви можете пограти з ним тут: кутова позначка як дотик


@VladimirPrudnikov Проблема полягає в тому, що при здійсненні рекурсивного виклику функції зазвичай пов'язано більше накладних витрат. Через це процесор витратить більше часу на обробку стека викликів. При використанні циклів процесор витратить найбільше часу на виконання самого алгоритму. Перевага рекурсії полягає в тому, що код, як правило, є більш читабельним. Отже, якщо продуктивність не є проблемою, я б сказав, що ви можете дотримуватися рекурсії.
Артур Сільва,

"Передчасна оптимізація - корінь усього зла".
Дем Пілафіян,

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


Цей виглядає так, ніби з часом може стати важким при додаванні нових правил перевірки. Але я зрозумів суть.
Гедріус Кіршис

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.