简体   繁体   English

使用 Angular 组件处理表单错误 - TypeScript

[英]Handle form errors using components Angular - TypeScript

I am currently working on a form in Angular / Typescript of several fields (more than 10 fields), and I wanted to manage the errors more properly without duplicating code in my html page.我目前正在处理多个字段(超过 10 个字段)的Angular / Typescript表单,我想更正确地管理错误,而无需在我的 html 页面中复制代码。

Here is an example of a form :这是一个表格的例子:

 <form [formGroup]="myForm"> <label>Name</label> <input type="text" formControlName="name"> <p class="error_message" *ngIf="myForm.get('name').invalid && (myForm.submitted || myForm.get('name').dirty)">Please provide name</p> <label>Lastname</label> <input type="text" formControlName="lastname"> <p class="error_message" *ngIf="myForm.get('lastname').invalid && (myForm.submitted || myForm.get('lastname').dirty)">Please provide email</p> <label>Email</label> <input type="text" formControlName="email"> <p class="error_message" *ngIf="myForm.get('email').hasError('required') && (myForm.submitted || myForm.get('email').dirty)">Please provide email</p> <p class="error_message" *ngIf="myForm.get('email').hasError('email') && (myForm.submitted || myForm.get('email').dirty)">Please provide valid email</p> </form>

In my case, I have two types of validation for my form :就我而言,我的表单有两种类型的验证:

  • Html validation : required, maxSize, ... etc. Html 验证:必需、maxSize、...等。
  • Back validation : For example, invalid account, size of loaded file, ... etc.返回验证:例如,无效帐户、加载文件的大小等。

I try to using a directive as mentioned here我尝试使用这里提到的指令

 <form [formGroup]="myForm"> <label>Name</label> <input type="text" formControlName="name"> <div invalidmessage="name"> <p *invalidType="'required'">Please provide name</p> </div> <label>Lastname</label> <input type="text" formControlName="lastname"> <div invalidmessage="lastname"> <p *invalidType="'required'">Please provide lastname</p> </div> <label>Email</label> <input type="text" formControlName="email"> <div invalidmessage="email"> <p *invalidType="'required'">Please provide email</p> <p *invalidType="'email'">Please provide valid email</p> </div> </form>

But even with this solution the code is always duplicated and no ability to handle both types of validation.但即使使用此解决方案,代码也始终是重复的,并且无法处理这两种类型的验证。

Do you have another approach ?你有另一种方法吗? Is use components appropriate in this case ?在这种情况下使用组件是否合适? If yes, how can do it.如果是,怎么办。

Thank you in advance for your investment.预先感谢您的投资。

You can move the validation errors into a component and pass in the formControl.errors as an input property.您可以将验证错误移动到组件中,并将 formControl.errors 作为输入属性传入。 That way all the validation messages can be re-used.这样,所有验证消息都可以重复使用。 Here is an example on StackBlitz .这是一个关于StackBlitz的例子。 The code is using Angular Material but still should be handy even if you aren't.该代码正在使用 Angular Material,但即使您不是,也应该很方便。

validation-errors.component.ts验证errors.component.ts

import { Component, OnInit, Input, ChangeDetectionStrategy } from '@angular/core';
import { FormGroup, ValidationErrors } from '@angular/forms';

@Component({
  selector: 'validation-errors',
  templateUrl: './validation-errors.component.html',
  styleUrls: ['./validation-errors.component.css'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ValidationErrorsComponent implements OnInit {
  @Input() errors: ValidationErrors;

  constructor() {}

  ngOnInit() {}

}

validation-errors.component.html验证errors.component.html

<ng-container *ngIf="errors && errors['required']"> Required</ng-container>
<ng-container *ngIf="errors && errors['notUnique']">Already exists</ng-container>
<ng-container *ngIf="errors && errors['email']">Please enter a valid email</ng-container>

For the back validation messages set the error manually on the form control.对于返回验证消息,在表单控件上手动设置错误。

const nameControl = this.userForm.get('name');
nameControl.setErrors({
  "notUnique": true
});

To use the validation component on the form:要在表单上使用验证组件:

   <form [formGroup]="userForm" (ngSubmit)="submit()">
      <mat-form-field>
        <input matInput placeholder="name" formControlName="name" required>
        <mat-error *ngIf="userForm.get('name').status === 'INVALID'">
          <validation-errors [errors]="userForm.get('name').errors"></validation-errors>      
        </mat-error>
      </mat-form-field>
      <mat-form-field>
        <input matInput placeholder="email" formControlName="email" required>
        <mat-error *ngIf="userForm.get('email').status === 'INVALID'">
          <validation-errors [errors]="userForm.get('email').errors"></validation-errors>
        </mat-error>
      </mat-form-field>
      <button mat-raised-button class="mat-raised-button" color="accent">SUBMIT</button>
    </form>

Demo演示

You can inject NgForm and access the FormControlName directive through @ContentChild within a custom validator component to achieve re-use:您可以在自定义验证器组件中注入NgForm并通过@ContentChild访问FormControlName指令以实现重用:

@Component({
  selector: '[validator]',
  template: `
    <ng-content></ng-content>
    <div *ngIf="formControl.invalid">
        <div *ngIf="formControl.errors.required && (form.submitted || formControl.dirty)">
             Please provide {{ formControl.name }}
        </div>
        <div *ngIf="formControl.errors.email && (form.submitted || formControl.dirty)">
             Please provide a valid email
        </div>
        <div *ngIf="formControl.errors.notstring && (form.submitted || formControl.dirty)">
             Invalid name
        </div>

    </div>
`})

export class ValidatorComponent implements OnInit {
   @ContentChild(FormControlName) formControl;
   constructor(private form: NgForm) { 

   }

   ngOnInit() { }

}

To use it, you would wrap all your form controls (which has a formControlName) with an HTML element and add a validator attribute:要使用它,你需要用一个 HTML 元素包装所有的表单控件(它有一个 formControlName)并添加一个验证器属性:

<form #f="ngForm" (ngSubmit)="onSubmit(f)" novalidate>
<div [formGroup]="myForm">
     <label>Name</label>
     <div validator>
         <input type="text" formControlName="name">
     </div>
     <label>Lastname</label>
     <div validator>
         <input type="text" formControlName="lastname">
     </div>
     <label>Email</label>
     <div validator>
         <input type="text" formControlName="email">
     </div>
</div>
<button type="submit">Submit</button>
</form>

This will work for synchronous and asynchronous validators.这适用于同步和异步验证器。

For the html validation I would write a custom formcontrol which will basically be a wrapper around an input.对于 html 验证,我将编写一个自定义表单控件,它基本上是输入的包装器。 I would also write custom validators which return an error message (Build-in validators return an object I believe).我还会编写返回错误消息的自定义验证器(内置验证器返回一个我相信的对象)。 Within your custom formcontrol you can do something like this:在您的自定义表单控件中,您可以执行以下操作:

<div *ngIf="this.formControl.errors">
    <p>this.formControl.errors?.message</p>
</div>

For the backend validator you can write an async validator .对于后端验证器,您可以编写一个异步验证器

I had the same requirement , nobody likes to re-write the same code twice.我有同样的要求,没有人喜欢两次重写相同的代码。

This can be done by creating custom form controls.这可以通过创建自定义表单控件来完成。 The idea is you create your custom form controls , have a common service that Generates a custom formControl object and inject appropriate Validators based on the data type provided into the FormControl Object.这个想法是你创建你的自定义表单控件,有一个通用的服务来生成一个自定义的 formControl 对象并根据提供给 FormControl 对象的数据类型注入适当的验证器。

Where did the Data type come from ?数据类型从何而来?

Have a file in your assets or anywhere which contains types like this :在您的资产或任何包含以下类型的文件中放置一个文件:

[{
  "nameType" : {
   maxLength : 5 , 
   minLength : 1 , 
   pattern  :  xxxxxx,
   etc
   etc

   }
}
]

This you can read in your ValidatorService and select appropriate DataType with which you can create your Validators and return to your Custom Form Control.这可以在您的ValidatorService读取并选择适当的数据类型,您可以使用它来创建您的验证器并返回到您的自定义表单控件。

For Example ,例如,

<ui-text name="name" datatype="nameType" [(ngModel)]="data.name"></ui-text>

This is a brief description of it on a high level of what I did to achieve this.这是对我为实现这一目标所做的工作的简要描述。 If you need additional information with this , do comment.如果您需要与此有关的其他信息,请发表评论。 I am out so cannot provide you with code base right now but sometime tomorrow might update the answer.我出去了,所以现在无法为您提供代码库,但明天某个时候可能会更新答案。

UPDATE for the Error Showing part错误显示部分的更新

You can do 2 things for it , bind your formControl's validator with a div within the control and toggle it with *ngIf="formControl.hasError('required )"` , etc.您可以为它做两件事,将您的 formControl 的验证器与控件中的 div 绑定,并使用*ngIf="formControl.hasError('required )"` 等进行切换。

For a Message / Error to be displayed in another generic place like a Message Board its better to put that Message Board markup somewhere in the ParentComponent which does not get removed while routing (debatable based on requirement) and make that component listen to a MessageEmit event which your ErrorStateMatcher of your formControl will fire whenever necessary(based on requirement).要将消息/错误显示在另一个通用位置(如消息板)中,最好将该消息板标记放在 ParentComponent 中的某个位置,该标记在路由时不会被删除(根据需求存在争议)并使该组件侦听 MessageEmit 事件您的 formControl 的ErrorStateMatcher将在必要时触发(根据要求)。

This is the design we used and it worked pretty well , you can do a lot with these formControls once you start Customising them.这是我们使用的设计并且效果很好,一旦您开始自定义它们,您就可以使用这些 formControl 做很多事情。

You could create a custom component ValidationMessagesComponent :您可以创建一个自定义组件ValidationMessagesComponent

Template :模板:

<p class="error_message" *ngIf="form.get(controlName).hasError('required') && (form.submitted || form.get(controlName).dirty)">Please provide {{controlName}}</p>
<p class="error_message" *ngIf="form.get(controlName).hasError('email') && (form.submitted || form.get(controlName).dirty)">Please provide valid {{controlName}}</p>
...other errors

And with the inputs :并使用输入:

@Input() controlName;
@Input() form;

Then use it like this :然后像这样使用它:

<validation-messages [form]="myForm" controlName="email"></validation-messages>

You can use this repo which has default validation messages and you can customize them as well你可以使用这个具有默认验证消息的repo ,你也可以自定义它们

example usage will be like this示例用法将是这样的

<form [formGroup]="editorForm" novalidate>
    <label>First Name</label>
    <input formControlName="firstName" type="text">
    <ng2-mdf-validation-message [control]="firstName" *ngIf="!firstName.pristine"></ng2-mdf-validation-message>
</form>

To make template code clear and avoid duplicated code of validating messages, we should change them to be more reusable, here creating a custom directive which adds and removes validating message code block is an option( shown in below demo ).为了使模板代码清晰并避免验证消息的重复代码,我们应该将它们更改为更可重用,这里创建一个添加和删除验证消息代码块的自定义指令是一个选项(如下面的演示所示)。

Show/Hide validating messages显示/隐藏验证消息

In the directive, we can access to directive' host form control and add/remove validating message based on validate status of it by subscribing to it's valueChanges event.在指令中,我们可以通过订阅它的valueChanges事件来访问指令的主机表单控件并根据它的验证状态添加/删除验证消息。

@Directive(...)
export class ValidatorMessageDirective implements OnInit {

  constructor(
    private container: ControlContainer,
    private elem: ElementRef,          // host dom element
    private control: NgControl         // host form control
  ) { }

  ngOnInit() {
    const control = this.control.control;

    control.valueChanges.pipe(distinctUntilChanged()).subscribe(() => {
      this.option.forEach(validate => {
        if (control.hasError(validate.type)) {
          const validateMessageElem = document.getElementById(validate.id);
          if (!validateMessageElem) {
            const divElem = document.createElement('div');
            divElem.innerHTML = validate.message;
            divElem.id = validate.id;
            this.elem.nativeElement.parentNode.insertBefore(divElem, this.elem.nativeElement.nextSibling);
          }
        } else {
          const validateMessageElem = document.getElementById(validate.id);
          if (validateMessageElem) {
             this.elem.nativeElement.parentNode.removeChild(validateMessageElem);
          }
        }
      })
    });
  }
}

Validate options验证选项

The directive adds and removes validating message based on corresponding validate errors.该指令根据相应的验证错误添加和删除验证消息。 So the last step we should do is to tell directive which types of validate errors to watch and what messages should be shown, that's the @Input field by which we transport validating options to directive.所以我们应该做的最后一步是告诉指令要观察哪些类型的验证错误以及应该显示哪些消息,这是我们将验证选项传输到指令的@Input字段。


Then we can simply write template code as below:然后我们可以简单地编写模板代码如下:

<form [formGroup]="form">
  <input type="text" formControlName="test" [validate-message]="testValidateOption"><br/>
  <input type="number" formControlName="test2" [validate-message]="test2ValidateOption">
</form>

Refer working demo .参考工作演示

The best way is to implement custom ControlValueAccessor s for each type of input, combining <label> , <input> and some tags for displaying error message (in my project I simply use title attribute for this purpose) in a single component.最好的方法是为每种类型的输入实现自定义ControlValueAccessor ,在单个组件中结合<label><input>和一些用于显示错误消息的标签(在我的项目中,我只是为此目的使用title属性)。

All value accessors should implement the same interface or extend base abstract class, providing methods to set and clear error message and any other methods which you may want to call from validator directives.所有值访问器都应该实现相同的接口或扩展基抽象类,提供设置和清除错误消息的方法以及您可能希望从验证器指令调用的任何其他方法。

Also, you will need to implement custom validator directives for each validation type (i had to re-implement even required and maxlength ), validators must return error objects in uniform way ie for email validator {email: "Invalid email address"} .此外,您需要为每种验证类型实现自定义验证器指令(我不得不重新实现甚至requiredmaxlength ),验证器必须以统一的方式返回错误对象,即电子邮件验证器{email: "Invalid email address"} Validator directives can get reference to your control value accessors via injection - @Inject(NG_VALUE_ACCESSOR) controls:AbstractFormComponent<any>[] (usually array with one element, AbstractFormComponent is your base class for accessors), use this reference to set or clear accessor error message.验证器指令可以通过注入获取对您的控件值访问器的引用 - @Inject(NG_VALUE_ACCESSOR) controls:AbstractFormComponent<any>[] (通常是一个元素的数组, AbstractFormComponent是访问器的基类),使用此引用来设置或清除访问器错误信息。

You can also implement two additional types of validator directives: sync and async, which can receive validator function via @Input ie [async]="loginValidatorFn" , where loginValidatorFn is defined in component class and returns Observable<ValidationErrors> .您还可以实现另外两种类型的验证器指令:sync 和 async,它们可以通过@Input接收验证器函数,即[async]="loginValidatorFn" ,其中loginValidatorFn在组件类中定义并返回Observable<ValidationErrors>

This is real code from our application:这是我们应用程序中的真实代码:

<div class="input" [caption]="'SSN: '" name="ssn" type="text" [(ngModel)]="item.ssn" [async]="memberSsnValidatorFn" required></div>

Here is some part of code I used in library to generate dynamic forms.这是我在库中用于生成动态表单的部分代码。

This is FormError.ts which is used to get error and custom messages if we want.这是FormError.ts ,如果我们需要,它用于获取错误和自定义消息。

import { AbstractControl } from "@angular/forms";

type ErrorFunction = (errorName: string, error: object) => string;
export type ErrorGetter =
    string | { [key2: string]: string } | ErrorFunction;

export class FormError {
    constructor(private errorGetter?: ErrorGetter) { }
    hasError(abstractControl: AbstractControl) {
        return abstractControl.errors && (abstractControl.dirty || abstractControl.touched);
    }
    getErrorMsgs(abstractControl: AbstractControl): string[] {
        if (!this.hasError(abstractControl))
            return null;
        let errors = abstractControl.errors;
        return Object.keys(errors).map(anyError => this.getErrorValue(anyError, errors[anyError]));
    }
    getErrorValue(errorName: string, error: object): string {
        let errorGetter = this.errorGetter;
        if (!errorGetter)
            return predictError(errorName, error);
        if (isString(errorGetter))
            return errorGetter;
        else if (isErrorFunction(errorGetter)) {
            let errorString = errorGetter(errorName, error);
            return this.predictedErrorIfEmpty(errorString, errorName, error)
        }
        else {
            let errorString = this.errorGetter[errorName];
            return this.predictedErrorIfEmpty(errorString, errorName, error)
        }
    }
    predictedErrorIfEmpty(errorString: string, errorName: string, error: object) {
        if (errorString == null || errorString == undefined)
            return predictError(errorName, error);
        return errorString;
    }


}
function predictError(errorName: string, error: object): string {
    if (errorName === 'required')
        return 'Cannot be blank';
    if (errorName === 'min')
        return `Should not be less than ${error['min']}`;
    if (errorName === 'max')
        return `Should not be more than ${error['max']}`;
    if (errorName === 'minlength')
        return `Alteast ${error['requiredLength']} characters`;
    if (errorName === 'maxlength')
        return `Atmost ${error['requiredLength']} characters`;
    // console.warn(`Error for ${errorName} not found. Error object = ${error}`);
    return 'Error';
}
export function isString(s: any): s is string {
    return typeof s === 'string' || s instanceof String;
}
export function isErrorFunction(f: any): f is ErrorFunction {
    return typeof f === "function";
}

Custom Messages自定义消息

 class FormError {
    constructor(private errorGetter?: ErrorGetter) { }
    }

Now ErrorGetter is like现在ErrorGetter就像

type ErrorFunction = (errorName: string, error: object) => string;
type ErrorGetter =
    string | { [key2: string]: string } | ErrorFunction;
  1. If we want constant error for any error then it should be like如果我们想要任何错误的常量错误,那么它应该像

    new FormError('Password is not right')

  2. If we want constant error for specific error then it should be like如果我们想要特定错误的常量错误,那么它应该像

    new FormError({required:'Address is necessary.'})

    For other errors it will go in predict error.对于其他错误,它将进入预测错误。

  3. If we want use function for specific error then it should be like如果我们想对特定错误使用函数,那么它应该像

    new FormError((errorName,errorObject)=>{ if(errorName=='a') return '2';})

    For other errors it will go in predict error.对于其他错误,它将进入预测错误。

  4. Modify predictError function according to your need.根据需要修改 predictError 函数。

FormError component表单错误组件

form-error.html

<ng-container *ngIf="formError.hasError(control)">
  <div class='form-error-message' *ngFor='let error of  formError.getErrorMsgs(control)'>{{error}}</div>
</ng-container>

form-error.scss

form-error {
    .form-error-message {
        color: red;
        font-size: .75em;
        padding-left: 16px;
    }
}

form-error.ts

@Component({
  selector: 'form-error',
  templateUrl: 'form-error.html'
})
export class FormErrorComponent {
  @Input() formError: FromError;
  @Input() control: AbstractControl;
}

Usage用法

<form-error [control]='thatControl' ></form-error>

Obviously FormError is not the best design.显然FormError不是最好的设计。 Modify however you like.随心所欲地修改。

 <form [formGroup]="myForm"> <label>Name</label> <input type="text" formControlName="name"> <p class="error_message" *ngIf="myForm.get('name').invalid && (myForm.submitted || myForm.get('name').dirty)">Please provide name</p> <label>Lastname</label> <input type="text" formControlName="lastname"> <p class="error_message" *ngIf="myForm.get('lastname').invalid && (myForm.submitted || myForm.get('lastname').dirty)">Please provide email</p> <label>Email</label> <input type="text" formControlName="email"> <p class="error_message" *ngIf="myForm.get('email').hasError('required') && (myForm.submitted || myForm.get('email').dirty)">Please provide email</p> <p class="error_message" *ngIf="myForm.get('email').hasError('email') && (myForm.submitted || myForm.get('email').dirty)">Please provide valid email</p> </form>

You can use NPM package.您可以使用NPM包。 Its simple easy to use and customize for both reactive and template driven forms.它简单易用,可针对反应式和模板驱动的表单进行自定义。

Code snippet:代码片段:

HTML HTML

<form [formGroup]="demoForm">
    <div>
         <label for="name">Name</label>
         <input type="text" formControlName="name" name="name" placeholder="Name validator">
         <tn-form-error [control]="demoForm.controls.name" [field]="'Name'"></tn-form-error>
    </div>
</form>

Component组件

<p>
 this.demoForm = new FormGroup({
      name: new FormControl(''[Validators.required])
 });

Play around here这里玩耍

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM