Biểu mẫu phản ứng - đánh dấu các trường là đã chạm


82

Tôi đang gặp khó khăn khi tìm cách đánh dấu tất cả các trường của biểu mẫu là đã chạm. Vấn đề chính là nếu tôi không chạm vào các trường và cố gắng gửi biểu mẫu - lỗi xác thực không hiển thị. Tôi có trình giữ chỗ cho đoạn mã đó trong bộ điều khiển của mình.
Ý tưởng của tôi rất đơn giản:

  1. người dùng nhấp vào nút gửi
  2. tất cả các trường đánh dấu là đã chạm
  3. trình định dạng lỗi chạy lại và hiển thị lỗi xác thực

Nếu ai có ý tưởng khác về cách hiển thị lỗi khi gửi mà không cần triển khai phương pháp mới - vui lòng chia sẻ chúng. Cảm ơn!


Biểu mẫu đơn giản của tôi:

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

Và bộ điều khiển của tôi:

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

Câu trả lời:


145

Hàm sau đây tái diễn thông qua các điều khiển trong một nhóm biểu mẫu và nhẹ nhàng chạm vào chúng. Bởi vì trường điều khiển là một đối tượng, mã gọi Object.values ​​() trên trường điều khiển của nhóm biểu mẫu.

  /**
   * 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);
      }
    });
  }

18
này buồn bã không làm việc trong Internet Explorer :( chỉ cần thay đổi (<any>Object).values(formGroup.controls)để Object.keys(formGroup.controls).map(x => formGroup.controls[x])(từ stackoverflow.com/questions/42830257/... )
moi_meme

1
Đây là một trợ giúp rất lớn đối với tôi khi sử dụng FormGroup và FormControl và tự hỏi làm thế nào để cho người dùng thấy rằng họ không chạm vào trường bắt buộc. Cảm ơn bạn.
NAMS

@NAMS không sao! Tôi rất vui vì nó đã giúp:]
masterwok

4
+1 Chỉ một vấn đề nhỏ ở phần đệ quy. Bạn đã iterating trên controlsvào lúc bắt đầu của hàm vì vậy nó nên được thay sau:if (control.controls) { markFormGroupTouched(control); }
zurfyx

3
touchedchỉ có nghĩa là đầu vào đã bị mờ một lần. Để làm cho lỗi xuất hiện, tôi cũng phải gọi các updateValueAndValidity()điều khiển của mình.
adamdport

101

Từ Angular 8/9, bạn có thể chỉ cần sử dụng

this.form.markAllAsTouched();

Để đánh dấu một điều khiển và điều khiển con của nó là được chạm.

Tài liệu AbstractControl


2
Đây phải là câu trả lời được chấp nhận cho những người sử dụng Angular 8.
Jacob Roberts

1
Đây là một giải pháp đơn giản và sạch sẽ.
HDJEMAI

1
đây là giải pháp được đề xuất cho góc 8 và cao hơn, tuyệt vời!
Duc Nguyen

1
Nếu điều này dường như không hoạt động đối với một số điều khiển, chúng có thể không nằm trong FormGroup đó.
Noumenon

11

Về câu trả lời của @ masterwork. Tôi đã thử giải pháp đó, nhưng tôi gặp lỗi khi hàm cố gắng đào, đệ quy, bên trong FormGroup, vì có truyền đối số FormControl, thay vì FormGroup, tại dòng này:

control.controls.forEach(c => this.markFormGroupTouched(c));

Đây là giải pháp của tôi

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

Vòng qua các điều khiển biểu mẫu và đánh dấu chúng là đã chạm cũng sẽ hoạt động:

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

1
Cảm ơn bạn, giải pháp của bạn khá tốt, điều duy nhất tôi muốn thêm vào vì tslint phàn nàn là điều này: for (const i in this.form.controls) {if (this.form.controls [i]) {this.form.controls [i ] .markAsTouched (); }}
Avram Virgil

1
Điều này không hoạt động nếu của bạn formGroupchứa các formGroups
adamdport

3

Đây là giải pháp của tôi

      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

Tôi đã gặp sự cố này nhưng đã tìm thấy cách "chính xác" để làm như vậy, mặc dù nó không có trong bất kỳ hướng dẫn Angular nào mà tôi từng tìm thấy.

Trong HTML của bạn, trên formthẻ, hãy thêm cùng một Biến tham chiếu mẫu #myVariable='ngForm'( biến 'thẻ bắt đầu bằng #') mà các ví dụ về Biểu mẫu theo hướng mẫu sử dụng, ngoài những gì các ví dụ về Biểu mẫu phản ứng sử dụng:

<form [formGroup]="myFormGroup" #myForm="ngForm" (ngSubmit)="submit()">

Bây giờ bạn có quyền truy cập myForm.submittedvào mẫu mà bạn có thể sử dụng thay thế cho (hoặc thêm vào) 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>

Biết điều đó myForm.form === myFormGrouplà đúng ... miễn là bạn không quên ="ngForm"phần. Nếu bạn sử dụng #myFormmột mình, nó sẽ không hoạt động vì var sẽ được đặt thành HtmlElement thay vì Chỉ thị điều khiển phần tử đó.

Biết rằng myFormGroupcó thể nhìn thấy trong mã nguyên cảo của Hợp phần của bạn theo hình thức hướng dẫn phản ứng, nhưng myFormkhông phải là, trừ khi bạn vượt qua nó trong thông qua một lời gọi phương thức, như submit(myForm)để submit(myForm: NgForm): void {...}. (Chú ý NgFormở chữ hoa tiêu đề trong bảng chữ nhưng chữ hoa lạc đà trong HTML.)


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

Chỉ cần thử điều đó và bằng cách nào đó nó không chạm vào các phần tử biểu mẫu con. Phải viết vòng lặp đánh dấu tất cả các phần tử con theo cách thủ công. Bạn có bất kỳ manh mối tại sao markAsTouched()không chạm vào các yếu tố trẻ em?
Giedrius Kiršys

Bạn đang sử dụng phiên bản góc cạnh nào?
Vlado Tesanovic vào

Phiên bản góc là 2.1.0
Giedrius Kiršys

1
Có vẻ như tôi đã tìm thấy lý do tại sao markAsTouched()không đánh dấu các phần tử con - github.com/angular/angular/issues/11774 . TL; DR: Nó không phải là một lỗi.
Giedrius Kiršys

1
Vâng, tôi nhớ bây giờ. Bạn có thể tắt nút gửi nếu hình thức là không hợp lệ, <nút [disable] = "this.form!"> Gửi </ button>
Vlado Tesanovic

1

Tôi đã gặp phải vấn đề tương tự, nhưng tôi không muốn "gây ô nhiễm" cho các thành phần của mình bằng mã xử lý vấn đề này. Đặc biệt là vì tôi cần điều này ở nhiều dạng và tôi không muốn lặp lại mã trong nhiều dịp khác nhau.

Vì vậy, tôi đã tạo một chỉ thị (sử dụng các câu trả lời được đăng cho đến nay). Chỉ thị trang trí NgForm's onSubmit-Method: Nếu biểu mẫu không hợp lệ, nó đánh dấu tất cả các trường là đã chạm và hủy gửi. Nếu không, phương thức onSubmit-Method thông thường thực thi bình thường.

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

Sử dụng:

<form (ngSubmit)="submit()" appValidateOnSubmit>
    <!-- ... form controls ... -->
</form>

1

Đây là mã mà tôi thực sự đang sử dụng.

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

Mã này phù hợp với tôi:

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

Một giải pháp không có đệ quy

Đối với những người lo lắng về hiệu suất, tôi đã đưa ra một giải pháp không sử dụng đệ quy, mặc dù nó vẫn lặp lại trên tất cả các điều khiển ở mọi cấp độ.

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

Giải pháp này hoạt động ở cả FormGroup và FormArray.

Bạn có thể chơi với nó ở đây: góc-đánh dấu-như-chạm vào


@VladimirPrudnikov Vấn đề là khi thực hiện cuộc gọi đệ quy đến một hàm thường có nhiều chi phí liên quan hơn. Do đó, CPU sẽ dành nhiều thời gian hơn để xử lý ngăn xếp cuộc gọi. Khi sử dụng các vòng lặp, CPU sẽ dành phần lớn thời gian để thực hiện chính thuật toán. Ưu điểm của đệ quy là mã thường dễ đọc hơn. Vì vậy, nếu hiệu suất không phải là một vấn đề, tôi sẽ nói rằng bạn có thể gắn bó với đệ quy.
Arthur Silva

"Tối ưu hóa sớm là gốc rễ của mọi điều xấu xa."
Dem Pilafian

@DemPilafian Tôi đồng ý với báo giá. Tuy nhiên, nó không áp dụng ở đây, vì nếu ai đó đưa ra chủ đề này, họ sẽ có thể nhận được giải pháp tối ưu hóa miễn phí (không tốn thời gian cho nó). Và, lý do btw, trong trường hợp của tôi, tôi thực sự đã phải tối ưu hóa nó =)
Arthur Silva

1

theo @masterwork

mã sắp chữ cho phiên bản góc 8

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

0

Đây là cách tôi làm điều đó. Tôi không muốn các trường lỗi hiển thị cho đến khi nhấn nút gửi (hoặc chạm vào biểu mẫu).

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>


Điều này có vẻ như nó có thể trở nên nặng nề theo thời gian, khi thêm các quy tắc xác thực mới. Nhưng tôi hiểu rõ.
Giedrius Kiršys

0

Tôi hoàn toàn hiểu sự thất vọng của OP. Tôi sử dụng như sau:

Chức năng tiện ích :

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

Cách sử dụng :

onSubmit() {
  if (!formValid(this.formGroup)) {
    return;
  }
  // ... TODO: logic if form is valid.
}

Lưu ý rằng chức năng này chưa phục vụ cho các điều khiển lồng nhau.


0

Xem đá quý này . Cho đến nay, giải pháp thanh lịch nhất mà tôi đã thấy.

Mã đầy đủ

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

Lượt xem:

<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

Mình đã làm một phiên bản có một số thay đổi trong đáp án đã trình bày, đối với những ai đang sử dụng phiên bản cũ hơn phiên bản 8 của góc cạnh, mình xin chia sẻ với những ai hữu ích.

Chức năng tiện ích:

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

Sử dụng:

onSubmit() {
 if (this.isValidForm()) {
   // ... TODO: logic if form is valid
 }
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.