87 votes

Comment ajouter un temps de débordement à un validateur asynchrone dans angular 2 ?

Voici mon validateur asynchrone, il n'a pas de temps de déblocage, 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});
          })
      })
    }
}

0 votes

Je pense que ce n'est pas possible... J'ai posé la question dans le passé mais je n'ai pas de réponse : github.com/angular/angular/issues/6895 .

0 votes

@ThierryTemplier alors avez-vous un moyen de contourner ce problème ?

118voto

n00dl3 Points 12707

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

La réponse de @izupet est correcte 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}));
    });
}

Depuis la sortie d'Angular 4, si une nouvelle valeur est envoyée pour être vérifiée, Angular se désinscrit de la liste de contrôle. Observable alors qu'il est toujours en pause dans le minuteur, de sorte que vous n'avez pas besoin de gérer la fonction setTimeout / clearTimeout logique par vous-même.

Utilisation de timer et le comportement du validateur asynchrone d'Angular, nous avons recréé RxJS debounceTime .

10 votes

A mon avis, c'est de loin la solution la plus élégante pour le problème du "debounce". Note : il n'y a pas de subscribe() parce que lorsqu'on retourne un Observable au lieu d'une Promise, l'Observable doit être froid .

0 votes

Ne fonctionne pas pour moi, observable sans subscribe ne fonctionne pas du tout.

1 votes

Problème résolu, j'envoyais un validateur asynchrone à côté d'autres validateurs.

33voto

izupet Points 797

Il est en fait assez simple de réaliser ceci (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);
    });
}

3 votes

Je pense que c'est la meilleure solution. Parce que la solution de @Thierry Templier va retarder toutes les règles de validation, pas seulement celle qui est asynchrone.

1 votes

La solution de @n00dl3 est plus élégante et puisque rxjs est déjà disponible, pourquoi ne pas l'utiliser pour simplifier davantage les choses.

0 votes

@BobanStojanovski cette question fait référence à angular 2. Ma solution ne fonctionne qu'avec angular 4+.

11voto

thierry templier Points 998

Ce n'est pas possible d'emblée, car le validateur est directement déclenché lorsque l'option input est utilisé pour déclencher les mises à jour. Voir cette ligne dans le code source :

Si vous voulez tirer parti d'un temps de déblocage à ce niveau, vous devez obtenir une observable directement liée à la fonction input de l'élément DOM correspondant. Ce problème dans Github pourrait vous donner le contexte :

Dans votre cas, une solution de contournement consisterait à mettre en œuvre un accesseur de valeur personnalisé en tirant parti de l'outil de gestion de l'accès à l'information. fromEvent méthode de l'observable.

En voici un exemple :

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 plunkr : https://plnkr.co/edit/u23ZgaXjAvzFpeScZbpJ?p=preview .

1 votes

Le validateur asynchrone fonctionne très bien mais mes autres validateurs ne semblent pas fonctionner, par exemple *ngIf="(email.touched && email.errors) ne se déclenche pas

5voto

Une solution alternative avec RxJs 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$;
  };
}

Utilisation :

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

-2voto

rkd Points 21

J'avais le même problème. Je voulais une solution pour débouncer l'entrée et ne demander au backend que lorsque l'entrée changeait.

Toutes les solutions de contournement avec une minuterie dans le validateur ont le problème qu'elles demandent le backend à chaque frappe. Ils n'interrompent que la réponse de validation. Ce n'est pas ce que l'on veut faire. Vous voulez que l'entrée soit débitée et distinguée et seulement après cela demander le backend.

Ma solution pour cela est la suivante (en utilisant les formes réactives et material2) :

Le composant

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

Le modèle

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

0 votes

C'est exactement ce que je cherche, mais où faites-vous exactement les appels http ici ? mon principal problème est que chaque pression sur une touche déclenche un appel backend-api.

0 votes

this.service.isUsernameReserved(username).subscribe(reserved => this.validated$.next(reserved)); l'appel http est dans le service.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X