问题
This is my Async Validator it doesn\'t have a debounce time, how can I add it?
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});
})
})
}
}
回答1:
It is actually pretty simple to achieve this (it is not for your case but it is general example)
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:
Angular 4+, Using Observable.timer(debounceTime)
:
@izupet 's answer is right but it is worth noticing that it is even simpler when you use Observable:
emailAvailability(control: Control) {
return Observable.timer(500).switchMap(()=>{
return this._service.checkEmail({email: control.value})
.mapTo(null)
.catch(err=>Observable.of({availability: true}));
});
}
Since angular 4 has been released, if a new value is sent for checking, the previous Observable
will get unsubscribed, so you don't actually need to manage the setTimeout
/clearTimeout
logic by yourself.
回答3:
It's not possible out of the box since the validator is directly triggered when the input
event is used to trigger updates. See this line in the source code:
- https://github.com/angular/angular/blob/master/modules/angular2/src/common/forms/directives/default_value_accessor.ts#L23
If you want to leverage a debounce time at this level, you need to get an observable directly linked with the input
event of the corresponding DOM element. This issue in Github could give you the context:
- https://github.com/angular/angular/issues/4062
In your case, a workaround would be to implement a custom value accessor leveraging the fromEvent
method of observable.
Here is a sample:
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; }
}
And use it this way:
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);
}
}
See this plunkr: https://plnkr.co/edit/u23ZgaXjAvzFpeScZbpJ?p=preview.
回答4:
Keep it simple: no timeout, no delay, no custom Observable
...
// 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
}
回答5:
an alternative solution with RxJs can be the following.
/**
* 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$;
};
}
Usage:
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);
回答6:
RxJS 6 example:
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 }))
)
)
);
}
回答7:
Here is an example from my live Angular project using 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)
)
)
);
};
}
}
and here is how I use it in my reactive form
this.form = this.formBuilder.group({
page_url: this.formBuilder.control('', [Validators.required], [ValidateAPI.createValidator(this.apiService, 'meta/check/pageurl', 'pageurl')])
});
回答8:
Here a service that returns a validator function that uses debounceTime(...)
and 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
};
}
}
Usage:
emailAddress = new FormControl('',
[Validators.required, Validators.email],
this.validator.createValidator() // async
);
If you add the validators Validators.required
and Validators.email
the request will only be made if the input string is non-empty and a valid email address. This should be done to avoid unnecessary API calls.
回答9:
I had the same problem. I wanted a solution for debouncing the input and only request the backend when the input changed.
All workarounds with a timer in the validator have the problem, that they request the backend with every keystroke. They only debounce the validation response. That's not what's intended to do. You want the input to be debounced and distincted and only after that to request the backend.
My solution for that is the following (using reactive forms and material2):
The component
@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;
}
}
}
The template
<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>
回答10:
To anyone still interested in this subject, it's important to notice this in angular 6 document:
- They must return a Promise or an Observable,
- The observable returned must be finite, meaning it must complete at some point. To convert an infinite observable into a finite one, pipe the observable through a filtering operator such as first, last, take, or takeUntil.
Be careful with the 2nd requirement above.
Here's a AsyncValidatorFn
implementation:
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 };
})
);
};
来源:https://stackoverflow.com/questions/36919011/how-to-add-debounce-time-to-an-async-validator-in-angular-2