parametros - ¿Cómo agregar tiempo de eliminación de rebote a un validador asíncrono en angular 2?
page title angular 4 (7)
Este es mi Validador de Async, no tiene un tiempo de eliminación de rebotes, ¿cómo puedo agregarlo?
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});
})
})
}
}
Aquí hay un ejemplo de mi proyecto Angular en vivo usando 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)
)
)
);
};
}
}
y así es como lo uso en mi forma reactiva
this.form = this.formBuilder.group({
page_url: this.formBuilder.control('''', [Validators.required], [ValidateAPI.createValidator(this.apiService, ''meta/check/pageurl'', ''pageurl'')])
});
Angular 4+, con Observable.timer(debounceTime)
:
La respuesta de @izupet es correcta, pero vale la pena notar que es aún más simple cuando usas Observable:
emailAvailability(control: Control) {
return Observable.timer(500).switchMap(()=>{
return this._service.checkEmail({email: control.value})
.mapTo(null)
.catch(err=>Observable.of({availability: true}));
});
}
Dado que se ha liberado el angular 4, si se envía un nuevo valor para la verificación, el Observable
anterior se Observable
suscripción, por lo que no es necesario que usted mismo administre la lógica de setTimeout
/ clearTimeout
.
En realidad, es bastante simple de lograr esto (no es para su caso, pero es un ejemplo general)
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);
});
}
No es posible de inmediato, ya que el validador se activa directamente cuando el evento de input
se usa para activar actualizaciones. Vea esta línea en el código fuente:
Si desea aprovechar un tiempo de eliminación de rebote en este nivel, debe obtener un observable directamente vinculado con el evento de input
del elemento DOM correspondiente. Este problema en Github podría darle el contexto:
En su caso, una solución alternativa sería implementar un acceso de valor personalizado o aprovechando el método de fromEvent
observable fromEvent
.
Aquí hay una muestra:
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; }
}
Y úsalo de esta manera:
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);
}
}
Ver este plunkr: https://plnkr.co/edit/u23ZgaXjAvzFpeScZbpJ?p=preview .
Yo tuve el mismo problema. Quería una solución para eliminar el rebote de la entrada y solo solicitar el backend cuando cambiaba la entrada.
Todas las soluciones provisionales con un temporizador en el validador tienen el problema, que solicitan el backend con cada pulsación de tecla. Solo refutan la respuesta de validación. Eso no es lo que se pretende hacer. Desea que la entrada sea eliminada y diferenciada y solo después de eso para solicitar el back-end.
Mi solución para eso es la siguiente (usando formas reactivas y material2):
El componente
@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;
}
}
}
La plantilla
<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>
una solución alternativa con RxJs puede ser la siguiente.
/**
* 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$;
};
}
Uso:
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);
Ejemplo de 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 }))
)
)
);
}