Angular 18 自定义输入:FormControl 有奇怪的行为

问题描述 投票:0回答:1

我在使用自定义输入组件时遇到一些奇怪的行为。

首先,我构建了一个简单的抽象类,它具有组件的主要“功能”和方法,然后,输入组件的代码很少:

// Abstract class

export abstract class BaseFormInput<T> implements ControlValueAccessor, Validator, AfterViewInit, OnDestroy {
    @Input() label: string
    @Output() onChange: EventEmitter<T> = new EventEmitter<T>()

    private changeInternal: (obj: T) => void
    private changeSub: Subscription
    private disabled$ = new BehaviorSubject(false)
    private required$ = new BehaviorSubject(false)

    public input = new FormControl(null)

    ngOnDestroy() {
        this.changeSub.unsubscribe()
    }

    ngAfterViewInit() {
        this.changeSub = this.input.valueChanges.subscribe(v => {
            if (!this.disabled$.getValue()) {
                this.onChange.emit(v)
                this.changeInternal(v)
            }
        })
    }

    writeValue = (obj: T) => this.input.setValue(obj)

    registerOnChange = (fn: (obj: T) => void) => this.changeInternal = fn

    registerOnTouched = (_fn: (obj: any) => void) => {}

    setDisabledState = (isDisabled: boolean) => this.disabled$.next(isDisabled)

    validate(control: AbstractControl): ValidationErrors {
        this.required$.next(control.hasValidator(Validators.required))

        // THIS LINE HAS WEIRD BEHAVIOR
        console.log(control, control.errors)

        return null
    }

    public get isDisabled$(){
        return this.disabled$.asObservable()
    }

    public get isRequired$(){
        return this.required$.asObservable()
    }
}

输入组件简单设计如下:

@Component({
    selector: "ec-input-text",
    template: `<div class="form-control">
            <label *ngIf="label">
                {{ label }}
                <span *ngIf="isRequired$ | async">*</span>
            </label>
            <input *ngIf="type !== 'textarea'" [type]="type" [formControl]="input" [attr.disabled]="isDisabled$ | async" />
            <textarea *ngIf="type === 'textarea'" [formControl]="input" [attr.disabled]="isDisabled$ | async"></textarea>
            <ng-template></ng-template>
        </div>`,
    changeDetection: ChangeDetectionStrategy.OnPush,
    providers: [
        { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => InputTextComponent), multi: true },
        { provide: NG_VALIDATORS, useExisting: forwardRef(() => InputTextComponent), multi: true }
    ]
})
export class InputTextComponent extends BaseFormInput<string> {
    @Input() type: "text" | "password" | "email" | "textarea" = "text"
    @Input() maxLength: number
}

最后,我创建了一个寄存器组件,它使用输入。

HTML:

<form [formGroup]="form">
    <ec-input-text label="First name" formControlName="firstName" />
    <ec-input-text label="Last name" formControlName="lastName" />
    <ec-input-text label="E-mail" formControlName="email" type="email" />
    <ec-input-text label="Password" formControlName="password" type="password" />
</form>

寄存器组件的 TS 有一个这样的公共属性:

public form = new FormGroup({
        firstName: new FormControl(null, [Validators.required, Validators.maxLength(50)]),
        lastName: new FormControl(null, [Validators.required, Validators.maxLength(50)]),
        email: new FormControl(null, [Validators.required, Validators.maxLength(100)]),
        password: new FormControl(null, Validators.required)
    })

现在,问题如下:在抽象类的 validate 方法中(我在其中添加了注释),我尝试记录控件错误,但出现了一个奇怪的行为:记录 formControl 时,我可以看到在控制台中,属性errors为空,但是如果我记录control.errors,它会记录:

{ required: true }

即使控件有效并且我输入了值(事实上,control.value有一个值并且结果有效)。 所以如果我这样做:

console.log(control)

我展开它,错误为空(预期行为,正确!)

但是如果我这样做:

console.log(control.errors)

它已被评估(不正确,控制有效!)

我该如何解决这个问题?预先感谢!

javascript angular forms
1个回答
0
投票

请勿在反应式表单中使用

attr.disabled
disabled
,您可以尝试使用指令或仅使用反应式表单方法手动禁用它。它可能会导致难以解决的错误,因此建议以编程方式禁用它。

在 Angular 中使用反应式表单时禁用表单控件


您没有在正确的位置检查验证错误,验证方法是为您插入自定义验证而设计的,该自定义验证会针对您的控件的值进行验证,大多数情况下不会涉及检查其他错误(未正确显示)。

当您在该位置检查控件的错误时,它显示的是之前的状态,所以我猜其他验证没有更新。所以请在

validate
函数内进行验证,不要检查其他错误。

您还可以导入

ControlContainer
,获取
formControlName
并获取实际的表单控件,您可以用它来检查
Validators.required
是否已添加。虽然这并不是万无一失,但它是访问自定义表单元素内的表单控件的一个很好的起点。

constructor(public inj: Injector) {}

ngOnInit() {
  const controlContainer = this.inj.get(ControlContainer);
  this.control = controlContainer!.control!.get(this.formControlName);
  this.required$.next(this.control!.hasValidator(Validators.required));
}

完整代码:

import {
  Component,
  OnDestroy,
  AfterViewInit,
  Input,
  Output,
  EventEmitter,
  ChangeDetectionStrategy,
  forwardRef,
  Directive,
  inject,
  Injector,
} from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import {
  ReactiveFormsModule,
  ControlValueAccessor,
  Validator,
  FormGroup,
  FormControl,
  AbstractControl,
  ValidationErrors,
  Validators,
  NG_VALUE_ACCESSOR,
  NG_VALIDATORS,
  NgControl,
  ControlContainer,
} from '@angular/forms';
import { BehaviorSubject, Subscription } from 'rxjs';
import { CommonModule } from '@angular/common';

@Directive()
export abstract class BaseFormInput<T>
  implements ControlValueAccessor, Validator, AfterViewInit, OnDestroy
{
  @Input() label!: string;
  @Input() formControlName!: string;
  @Output() onChange: EventEmitter<T> = new EventEmitter<T>();

  private changeInternal!: (obj: T) => void;
  private changeSub!: Subscription;
  private disabled$ = new BehaviorSubject(false);
  private required$ = new BehaviorSubject(false);

  public input = new FormControl(null);

  control!: AbstractControl<any, any> | null;

  ngOnDestroy() {
    this.changeSub.unsubscribe();
  }

  constructor(public inj: Injector) {}

  ngOnInit() {
    const controlContainer = this.inj.get(ControlContainer);
    this.control = controlContainer!.control!.get(this.formControlName);
    this.required$.next(this.control!.hasValidator(Validators.required));
  }

  ngAfterViewInit() {
    this.changeSub = this.input.valueChanges.subscribe((v: any) => {
      if (!this.disabled$.getValue()) {
        this.onChange.emit(v);
        this.changeInternal(v);
      }
    });
  }

  writeValue = (obj: any) => this.input.setValue(obj);

  registerOnChange = (fn: (obj: T) => void) => (this.changeInternal = fn);

  registerOnTouched = (_fn: (obj: any) => void) => {};

  setDisabledState = (isDisabled: boolean) => this.disabled$.next(isDisabled);

  validate(control: AbstractControl): ValidationErrors | null {
    // THIS LINE HAS WEIRD BEHAVIOR
    // console.log(
    //   control,
    //   control.getError('required'),
    //   control.errors,
    //   control.value
    // );
    return null;
  }

  public get isDisabled$() {
    return this.disabled$.asObservable();
  }

  public get isRequired$() {
    return this.required$.asObservable();
  }
}

@Component({
  selector: 'ec-input-text',
  standalone: true,
  imports: [ReactiveFormsModule, CommonModule],
  template: `<div class="form-control">
          <label *ngIf="label">
              {{ label }}
              <span *ngIf="isRequired$ | async">*</span>
          </label>
          <input *ngIf="type !== 'textarea'" [type]="type" [formControl]="input" />
          <textarea *ngIf="type === 'textarea'" [formControl]="input" ></textarea>
          <ng-template></ng-template>
      </div>`,
  changeDetection: ChangeDetectionStrategy.OnPush,
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => InputTextComponent),
      multi: true,
    },
    {
      provide: NG_VALIDATORS,
      useExisting: forwardRef(() => InputTextComponent),
      multi: true,
    },
  ],
})
export class InputTextComponent extends BaseFormInput<string> {
  @Input() type: 'text' | 'password' | 'email' | 'textarea' = 'text';
  @Input() maxLength!: number;
}

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [ReactiveFormsModule, CommonModule, InputTextComponent],
  template: `
    <form [formGroup]="form">
      <ec-input-text label="First name" formControlName="firstName" />
      <ec-input-text label="Last name" formControlName="lastName" />
      <ec-input-text label="E-mail" formControlName="email" type="email" />
      <ec-input-text label="Password" formControlName="password" type="password" />
  </form><br/>
  {{form.errors | json}}
  <br/>
  {{form.controls.firstName.errors | json}}
  `,
})
export class App {
  public form = new FormGroup({
    firstName: new FormControl(null, [
      Validators.required,
      Validators.maxLength(50),
    ]),
    lastName: new FormControl(null, [
      Validators.required,
      Validators.maxLength(50),
    ]),
    email: new FormControl(null, [
      Validators.required,
      Validators.maxLength(100),
    ]),
    password: new FormControl(null, Validators.required),
  });
}

bootstrapApplication(App);

Stackblitz 演示

© www.soinside.com 2019 - 2024. All rights reserved.