Отображать ошибку пользовательского валидатора с ошибкой mat

Я прихожу к вам за разговором о проблеме с угловым материалом. На самом деле, я думаю, что это проблема, но я предпочитаю сначала искать недоразумения.

Первое, что касается моей проблемы, это контекст, я пытаюсь сделать простую форму, содержащую два входа: пароль и его "подтверждение".

пользовательский 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. Вы можете увидеть на нем валидатор.

Здесь валидатор:

согласующие-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.

пользователя 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

Надеюсь, что я был ясен, я действительно хочу вашу точку зрения, прежде чем опубликовать вопрос о материальном дизайне github.

Если я что-то проигнорировал, пожалуйста, зажгите мой огонь по тому, что я пропустил.

Последнее, мои тесты проводились с ngxerrors и * ngif. Чтобы быть более читаемым, в моем примере кода используются только ngxerrors.

Заранее благодарим вас за время, которое вы возьмете.

Ответы

Ответ 1

Алекс прав. Вы должны использовать ErrorStateMatcher. Я должен был сделать много исследований, чтобы понять это, и не было ни одного источника, который дал бы мне весь ответ. Мне пришлось собрать информацию, которую я узнал из нескольких источников, чтобы решить мое собственное решение проблемы. Надеюсь, следующий пример спасет вас от головной боли, которую я испытал.

Форма

Ниже приведен пример формы, использующей элементы углового материала для страницы регистрации пользователя.

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

Как вы можете видеть, я использую теги <mat-form-field>, <input matInput> и <mat-error> из углового материала. Моя первая мысль заключалась в том, чтобы добавить директиву *ngIf для управления, когда появляются разделы <mat-error>, но это не имеет никакого эффекта! Видимость фактически контролируется состоянием (и "затронутым" статусом) поля <mat-form-field>, и нет проверочного удостоверения для проверки равенства для другого поля формы в HTML или Angular. Именно здесь errorStateMatcher в errorStateMatcher директивы errorStateMatcher в полях подтверждения.

Директива errorStateMatcher встроена в "Угловой материал" и предоставляет возможность использовать собственный метод для определения допустимости элемента управления формой <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])(?=.*[[email protected]#$%^&*])[[email protected]#$%^&*]{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 ([email protected])',
    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 требует, чтобы вы указывали на экземпляр класса, который реализует предоставленный класс ErrorStateMatcher в Angular Material. Итак, это подпись, используемая здесь. ErrorStateMatcher требует реализации метода isErrorState с сигнатурой, показанной в коде. Он возвращает true или false; true указывает, что существует ошибка, которая делает статус входного элемента недействительным.

Одна строка кода в этом методе довольно проста; он возвращает true (ошибка существует), если родительский элемент управления (наша FormGroup) недействителен, но только если поле было затронуто. Это соответствует поведению <mat-error> по умолчанию, которое мы используем для остальных полей формы.

Чтобы собрать все вместе, теперь у нас есть FormGroup с настраиваемым валидатором, который возвращает ошибку, когда наши поля не равны, и <mat-error> которая отображается, когда группа недействительна. Чтобы увидеть эту функциональность в действии, вот рабочий плункер с реализацией упомянутого кода.

Кроме того, я написал это решение здесь.

Ответ 2

obsessiveprogrammer ответ был правильным для меня, но я должен был изменить childrenEqual функцию с угловыми 6 и strictNullChecks (который является вариант рекомендуется угловой командой) к этому:

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

Ответ 3

Как создать пользовательскую проверку:

если внутреннее свойство '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.