Come aggiungere il tempo di rimbalzo a un validatore asincrono in angular 2?

Questo è il mio convalidatore asincrono, non ha un tempo di rimbalzo, come posso aggiungerlo?

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

In realtà è piuttosto semplice da raggiungere (non è per il tuo caso, ma è un esempio generale)

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

Angular 4+, Using Observable.timer(debounceTime) :

La risposta di @izupet è giusta ma vale la pena notare che è ancora più semplice quando si utilizza Observable:

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

Poiché è stato rilasciato il 4 angular, se viene inviato un nuovo valore per il controllo, il precedente Observable verrà annullato, quindi non è necessario gestire da solo la logica setTimeout / clearTimeout .

Non è ansible out-of-box poiché il validatore viene triggersto direttamente quando l’evento di input viene utilizzato per triggersre gli aggiornamenti. Vedi questa riga nel codice sorgente:

Se si desidera sfruttare un tempo di rimbalzo a questo livello, è necessario ottenere un osservabile direttamente collegato all’evento di input dell’elemento DOM corrispondente. Questo problema in Github potrebbe darti il ​​contesto:

Nel tuo caso, una soluzione alternativa sarebbe implementare una funzione di accesso al valore personalizzato sfruttando il metodo fromEvent di osservabile.

Ecco un esempio:

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

E usalo in questo modo:

 function validator(ctrl) { console.log('validator called'); console.log(ctrl); } @Component({ selector: 'app' template: ` 
value : {{ctrl.value}}
`, directives: [ DebounceInputControlValueAccessor ] }) export class App { constructor(private fb:FormBuilder) { this.ctrl = new Control('', validator); } }

Vedi questo plunkr: https://plnkr.co/edit/u23ZgaXjAvzFpeScZbpJ?p=preview .

una soluzione alternativa con RxJ può essere la seguente.

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

Uso:

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

Ho avuto lo stesso problema. Volevo una soluzione per il debouncing dell’input e richiedevo solo il backend quando l’input è cambiato.

Tutti i workaround con un timer nel validatore hanno il problema, che richiedono il back-end con ogni tasto premuto. Rimbalzano solo la risposta di convalida. Questo non è ciò che è destinato a fare. Si desidera che l’input venga rimosso e distinto e solo successivamente per richiedere il back-end.

La mia soluzione per questo è la seguente (usando forms reattive e materiale2):

Il componente

 @Component({ selector: 'prefix-username', templateUrl: './username.component.html', styleUrls: ['./username.component.css'] }) export class UsernameComponent implements OnInit, OnDestroy { usernameControl: FormControl; destroyed$ = new Subject(); // observes if component is destroyed validated$: Subject; // observes if validation responses changed$: Subject; // observes changes on username constructor( private fb: FormBuilder, private service: UsernameService, ) { this.createForm(); } ngOnInit() { this.changed$ = new Subject(); 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(); 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; } } } 

Il template

   Your username   Sorry, you can't use this username  

RxJS 6 esempio:

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

Ecco un esempio dal mio progetto Angular live che utilizza 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) ) ) ); }; } } 

ed ecco come lo uso nella mia forma retriggers

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