首页 文章

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

提问于
浏览
9

我来找你谈论角材料的问题 . 事实上,我认为这是一个问题,但我更喜欢先寻找误解 .

关于我的问题的第一件事是上下文,我尝试做一个包含两个输入的简单表单:密码及其'确认 .

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)]],
}

我的兴趣是匹配PasswordsForm 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;
    }
}

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

我用评论包围了有趣的代码 .

现在,一些解释:使用标签,当触摸password2时,显示我的错误:

Password2 just touched

但是,当我写错密码时,错误不再显示:

Wrong password2

首先我以为我误解了自定义验证器的使用 . 但是,当我更换整个东西工作完美!

replace error by hint

<mat-hint ngxErrors="matchinghPasswordsForm">
    <p ngxError="matchingPassword" [when]="['dirty', 'touched']">{{passwordMatchErrorMessage}}</p>
</mat-hint>

With mat-hint tag

我希望我很清楚,在材料设计github上发布问题之前我真的想要你的观点 .

如果我误解了什么,请点亮我错过的东西 .

最后一件事,我的测试是用ngxerrors和* ngif完成的 . 为了更具可读性,我的代码示例仅使用ngxerrors .

提前感谢您的参与时间 .

2 回答

  • 0

    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用于此用例,让我们首先看一下组件类 .

    组件类

    这是一个Angular Component类,它使用FormBuilder为表单设置验证 .

    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,其中包含所提及代码的实现 .

    另外,我在博客上发布了这个解决方案here .

  • 25

    obsessiveprogrammer的回答对我来说是正确的,但是我必须用角度6和 strictNullChecks (这是角度团队推荐的选项)更改 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 };
        }
    

相关问题