使用mat-error显示自定义验证器错误

42

我来找你谈一下与Angular Material相关的问题。实际上,我认为这是一个问题,但我更喜欢先寻找误解。

我的问题首先与上下文有关,我试图创建一个简单的表单,包含两个输入框:密码和确认密码。

user-form.component.ts

this.newUserForm = this.fb.group({
  type: ['', Validators.required],
  firstname: ['', Validators.required],
  lastname: ['', Validators.required],
  login: ['', Validators.required],
  matchingPasswordsForm: this.fb.group(
    {
      password1: ['', Validators.required],
      password2: ['', Validators.required],
    },
    {
      validator: MatchingPasswordValidator.validate,
    },
  ),
  mail: ['', [Validators.required, Validators.pattern(EMAIL_PATTERN)]],
  cbaNumber: [
    '411000000',
    [Validators.required, Validators.pattern(CBANUMBER_PATTERN)],
  ],
  phone: ['', [Validators.required, Validators.pattern(PHONE_PATTERN)]],
}

我对匹配密码表单 FormGroup 感兴趣。您可以在其中看到验证器。

这里是验证器:

matching-password.validator.ts

export class MatchingPasswordValidator {
    constructor() {}

    static validate(c: FormGroup): ValidationErrors | null {
        if (c.get('password2').value !== c.get('password1').value) {
            return { matchingPassword: true};
        }
        return null;
    }
}

and the HTML.

user-form.component.html

<div class="row" formGroupName="matchingPasswordsForm">
    <mat-form-field class="col-md-6 col-sm-12">
        <input matInput placeholder="Mot de passe:" formControlName="password1">
        <mat-error ngxErrors="matchingPasswordsForm.password1">
            <p ngxError="required" [when]="['dirty', 'touched']">{{requiredMessage}}</p>
        </mat-error>
    </mat-form-field>

    <mat-form-field class="col-md-6 col-sm-12">
        <input matInput placeholder="Confirmez" formControlName="password2">
        <mat-error ngxErrors="matchingPasswordsForm.password2">
            <p ngxError="required" [when]="['dirty', 'touched']">{{requiredMessage}}</p>
        </mat-error>
        <!--                 -->
        <!-- problem is here -->
        <!--                 -->
        <mat-error ngxErrors="matchingPasswordsForm" class="mat-error">
            <p ngxError="matchingPassword" [when]="['dirty', 'touched']">{{passwordMatchErrorMessage}}</p>
        </mat-error>
        <!-- ^^^^^^^^^^^^^^^^ -->
        <!-- /problem is here -->
        <!--                  -->
    </mat-form-field>
</div>

我已在有趣的代码周围添加了注释。
现在,一些解释:使用标签时,当触摸密码2时,我的错误会被显示出来: 刚刚触摸了密码2 但是,当我输入错误的密码时,错误不再显示: 错误的密码2 起初我以为我误解了自定义验证器的使用。但当我用替换错误提示时,整个程序就完美地运行了!
<mat-hint ngxErrors="matchinghPasswordsForm">
    <p ngxError="matchingPassword" [when]="['dirty', 'touched']">{{passwordMatchErrorMessage}}</p>
</mat-hint>

使用mat-hint标签

我希望我表达清楚了,在在material design的github上发布问题之前,我真的想知道您的观点。

如果我误解了什么,请指出我错过了什么。

最后一件事,我的测试是使用ngxerrors和*ngif完成的。为了更易读,我的代码示例只使用ngxerrors。


https://dev59.com/vVYN5IYBdhLWcg3wXnJa#47670892 - AT82
@AJT_82:就像你链接中的答案所说,如果更改了先前的日期,错误消息将不再显示,问题似乎与我的相同。 - Pouette
是的,因此创建一个错误状态匹配器 ;) - AT82
这有多难简直不真实。真是难以置信!有没有更好的方法来获取mat-error的样式而不必处理这些问题? - mel
4个回答

105

Alex 是正确的。你必须使用一个 ErrorStateMatcher。我研究了很多才弄明白这一点,没有一个单一的来源给了我完整的答案。我不得不从多个来源搜集所学到的信息来制定自己的解决方案。希望下面的示例可以避免您经历的头疼。

表单

这是一个示例表单,它使用 Angular Material 元素用于用户注册页面。

<form [formGroup]="userRegistrationForm" novalidate>

    <mat-form-field>
        <input matInput placeholder="Full name" type="text" formControlName="fullName">
        <mat-error>
            {{errors.fullName}}
        </mat-error>
    </mat-form-field>

    <div formGroupName="emailGroup">
        <mat-form-field>
            <input matInput placeholder="Email address" type="email" formControlName="email">
            <mat-error>
                {{errors.email}}
            </mat-error>
        </mat-form-field>

        <mat-form-field>    
            <input matInput placeholder="Confirm email address" type="email" formControlName="confirmEmail" [errorStateMatcher]="confirmValidParentMatcher">
            <mat-error>
                {{errors.confirmEmail}}
            </mat-error>
        </mat-form-field>
    </div>

    <div formGroupName="passwordGroup">
        <mat-form-field>
            <input matInput placeholder="Password" type="password" formControlName="password">
            <mat-error>
                {{errors.password}}
            </mat-error>
        </mat-form-field>
    
        <mat-form-field>
            <input matInput placeholder="Confirm password" type="password" formControlName="confirmPassword" [errorStateMatcher]="confirmValidParentMatcher">
            <mat-error>
                {{errors.confirmPassword}}
            </mat-error>
        </mat-form-field>
    </div>

    <button mat-raised-button [disabled]="userRegistrationForm.invalid" (click)="register()">Register</button>

</form>

正如您所看到的,我正在使用来自Angular Material的<mat-form-field><input matInput><mat-error>标签。 我最初的想法是添加*ngIf指令来控制何时显示<mat-error>部分,但这没有效果! 实际上,可见性由<mat-form-field>的有效性(和“touched”状态)控制,在HTML或Angular中没有提供测试与另一个表单字段相等的验证器。 这就是确认字段上的errorStateMatcher指令发挥作用的地方。

errorStateMatcher指令内置于Angular Material中,提供使用自定义方法确定<mat-form-field>表单控件的有效性,并允许访问其父级的有效性状态的能力。 要开始理解如何在此用例中使用errorStateMatcher,请先查看组件类。

组件类

这是一个使用FormBuilder为表单设置验证的Angular组件类。

export class App {
    userRegistrationForm: FormGroup;

    confirmValidParentMatcher = new ConfirmValidParentMatcher();

    errors = errorMessages;

    constructor(
        private formBuilder: FormBuilder
    ) {
        this.createForm();
    }

    createForm() {
        this.userRegistrationForm = this.formBuilder.group({
            fullName: ['', [
                Validators.required,
                Validators.minLength(1),
                Validators.maxLength(128)
            ]],
            emailGroup: this.formBuilder.group({
                email: ['', [
                    Validators.required,
                    Validators.email
                ]],
                confirmEmail: ['', Validators.required]
            }, { validator: CustomValidators.childrenEqual}),
            passwordGroup: this.formBuilder.group({
                password: ['', [
                    Validators.required,
                    Validators.pattern(regExps.password)
                ]],
                confirmPassword: ['', Validators.required]
            }, { validator: CustomValidators.childrenEqual})
        });
    }

    register(): void {
        // API call to register your user
    }
}

该类为用户注册表格设置了一个FormBuilder。 请注意,该类中有两个FormGroup,一个用于确认电子邮件地址,另一个用于确认密码。各个字段使用适当的验证器函数,但两个组都在组级别上使用自定义验证器,以确保每个组中的字段相等,并在它们不相等时返回验证错误。

自定义验证器和errorStateMatcher指令的组合为我们提供了完整的功能,可以适当地显示确认字段的验证错误。让我们看一下自定义验证模块,将其整合起来。

自定义验证模块

我选择将自定义验证功能拆分成自己的模块,以便可以轻松重用。出于同样的原因,我还选择将与表格验证相关的其他内容放入该模块中,即正则表达式和错误消息。稍微考虑一下未来,您可能也会允许用户在用户更新表单中更改其电子邮件地址和密码,对吧?这里是整个模块的代码:

import { FormGroup, FormControl, FormGroupDirective, NgForm, ValidatorFn } from '@angular/forms';
import { ErrorStateMatcher } from '@angular/material';

/**
 * Custom validator functions for reactive form validation
 */
export class CustomValidators {
    /**
     * Validates that child controls in the form group are equal
     */
    static childrenEqual: ValidatorFn = (formGroup: FormGroup) => {
        const [firstControlName, ...otherControlNames] = Object.keys(formGroup.controls || {});
        const isValid = otherControlNames.every(controlName => formGroup.get(controlName).value === formGroup.get(firstControlName).value);
        return isValid ? null : { childrenNotEqual: true };
    }
}

/**
 * Custom ErrorStateMatcher which returns true (error exists) when the parent form group is invalid and the control has been touched
 */
export class ConfirmValidParentMatcher implements ErrorStateMatcher {
    isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
        return control.parent.invalid && control.touched;
    }
}

/**
 * Collection of reusable RegExps
 */
export const regExps: { [key: string]: RegExp } = {
    password: /^(?=.*[0-9])(?=.*[!@#$%^&*])[a-zA-Z0-9!@#$%^&*]{7,15}$/
};

/**
 * Collection of reusable error messages
 */
export const errorMessages: { [key: string]: string } = {
    fullName: 'Full name must be between 1 and 128 characters',
    email: 'Email must be a valid email address (username@domain)',
    confirmEmail: 'Email addresses must match',
    password: 'Password must be between 7 and 15 characters, and contain at least one number and special character',
    confirmPassword: 'Passwords must match'
};

首先,让我们来看一下组的自定义验证器函数:CustomValidators.childrenEqual()。由于我的编程背景是面向对象的,我选择将此函数设置为静态类方法,但您也可以轻松地将其设置为独立的函数。该函数必须是ValidatorFn类型(或适当的文本签名),并接受一个类型为AbstractControl或任何派生类型的单个参数。我选择了FormGroup,因为这是它的使用情况。

该函数的代码遍历FormGroup中的所有控件,并确保它们的值都等于第一个控件的值。如果是,则返回null(表示没有错误),否则返回childrenNotEqual错误。

所以,当字段不相等时,我们现在在组上有一个无效的状态,但我们仍然需要使用该状态来控制何时显示我们的错误消息。我们的ErrorStateMatcher,ConfirmValidParentMatcher,可以为我们完成这项工作。errorStateMatcher指令要求您指向实现Angular Material提供的ErrorStateMatcher类的类的实例。所以这里使用的是该签名。 ErrorStateMatcher要求实现一个带有代码中所示签名的isErrorState方法。它返回truefalse; true表示存在错误,这使输入元素的状态无效。

此方法中的单行代码非常简单;如果父控件(我们的FormGroup)无效,但字段已被触摸,则它返回true(存在错误)。这与我们在表单上使用的其余字段的<mat-error>的默认行为相一致。

将所有这些内容汇总起来,现在我们拥有一个FormGroup,其中包含一个自定义验证器,当我们的字段不相等时返回错误,并且<mat-error>会在组无效时显示。要查看此功能的实际操作,请参见plunker,其中包含提到的代码实现。

另外,我在这里发布了这个解决方案。


2
Mogsdad,非常感谢您提供的信息。这是我在StackOverflow上的第一个回答,所以我还在学习中。我已经查看了相关信息并相应地更新了我的帖子。如果这样可以接受,请告诉我。 - obsessiveprogrammer
太棒了!感谢您对提示的积极响应。祝你好运! - Mogsdad
1
我正在使用自定义的MatFormFieldControl,而不是matInput。我该怎么办? - Hoang Duc Nguyen
4
我不知道为什么这需要变得如此复杂... 为什么响应式表单的团队不直接在使用FormBuilderFormControl创建Form时添加一项供应错误信息的选项呢?然后,创建某种全局提供程序来配置Validation对象的默认消息。 - Leonid Dashko
2
注册 没有一个来源能够给我完整的答案 你正在处理谷歌的产品(Angular),期望有相当数量的学习曲线和频繁的版本升级。注意:据我所知,调用 this.createForm() 的推荐方式是从 ngOnInit 而不是 constructor - Anand Rockzz
显示剩余3条评论

4
如何创建自定义验证:
如果组件的内部属性“isValid”为false,则将输入状态设置为错误,并显示一条消息。
HTML:
<input matInput
[formControl]="inputControl"
[placeholder]="placeholder"
[readonly]="readonly"
[errorStateMatcher]="matcher">

<mat-error *ngIf="!isValid">
Input not valid.
</mat-error>

TS:

isValid = true;

changeValitationStatus() {
this.matcher = new InputErrorStateMatcher(!this.isValid);
}

matcher = new InputErrorStateMatcher(!this.isValid);



class InputErrorStateMatcher implements ErrorStateMatcher {
    constructor(private errorstate: boolean) {}
    isErrorState(control: FormControl|null, form: FormGroupDirective|NgForm|null):boolean {
    return this.errorstate;
  }
}
以此方式,您可以仅使用formControl进行验证。

3
自定义错误是Angular Material表单的一部分,不需要外部库。
在您的表单中添加自定义验证器:

this.form = this.formBuilder.group({
    formField: ['value', this.customValidator.bind(this)]
});

创建自定义验证器:
private customValidator(control: FormControl): void {
    if (control.valid) {
        if (someCondition === false) {
            setTimeout(() => {
                control.setErrors({ myCustomError: true });
            });
        }
    }
}

将错误添加到您的模板中:
<!-- As part of the form input -->
<form [formGroup]="form">
    <mat-form-field>
        <mat-label>Form field</mat-label>
        <input matInput formControlName="formField">
        <mat-error *ngIf="form.get('formField').hasError('myCustomError')">Custom error message</mat-error>
    </mat-form-field>
</form>

<!-- Outside of the form -->
<span *ngIf="form.get('formField').hasError('myCustomError')">Custom error message</span>

1

obsessiveprogrammer的答案对我来说是正确的,但是我不得不在使用Angular 6和strictNullChecks(这是Angular团队推荐的选项)时将childrenEqual函数更改为:

static childrenEqual: ValidatorFn = (control: AbstractControl): ValidationErrors | null => {
        const f = control as FormGroup;

        const [firstControlName, ...otherControlNames] = Object.keys(f.controls || {});

        if(f.get(firstControlName) == null) {
            return null;
        }

        otherControlNames.forEach(controlName => {
            if(f.get(controlName) == null) {
                return null;
            }
        })

        const isValid = otherControlNames.every(controlName => f.get(controlName)!.value === f.get(firstControlName)!.value);
        return isValid ? null : { childrenNotEqual: true };
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接