Как добавить время отладки к асинхронному валидатору в angular 2?
Это мой Async Validator, у него нет времени отладки, как я могу его добавить?
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
На самом деле довольно просто добиться этого (это не для вашего случая, но это общий пример)
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+, используя 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}));
});
}
Поскольку angular 4 был выпущен, если новое значение отправлено для проверки, предыдущий Observable
будет отменен, поэтому вам фактически не нужно управлять логикой setTimeout
/clearTimeout
самостоятельно.
Ответ 3
Это невозможно из коробки, поскольку валидатор запускается напрямую, когда событие input
используется для запуска обновлений. См. Эту строку в исходном коде:
Если вы хотите использовать время отладки на этом уровне, вам нужно получить наблюдаемое, напрямую связанное с событием input
соответствующего элемента DOM. Эта проблема в 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.
Ответ 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
Пример 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
Вот пример из моего живого проекта 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
Вот сервис, который возвращает функцию валидатора, которая использует 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.required
и Validators.email
запрос будет выполнен только в том случае, если входная строка не пуста и правильный адрес электронной почты. Это должно быть сделано, чтобы избежать ненужных вызовов API.
Ответ 8
У меня такая же проблема. Я хотел найти решение для отмены ввода и запрашивать бэкэнд только при изменении ввода.
Все обходные пути с таймером в валидаторе имеют проблему, заключающуюся в том, что они запрашивают бэкэнд при каждом нажатии клавиши. Они только отклоняют ответ проверки. Это не то, что намеревалось сделать. Вы хотите, чтобы входные данные были разоблачены и различались, и только после этого запрашивать сервер
Мое решение для этого заключается в следующем (используя реактивные формы и материал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>
Ответ 9
Для тех, кто все еще интересуется этой темой, важно отметить это в угловом документе 6:
- Они должны вернуть Обещание или Наблюдаемое,
- Возвращаемая наблюдаемая должна быть конечной, что означает, что она должна завершиться в какой-то момент. Чтобы преобразовать бесконечное наблюдаемое в конечное, направьте наблюдаемое через оператор фильтрации, такой как first, last, take или takeUntil.
Будьте осторожны со вторым требованием выше.
Вот реализация 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 };
})
);
};
Ответ 10
Сохраняйте это простым: нет тайм-аута, нет задержки, нет пользовательских 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
}