如何在角度2中向异步验证器添加去抖时间?

时间:2016-04-28 15:23:30

标签: angular validation asynchronous debouncing

这是我的异步验证器,它没有去抖时间,我该如何添加它?

static emailExist(_signupService:SignupService) {
  return (control:Control) => {
    return new Promise((resolve, reject) => {
      _signupService.checkEmail(control.value)
        .subscribe(
          data => {
            if (data.response.available == true) {
              resolve(null);
            } else {
              resolve({emailExist: true});
            }
          },
          err => {
            resolve({emailExist: true});
          })
      })
    }
}

14 个答案:

答案 0 :(得分:65)

Angular 4+,使用Observable.timer(debounceTime)

@izupet的答案是正确的,但值得注意的是,使用Observable时更简单:

emailAvailability(control: Control) {
    return Observable.timer(500).switchMap(()=>{
      return this._service.checkEmail({email: control.value})
        .mapTo(null)
        .catch(err=>Observable.of({availability: true}));
    });
}

由于角度4已被释放,如果发送新值进行检查,之前的Observable将被取消订阅,因此您实际上不需要管理setTimeout / {{1}你自己的逻辑。

答案 1 :(得分:27)

实现这一点实际上非常简单(它不适用于您的情况,但它是一般示例)

private emailTimeout;

emailAvailability(control: Control) {
    clearTimeout(this.emailTimeout);
    return new Promise((resolve, reject) => {
        this.emailTimeout = setTimeout(() => {
            this._service.checkEmail({email: control.value})
                .subscribe(
                    response    => resolve(null),
                    error       => resolve({availability: true}));
        }, 600);
    });
}

答案 2 :(得分:11)

由于在input事件用于触发更新时直接触发验证器,因此无法开箱即用。请参阅源代码中的这一行:

如果您想在此级别利用去抖时间,则需要获得与相应DOM元素的input事件直接链接的observable。 Github中的这个问题可以为您提供上下文:

在您的情况下,解决方法是使用fromEvent可观察方法实现自定义值访问器。

以下是一个示例:

const DEBOUNCE_INPUT_VALUE_ACCESSOR = new Provider(
  NG_VALUE_ACCESSOR, {useExisting: forwardRef(() => DebounceInputControlValueAccessor), multi: true});

@Directive({
  selector: '[debounceTime]',
  //host: {'(change)': 'doOnChange($event.target)', '(blur)': 'onTouched()'},
  providers: [DEBOUNCE_INPUT_VALUE_ACCESSOR]
})
export class DebounceInputControlValueAccessor implements ControlValueAccessor {
  onChange = (_) => {};
  onTouched = () => {};
  @Input()
  debounceTime:number;

  constructor(private _elementRef: ElementRef, private _renderer:Renderer) {

  }

  ngAfterViewInit() {
    Observable.fromEvent(this._elementRef.nativeElement, 'keyup')
      .debounceTime(this.debounceTime)
      .subscribe((event) => {
        this.onChange(event.target.value);
      });
  }

  writeValue(value: any): void {
    var normalizedValue = isBlank(value) ? '' : value;
    this._renderer.setElementProperty(this._elementRef.nativeElement, 'value', normalizedValue);
  }

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

并以这种方式使用它:

function validator(ctrl) {
  console.log('validator called');
  console.log(ctrl);
}

@Component({
  selector: 'app'
  template: `
    <form>
      <div>
        <input [debounceTime]="2000" [ngFormControl]="ctrl"/>
      </div>
      value : {{ctrl.value}}
    </form>
  `,
  directives: [ DebounceInputControlValueAccessor ]
})
export class App {
  constructor(private fb:FormBuilder) {
    this.ctrl = new Control('', validator);
  }
}

请参阅此plunkr:https://plnkr.co/edit/u23ZgaXjAvzFpeScZbpJ?p=preview

答案 3 :(得分:7)

带去抖功能的Angular 9+ asyncValidator

@ n00dl3具有正确的答案。我喜欢依靠Angular代码退订并通过抛出定时暂停来创建新的异步验证器。自从编写答案以来,Angular和RxJS API就已经发展了,所以我要发布一些更新的代码。

我还做了一些更改。 (1)代码应报告捕获的错误,而不是将其隐藏在电子邮件地址的匹配项下,否则我们将使用户感到困惑。如果网络中断,为什么要说电子邮件匹配?! UI演示代码将区分电子邮件冲突和网络错误。 (2)验证者应在延时之前捕获控件的值,以防止出现任何可能的比赛情况。 (3)使用delay代替timer,因为后者将每半秒触发一次,并且如果我们的网络速度较慢并且电子邮件检查花费很长时间(一秒),计时器将继续触发switchMap和通话将永远无法完成。

Angular 9+兼容片段:

emailAvailableValidator(control: AbstractControl) {
  return of(control.value).pipe(
    delay(500),
    switchMap((email) => this._service.checkEmail(email).pipe(
      map(isAvail => isAvail ? null : { unavailable: true }),
      catchError(err => { error: err }))));
}

PS:任何想深入探究Angular源代码的人(我强烈建议这样做),您可以找到运行异步验证here的Angular代码以及取消订阅的here代码this。所有相同的文件,都在updateValueAndValidity下。

答案 4 :(得分:4)

使用RxJ的替代解决方案可以如下。

/**
 * From a given remove validation fn, it returns the AsyncValidatorFn
 * @param remoteValidation: The remote validation fn that returns an observable of <ValidationErrors | null>
 * @param debounceMs: The debounce time
 */
debouncedAsyncValidator<TValue>(
  remoteValidation: (v: TValue) => Observable<ValidationErrors | null>,
  remoteError: ValidationErrors = { remote: "Unhandled error occurred." },
  debounceMs = 300
): AsyncValidatorFn {
  const values = new BehaviorSubject<TValue>(null);
  const validity$ = values.pipe(
    debounceTime(debounceMs),
    switchMap(remoteValidation),
    catchError(() => of(remoteError)),
    take(1)
  );

  return (control: AbstractControl) => {
    if (!control.value) return of(null);
    values.next(control.value);
    return validity$;
  };
}

用法:

const validator = debouncedAsyncValidator<string>(v => {
  return this.myService.validateMyString(v).pipe(
    map(r => {
      return r.isValid ? { foo: "String not valid" } : null;
    })
  );
});
const control = new FormControl('', null, validator);

答案 5 :(得分:2)

RxJS 6示例:

import { of, timer } from 'rxjs';
import { catchError, mapTo, switchMap } from 'rxjs/operators';      

validateSomething(control: AbstractControl) {
    return timer(SOME_DEBOUNCE_TIME).pipe(
      switchMap(() => this.someService.check(control.value).pipe(
          // Successful response, set validator to null
          mapTo(null),
          // Set error object on error response
          catchError(() => of({ somethingWring: true }))
        )
      )
    );
  }

答案 6 :(得分:1)

这是我的实时Angular项目中使用rxjs6的示例

import { ClientApiService } from '../api/api.service';
import { AbstractControl } from '@angular/forms';
import { HttpParams } from '@angular/common/http';
import { map, switchMap } from 'rxjs/operators';
import { of, timer } from 'rxjs/index';

export class ValidateAPI {
  static createValidator(service: ClientApiService, endpoint: string, paramName) {
    return (control: AbstractControl) => {
      if (control.pristine) {
        return of(null);
      }
      const params = new HttpParams({fromString: `${paramName}=${control.value}`});
      return timer(1000).pipe(
        switchMap( () => service.get(endpoint, {params}).pipe(
            map(isExists => isExists ? {valueExists: true} : null)
          )
        )
      );
    };
  }
}

这就是我以反应形式使用它的方式

this.form = this.formBuilder.group({
page_url: this.formBuilder.control('', [Validators.required], [ValidateAPI.createValidator(this.apiService, 'meta/check/pageurl', 'pageurl')])
});

答案 7 :(得分:1)

这里有一个返回使用debounceTime(...)distinctUntilChanged()的验证函数的服务:

@Injectable({
  providedIn: 'root'
})
export class EmailAddressAvailabilityValidatorService {

  constructor(private signupService: SignupService) {}

  debouncedSubject = new Subject<string>();
  validatorSubject = new Subject();

  createValidator() {

    this.debouncedSubject
      .pipe(debounceTime(500), distinctUntilChanged())
      .subscribe(model => {

        this.signupService.checkEmailAddress(model).then(res => {
          if (res.value) {
            this.validatorSubject.next(null)
          } else {
            this.validatorSubject.next({emailTaken: true})
          }
        });
      });

    return (control: AbstractControl) => {

      this.debouncedSubject.next(control.value);

      let prom = new Promise<any>((resolve, reject) => {
        this.validatorSubject.subscribe(
          (result) => resolve(result)
        );
      });

      return prom
    };
  }
}

用法:

emailAddress = new FormControl('',
    [Validators.required, Validators.email],
    this.validator.createValidator() // async
  );

如果添加验证器Validators.requiredValidators.email,则仅当输入字符串为非空且有效的电子邮件地址时,才会发出请求。应该这样做以避免不必要的API调用。

答案 8 :(得分:1)

保持简单:没有超时,没有延迟,没有自定义的可观察值

...
// assign async validator to a field
this.cardAccountNumber.setAsyncValidators(this.uniqueCardAccountValidatorFn());
...
// subscribe to control.valueChanges and define pipe
uniqueCardAccountValidatorFn(): AsyncValidatorFn {
  return control => control.valueChanges
    .pipe(
      debounceTime(400),
      distinctUntilChanged(),
      switchMap(value => this.customerService.isCardAccountUnique(value)),
      map((unique: boolean) => (unique ? null : {'cardAccountNumberUniquenessViolated': true})),
      first()); // important to make observable finite
}

答案 9 :(得分:1)

事情可以简化一点

export class SomeAsyncValidator {
   static createValidator = (someService: SomeService) => (control: AbstractControl) =>
       timer(500)
           .pipe(
               map(() => control.value),
               switchMap((name) => someService.exists({ name })),
               map(() => ({ nameTaken: true })),
               catchError(() => of(null)));
}

答案 10 :(得分:0)

我遇到了同样的问题。我想要一个解除输入的解决方案,只在输入改变时请求后端。

验证程序中带有计时器的所有变通方法都存在问题,即每次按键都会请求后端。他们只是对验证响应进行了辩护。这不是我们打算做的。您希望对输入进行去抖动和区分,然后才能请求后端。

我的解决方案如下(使用反应形式和材料2):

组件

@Component({
    selector: 'prefix-username',
    templateUrl: './username.component.html',
    styleUrls: ['./username.component.css']
})
export class UsernameComponent implements OnInit, OnDestroy {

    usernameControl: FormControl;

    destroyed$ = new Subject<void>(); // observes if component is destroyed

    validated$: Subject<boolean>; // observes if validation responses
    changed$: Subject<string>; // observes changes on username

    constructor(
        private fb: FormBuilder,
        private service: UsernameService,
    ) {
        this.createForm();
    }

    ngOnInit() {
        this.changed$ = new Subject<string>();
        this.changed$

            // only take until component destroyed
            .takeUntil(this.destroyed$)

            // at this point the input gets debounced
            .debounceTime(300)

            // only request the backend if changed
            .distinctUntilChanged()

            .subscribe(username => {
                this.service.isUsernameReserved(username)
                    .subscribe(reserved => this.validated$.next(reserved));
            });

        this.validated$ = new Subject<boolean>();
        this.validated$.takeUntil(this.destroyed$); // only take until component not destroyed
    }

    ngOnDestroy(): void {
        this.destroyed$.next(); // complete all listening observers
    }

    createForm(): void {
        this.usernameControl = this.fb.control(
            '',
            [
                Validators.required,
            ],
            [
                this.usernameValodator()
            ]);
    }

    usernameValodator(): AsyncValidatorFn {
        return (c: AbstractControl) => {

            const obs = this.validated$
                // get a new observable
                .asObservable()
                // only take until component destroyed
                .takeUntil(this.destroyed$)
                // only take one item
                .take(1)
                // map the error
                .map(reserved => reserved ? {reserved: true} : null);

            // fire the changed value of control
            this.changed$.next(c.value);

            return obs;
        }
    }
}

模板

<mat-form-field>
    <input
        type="text"
        placeholder="Username"
        matInput
        formControlName="username"
        required/>
    <mat-hint align="end">Your username</mat-hint>
</mat-form-field>
<ng-template ngProjectAs="mat-error" bind-ngIf="usernameControl.invalid && (usernameControl.dirty || usernameControl.touched) && usernameControl.errors.reserved">
    <mat-error>Sorry, you can't use this username</mat-error>
</ng-template>

答案 11 :(得分:0)

对于仍然对此主题感兴趣的任何人,请务必在angular 6 document中注意这一点:

  
      
  1. 他们必须返回一个Promise或Observable,
  2.   
  3. 可观察到的返回必须是有限的,这意味着它必须在某个时候完成。要将无限的可观察对象转换为有限的可观察对象,请通过诸如first,last,take或takeUntil之类的过滤运算符来传递可观察对象。
  4.   

请注意上面的第二个要求。

这是一个AsyncValidatorFn的实现:

const passwordReapeatValidator: AsyncValidatorFn = (control: FormGroup) => {
  return of(1).pipe(
    delay(1000),
    map(() => {
      const password = control.get('password');
      const passwordRepeat = control.get('passwordRepeat');
      return password &&
        passwordRepeat &&
        password.value === passwordRepeat.value
        ? null
        : { passwordRepeat: true };
    })
  );
};

答案 12 :(得分:0)

尝试使用计时器。

static verificarUsuario(usuarioService: UsuarioService) {
    return (control: AbstractControl) => {
        return timer(1000).pipe(
            switchMap(()=>
                usuarioService.buscar(control.value).pipe(
                    map( (res: Usuario) => { 
                        console.log(res);
                        return Object.keys(res).length === 0? null : { mensaje: `El usuario ${control.value} ya existe` };
                    })
                )
            )
        )
    }
}

答案 13 :(得分:0)

由于我们正在尝试减少向服务器发出的请求数量,因此我还建议添加检查以确保仅将有效电子邮件发送到服务器进行检查

我使用了来自 JavaScript: HTML Form - email validation

的简单 RegEx

我们还使用 timer(1000) 创建一个在 1 秒后执行的 Observable。

设置这两项后,我们只检查有效的电子邮件地址,并且仅在用户输入后 1 秒后检查。如果发出新请求,switchMap 操作员将取消之前的请求


const emailRegExp = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/;
const emailExists = control =>
  timer(1000).pipe(
    switchMap(() => {
      if (emailRegExp.test(control.value)) {
        return MyService.checkEmailExists(control.value);
      }
      return of(false);
    }),
    map(exists => (exists ? { emailExists: true } : null))
  );

然后我们可以将此验证器与 Validator.pattern() 函数一起使用

  myForm = this.fb.group({
    email: [ "", { validators: [Validators.pattern(emailRegExp)], asyncValidators: [emailExists] }]
  });

下面是一个Sample demo on stackblitz