Ceci est mon validateur Async, il n’a pas de temps de réponse, comment puis-je l’ajouter?
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});
})
})
}
}
C'est en fait assez simple pour y parvenir (ce n'est pas pour votre cas mais c'est un exemple général)
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);
});
}
Observable.timer(debounceTime)
:La réponse de @izupet est juste, mais il est intéressant de noter que c'est encore plus simple lorsque vous utilisez Observable:
emailAvailability(control: Control) {
return Observable.timer(500).switchMap(()=>{
return this._service.checkEmail({email: control.value})
.mapTo(null)
.catch(err=>Observable.of({availability: true}));
});
}
Puisque angular 4 a été publié, si une nouvelle valeur est envoyée pour vérification, le précédent Observable
sera désabonné, vous n'avez donc pas besoin de gérer le setTimeout
/clearTimeout
logique par vous-même.
Ce n'est pas possible immédiatement car le validateur est directement déclenché lorsque l'événement input
est utilisé pour déclencher des mises à jour. Voir cette ligne dans le code source:
Si vous souhaitez exploiter un temps d'anti-rebond à ce niveau, vous devez obtenir un observable directement lié à l'événement input
de l'élément DOM correspondant. Ce numéro dans Github pourrait vous donner le contexte:
Dans votre cas, une solution de contournement consisterait à implémenter un accesseur de valeur personnalisé utilisant la méthode fromEvent
d’observable.
Voici un échantillon:
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; }
}
Et utilisez-le de cette façon:
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);
}
}
Voir ce dossier: https://plnkr.co/edit/u23ZgaXjAvzFpeScZbpJ?p=preview .
une solution alternative avec les RxJ peut être la suivante.
/**
* 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);
Keep it simple: pas de délai d'attente, pas de délai, pas d'observable personnalisé
...
// 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
}
exemple 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 }))
)
)
);
}
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')])
});
Voici un service qui retourne une fonction de validation qui utilise debounceTime(...)
et 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
);
Si vous ajoutez les validateurs Validators.required
et Validators.email
_ la demande ne sera faite que si la chaîne de saisie est non vide et une adresse email valide. Cela devrait être fait pour éviter les appels d'API inutiles.