リアクティブフォーム-フィールドにタッチ済みのマークを付ける


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
これは残念ながらInternetExplorerでは機能しません:(単に(stackoverflow.com/questions/42830257/…から)に変更(<any>Object).values(formGroup.controls)してくださいObject.keys(formGroup.controls).map(x => formGroup.controls[x])
moi_meme 2017年

1
これは、FormGroupとFormControlを使用していて、必要なフィールドに触れていないことをユーザーに表示する方法を考えている私にとって大きな助けになりました。ありがとうございました。
NAMS 2017年

@NAMS問題ありません!お役に立ててうれしいです:]
masterwok 2018年

4
+1再帰部分でのマイナーな問題が1つだけ。あなたは既に繰り返し処理されているcontrols:それは、次の代わりにする必要がありますので、関数の先頭でif (control.controls) { markFormGroupTouched(control); }
zurfyx

3
touched入力が一度ぼやけたことを意味します。エラーを表示するにはupdateValueAndValidity()、コントロールも呼び出す必要がありました。
adamdport 2018

109

Angular 8/9から、簡単に使用できます

this.form.markAllAsTouched();

コントロールとその子孫コントロールをタッチ済みとしてマークします。

AbstractControlドキュメント


2
これは、角度8を使用してそれらのための受け入れ答えなければなりません
ジェイコブ・ロバーツ

1
これは、よりシンプルでクリーンなソリューションです。
HDJEMAI

1
これはAngular8以上に推奨されるソリューションです。
DucNguyen20年

1
これが一部のコントロールで機能していないように思われる場合、それらはおそらくそのFormGroupにありません。
ヌーメノン

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

1
formGroupformGroupのが含まれている場合、これは機能しません
adamdport 2018

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は、リアクティブフォームのチュートリアルに従ってコンポーネントのタイプスクリプトコードに表示されますが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()子要素に触れないのか手がかりはありますか?
GiedriusKiršys

使用しているAngularバージョンは何ですか?
ウラドテサノビッチ2016年

角度のバージョンは2.1.0です
GiedriusKiršys

1
markAsTouched()子要素にマークを付けない理由を見つけたようです-github.com/angular/angular/issues/11774。TL; DR:それはバグではありません。
GiedriusKiršys

1
うん、今覚えてる。フォームが無効な場合は、送信ボタンを無効にできます。<button [disable] = "!this.form">送信</ button>
Vlado

1

同じ問題が発生しましたが、これを処理するコードでコンポーネントを「汚染」したくありません。特に私はこれを多くの形で必要とし、さまざまな機会にコードを繰り返したくないので。

したがって、私はディレクティブを作成しました(これまでに投稿された回答を使用して)。ディレクティブは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>

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問題は、関数を再帰的に呼び出すと、通常、より多くのオーバーヘッドが関連付けられることです。そのため、CPUはコールスタックの処理により多くの時間を費やします。ループを使用する場合、CPUはアルゴリズム自体の実行にほとんどの時間を費やします。再帰の利点は、通常、コードが読みやすくなることです。したがって、パフォーマンスが問題にならない場合は、再帰に固執することができます。
アーサー・シルバ

「時期尚早の最適化はすべての悪の根源です。」
DemPilafian19年

@DemPilafian私は引用に同意します。ただし、ここでは適用されません。誰かがこのスレッドにアクセスした場合、最適化されたソリューションを無料で入手できるためです(時間はかかりません)。そして、ところで、私の場合、私は本当に)=それを最適化するための理由があった
アーサー・シルバ

1

@masterworkによる

Angularバージョン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>


これは、新しい検証ルールを追加すると、時間の経過とともに重くなる可能性があるようです。しかし、私は要点を得ました。
GiedriusKiršys

0

OPの欲求不満を完全に理解しています。私は以下を使用します:

ユーティリティ機能

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

提示された回答にいくつかの変更を加えたバージョンを作成しました。Angularのバージョン8より古いバージョンを使用している場合は、有用なユーザーと共有したいと思います。

効用関数:

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
 }
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.