提交不会在未触及的自定义表单控件元素上触发必需的错误

submit doesn't trigger required error on untouched custom form control element

我使用 Angular 13.1.1 编写我的应用程序。

我有一个带有电子邮件和密码的简单登录表单,我想为电子邮件创建我自己的表单控制组件,添加我自己的验证器和 mat-error 用于错误消息,还允许父组件检测错误并获取值并能够将其用作表单控件。

我遇到的问题是组件外部的提交按钮不会触发 required 错误。通常我会调试自定义表单控件的验证功能,但单击提交按钮不会触发它。

让我们从自定义电子邮件组件开始。

这是 class:

import {ChangeDetectorRef, Component, forwardRef, OnInit} from '@angular/core';
import {
  AbstractControl,
  ControlValueAccessor, FormBuilder, FormGroup, NG_VALIDATORS,
  NG_VALUE_ACCESSOR,
  ValidationErrors,
  Validator,
  Validators
} from '@angular/forms';

@Component({
  selector: 'app-tuxin-form-email-input',
  templateUrl: './tuxin-form-email-input.component.html',
  styleUrls: ['./tuxin-form-email-input.component.scss'],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      multi:true,
      useExisting:  TuxinFormEmailInputComponent,
    },
    {
      provide: NG_VALIDATORS,
      multi: true,
      useExisting: TuxinFormEmailInputComponent
    }
  ]
})
export class TuxinFormEmailInputComponent implements ControlValueAccessor, Validator, OnInit {

  newChildForm: FormGroup;

  onChange = (email: string) => {};

  onTouched = () => {};

  onValidationChange: any = () => {};

  touched = false;

  disabled = false;

  constructor(private fb: FormBuilder) {
    this.newChildForm = this.fb.group({
      email: [null, [Validators.required, Validators.email]],
    });
  }



  writeValue(email: string) {
    this.newChildForm.get('email')?.setValue(email, { emitEvent: true });
  }

  registerOnChange(onChange: any) {
    this.onChange = onChange;
  }

  registerOnTouched(onTouched: any) {
    this.onTouched = onTouched;
  }

  markAsTouched() {
    if (!this.touched) {
      this.onTouched();
      this.touched = true;
    }
  }

  registerOnValidatorChange?(fn: () => void): void {
    this.onValidationChange = fn;
  }

  ngOnInit(): void {
    this.newChildForm.valueChanges.subscribe((val) => {
      this.onChange(val.email);

      this.onValidationChange();
    });
  }

  setDisabledState(disabled: boolean) {
    this.disabled = disabled;
    disabled ? this.newChildForm.disable() : this.newChildForm.enable();
  }

  get email() {
    return this.newChildForm.get('email');
  }

  validate(control: AbstractControl): ValidationErrors | null {
    if (this.newChildForm?.invalid) {
      return { invalid: true };
    } else {
      return null;
    }
  }
}

这是模板:

<form [formGroup]="newChildForm">
<mat-form-field>
  <mat-label>Email</mat-label>
  <input matInput type="email" formControlName="email"/>
  <mat-error i18n *ngIf="email?.hasError('required')">Email is required</mat-error>
  <mat-error i18n *ngIf="email?.hasError('email')">Email Invalid</mat-error>
</mat-form-field>
</form>

使用邮件组件的组件模板:

<form [formGroup]="loginForm" (ngSubmit)="onSubmit()" novalidate>
<div fxLayout="column" fxLayoutAlign="space-around center">
  <h4 i18n>Login</h4>
  <app-tuxin-form-email-input formControlName="email"></app-tuxin-form-email-input>
  <mat-form-field>
    <mat-label>Password</mat-label>
    <input matInput type="password" formControlName="password" />
    <mat-hint i18n>8-30 characters length</mat-hint>
    <mat-error i18n *ngIf="password?.hasError('required')">Password is required</mat-error>
  </mat-form-field>
  <button mat-raised-button type="submit" color="primary" i18n>Login</button>
</div>
</form>

和主成分的class:

import { Component, OnInit } from '@angular/core';
import {FormBuilder, FormGroup, Validators} from '@angular/forms';
import {ToastrService} from 'ngx-toastr';
import {GraphqlService} from '../graphql.service';

@Component({
  selector: 'app-login-tab',
  templateUrl: './login-tab.component.html',
  styleUrls: ['./login-tab.component.scss'],
})
export class LoginTabComponent implements OnInit {

  loginForm: FormGroup;

  constructor(private formBuilder: FormBuilder, private toastr: ToastrService,
              private gql:GraphqlService) {
    this.loginForm = this.formBuilder.group({
      email: [''],
      password: ['', [Validators.required, Validators.min(8), Validators.max(30)]]
    });
  }

  get email() {
    return this.loginForm.get('email');
  }

  get password() {
    return this.loginForm.get('password');
  }

  onSubmit() {
    if (this.loginForm.status === 'INVALID') {
      this.toastr.error("please fill all forms properly");
    } else {
      const value = this.loginForm.value;
      const email = value.email;
      const password = value.password;
      this.gql.login(email,password).subscribe(({data})=>{
        console.info(data);
      })
      console.log(value);
    }
  }

  ngOnInit(): void {
  }
  
}

如果我触摸实际的电子邮件组件,错误会立即出现,

但是提交按钮没有启动验证功能,知道为什么以及如何更正它吗?

谢谢

我们遇到了同样的问题。我认为这通常是一个 angular 问题,子 -> 父关系适用于 CVA,但父 -> 子关系更复杂。

特别是因为 email formControl 在访问 CVA 时未完全绑定到 loginForm

我们的解决方案是提交 submitFormEvent$ 并将其传递给组件,以便您可以管理何时触发提交验证。

在你的情况下它将是:

import ...

@Component({
  selector: 'app-tuxin-form-email-input',
  templateUrl: './tuxin-form-email-input.component.html',
  styleUrls: ['./tuxin-form-email-input.component.scss'],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      multi:true,
      useExisting:  TuxinFormEmailInputComponent,
    },
    {
      provide: NG_VALIDATORS,
      multi: true,
      useExisting: TuxinFormEmailInputComponent
    }
  ]
})
export class TuxinFormEmailInputComponent implements ControlValueAccessor, Validator, OnInit {
  @Input() submitEvent$ = new Observable<void>()

  newChildForm: FormGroup;

  onChange = (email: string) => {};

  onTouched = () => {};

  onValidationChange: any = () => {};

  touched = false;

  disabled = false;

  constructor(private fb: FormBuilder) {
   ...
  }

  ...

  ngOnInit(): void {
    this.newChildForm.valueChanges.subscribe((val) => {
      this.onChange(val.email);
      this.onValidationChange();
    });
    this.submitEvent$.subscribe(() => {
          this.newChildForm.markAsTouched()
      })
  }
...
}

对于基础组件

import ...

@Component({
  selector: 'app-login-tab',
  templateUrl: './login-tab.component.html',
  styleUrls: ['./login-tab.component.scss'],
})
export class LoginTabComponent implements OnInit {
  submitFormEvent$ = new Subject<void>()

  loginForm: FormGroup;

  constructor(private formBuilder: FormBuilder, private toastr: ToastrService,
              private gql:GraphqlService) {
    ...
  }

  get email() {
    return this.loginForm.get('email');
  }

  get password() {
    return this.loginForm.get('password');
  }

  onSubmit() {
    this.submitFormEvent.next();
    if (this.loginForm.status === 'INVALID') {
      this.toastr.error("please fill all forms properly");
    } else {
      const value = this.loginForm.value;
      const email = value.email;
      const password = value.password;
      this.gql.login(email,password).subscribe(({data})=>{
        console.info(data);
      })
      console.log(value);
    }
  }

  ngOnInit(): void {
  }
  
}