如何将验证器添加到订阅的自定义输入?

How to add Validator to Custom Input from Subscribe?

我有一个用户表单,其中必须检查两个输入 'Username' 和 'Email' 是否重复。 ▼

        this.userForm = this.formBuilder.group({
            username: [
                this.isEdit ? this.data.user?.nameIdentifier : null,
                Validators.required
            ],
            email: [
                this.isEdit ? this.data.user?.email : null,
                [Validators.required, Validators.email]
            ]
        });

我有自定义验证器,它需要加载异步数据,然后使用该数据检查重复项。因此,我将该验证器置于订阅中,并在异步数据加载时将该验证设置为表单控件。 ▼

        this.userService
            .getUsers({ pageSize: 5000 })
            .pipe(takeUntil(this.destroy$))
            .subscribe((users: IGetUsersRes) => {
                this.userForm
                    .get('username')
                    ?.setValidators(
                        duplicateNameValidator(
                            users.list, // this one
                            ['nameIdentifier'],
                            this.data.user
                        )
                    );

                this.userForm
                    .get('email')
                    ?.setValidators(
                        duplicateNameValidator(
                            users.list, // this one
                            ['email'],
                            this.data.user
                        )
                    );
            });

这在当时有效,但现在当我切换到我的自定义输入时,来自订阅的验证不会分配给自定义输入(可能是因为它处于异步状态)。我的自定义输入以这种方式接受父表单控件验证 ▼

export class InputComponent implements OnInit, OnDestroy, ControlValueAccessor {
    inputControl = new FormControl();
    isDisabled!: boolean;

    @Input() debounce = false;

    onChange: any = () => {
        // any
    };
    onTouched: any = () => {
        // any
    };

    constructor(@Self() @Optional() public ngControl: NgControl) {
        this.ngControl && (this.ngControl.valueAccessor = this);
    }

    ngOnInit(): void {
        this.updateInputValue();
        this.setValidators();
    }

    ngOnDestroy(): void {
        this.destroy$.next();
        this.destroy$.complete();
    }

    setValidators(): void {
        const control = this.ngControl.control;
        const validators: ValidatorFn[] = control?.validator
            ? [control?.validator]
            : [];
        this.inputControl.setValidators(validators);
    }

    updateInputValue(): void {
        this.inputControl.valueChanges
            .pipe(
                takeUntil(this.destroy$),
                debounceTime(this.debounce ? 500 : 0),
                distinctUntilChanged()
            )
            .subscribe((res) => {
                this.onChange(res);
            });
    }

    clearInput(): void {
        this.inputControl.reset(null, { emitEvent: false });
        this.onChange();
    }

    // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
    writeValue(value: any): void {
        this.inputControl.setValue(value);
    }

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

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

    setDisabledState(disabled: boolean): void {
        this.isDisabled = disabled;
    }
}

我不确定我做的每件事都是正确的,所以有人知道如何解决这个问题吗?我认为父控件验证是在 ngOnInit 上分配的,但由于我的 duplicateNameValidator() 处于异步状态,因此需要额外处理,而且我不知道如何在我的自定义输入中查看它。在 Google 中搜索结果不佳,google 被误导。

如果你想用响应式表单做一些异步的事情,我们有专门的异步验证器!所以首先我会使用它,所以我建议以下...

创建验证函数。我在这里传递表单控件名称和 http 请求结果的可观察值作为参数。为了获取我们在组件中分配的可观察对象,附加一个 shareReplay 以便只发出一次请求。

在这里,我直接从我的组件中懒惰地使用 http,像您已经在做的那样使用服务 ;)

users$ = this.http.get('https://jsonplaceholder.typicode.com/users').pipe(
  shareReplay(1)
);

然后我们将 user$ 传递给异步验证器。然后我们以与自定义同步验证器类似的方式使用此验证器,但异步验证器需要是第三个参数:

this.reactiveForm = this.fb.group({
  username: ['', [], [validate('username', this.users$)]],
  email: ['', [], [validate('email', this.users$)]],
});

验证函数:

export function validate(ctrlName: string, result$): ValidatorFn {
  return (ctrl: AbstractControl): ValidationErrors | null => {
    if (ctrl) {
      return result$.pipe(
        map((values: any) => {
          const found = values.find((x) => x[ctrlName].toLowerCase() === ctrl.value.toLowerCase());
          if (found) {
            return { notValid: true };
          }
          return null;
        }),
        catchError(() => of(null))
      );
    }
    return of(null);
  };
}

因此,当我们将控件名称作为字符串传递时,这个验证器将同时适用于您的用户名和电子邮件。

然后您可以在自定义组件中像任何其他同步验证器一样显示此错误:

 <input type="text" [formControl]="$any(control)">
 <small *ngIf="control.hasError('notValid')">Value already taken!</small>

这是一个 STACKBLITZ 上面的代码 - 也使用自定义输入。