Skip to content

Comment créer une fonction personnalisée de validation asynchrone ?

Voici comment vous pourriez créer une fonction de validation asynchrone qui vérifie si une adresse email existe déjà dans une base de données, sur Angular :

  1. Créez une fonction dans votre composant Angular qui prendra en paramètre une adresse email et retournera un Observable. Cette fonction enverra une requête HTTP à votre backend pour vérifier si l'adresse email existe déjà dans la base de données.

  2. Utilisez la méthode asyncValidator de AbstractControl pour créer un validateur asynchrone personnalisé. Cette méthode prend en paramètre une fonction qui retourne un Observable et retourne également un Observable.

  3. Dans la fonction passée à asyncValidator, appelez la fonction que vous avez créée dans l'étape 1 en lui passant l'adresse email à vérifier. Transformez le résultat de cette fonction en un Observable qui émet une erreur si l'adresse email existe déjà, ou qui ne produit aucune valeur si l'adresse email n'existe pas.

  4. Ajoutez le validateur asynchrone à votre formulaire en utilisant la méthode setAsyncValidators de FormControl ou FormGroup.

Voici un exemple de code qui montre comment mettre en œuvre cette approche :

ts
import { Observable, timer, map, switchMap } from 'rxjs';
import { AbstractControl, AsyncValidatorFn } from '@angular/forms'
import { HttpClient } from '@angular/common/http';

@Injectable({
  providedIn: 'root'
})
export class EmailValidatorService {
  constructor(private http: HttpClient) {}

  checkEmailExists(email: string): Observable<boolean> {
    return this.http.get<boolean>(`/api/email-exists?email=${email}`);
  }

  checkEmailExistsAsync(): Observable<{ emailExists: boolean | null }> {
    return timer(500).pipe(
        switchMap(() => this.checkEmailExists(control.value)),
        map(exists => (exists ? { emailExists: true } : null))
    );
  }
}
import { Observable, timer, map, switchMap } from 'rxjs';
import { AbstractControl, AsyncValidatorFn } from '@angular/forms'
import { HttpClient } from '@angular/common/http';

@Injectable({
  providedIn: 'root'
})
export class EmailValidatorService {
  constructor(private http: HttpClient) {}

  checkEmailExists(email: string): Observable<boolean> {
    return this.http.get<boolean>(`/api/email-exists?email=${email}`);
  }

  checkEmailExistsAsync(): Observable<{ emailExists: boolean | null }> {
    return timer(500).pipe(
        switchMap(() => this.checkEmailExists(control.value)),
        map(exists => (exists ? { emailExists: true } : null))
    );
  }
}
ts
import { Component, OnInit } from '@angular/core';
import { FormGroup, Validators } from '@angular/forms';
import { EmailValidatorService } from 'email-validator.service'

@Component({
  // ...
})
export class MyComponent  {
  form: FormGroup = this.fb.group({
      email: ['', [Validators.required], this.emailValidatorService.checkEmailExistsAsync.bind(this.emailValidatorService)]
  });

  constructor(private emailValidatorService: EmailValidatorService) {}
}
import { Component, OnInit } from '@angular/core';
import { FormGroup, Validators } from '@angular/forms';
import { EmailValidatorService } from 'email-validator.service'

@Component({
  // ...
})
export class MyComponent  {
  form: FormGroup = this.fb.group({
      email: ['', [Validators.required], this.emailValidatorService.checkEmailExistsAsync.bind(this.emailValidatorService)]
  });

  constructor(private emailValidatorService: EmailValidatorService) {}
}

Dans cet exemple, nous avons une classe de composant Angular qui contient une fonction checkEmailExists qui envoie une requête HTTP à l'aide de l'objet HttpClient pour vérifier si une adresse email donnée existe déjà dans une base de données.

Nous avons également une fonction emailExistsValidator qui utilise la méthode asyncValidator de AbstractControl pour créer un validateur asynchrone personnalisé. Cette fonction prend en paramètre un contrôle de formulaire et retourne un Observable qui émet une erreur si l'adresse email existe déjà, ou qui ne produit aucune valeur si l'adresse email n'existe pas.

La fonction emailExistsValidator utilise la méthode timer de RxJS pour ajouter un délai de 500 millisecondes avant de vérifier si l'adresse email existe déjà en utilisant la fonction checkEmailExists. Cela permet d'éviter d'envoyer trop de requêtes HTTP si l'utilisateur tape rapidement dans le champ de formulaire.

Attention avec this

En JavaScript, la valeur de this dans une fonction dépend du contexte d'exécution, et non de la manière dont ou de l'endroit où la fonction est définie. Ainsi, lorsqu'une méthode est extraite d'un objet et passée ailleurs, elle peut perdre son contexte d'origine.

La méthode .bind() permet de créer une nouvelle fonction où this est fixé à la valeur que vous fournissez. Cela vous permet de "verrouiller" la valeur de this pour une fonction, quel que soit le contexte d'exécution.

Donc, lorsque vous faites:

ts
this.emailValidatorService.checkEmailExistsAsync.bind(this.emailValidatorService)
this.emailValidatorService.checkEmailExistsAsync.bind(this.emailValidatorService)

Vous créez essentiellement une nouvelle version de checkEmailExistsAsyncthis est toujours this.emailValidatorService, garantissant ainsi que la méthode a accès à toutes les propriétés et méthodes de EmailExistsValidatorService.

Références

En utilisant une promesse

Voici comment vous pourriez réécrire l'exemple précédent pour utiliser une promesse au lieu d'un Observable :

ts
import { Observable, timer, map, switchMap, lastValueFrom } from 'rxjs';
import { AbstractControl, AsyncValidatorFn } from '@angular/forms'
import { HttpClient } from '@angular/common/http';

@Injectable({
  providedIn: 'root'
})
export class EmailValidatorService {
  constructor(private http: HttpClient) {}

  checkEmailExists(email: string): Promise<boolean> {
    return lastValueFrom(this.http.get<boolean>(`/api/email-exists?email=${email}`))
  }

  async emailExistsValidatorAsync():  Promise<{ emailExists: boolean | null }> {
      await lastValueFrom(timer(500))
      const exists = await this.checkEmailExists(control.value);
      return exists ? { emailExists: true } : null;
  }
}
import { Observable, timer, map, switchMap, lastValueFrom } from 'rxjs';
import { AbstractControl, AsyncValidatorFn } from '@angular/forms'
import { HttpClient } from '@angular/common/http';

@Injectable({
  providedIn: 'root'
})
export class EmailValidatorService {
  constructor(private http: HttpClient) {}

  checkEmailExists(email: string): Promise<boolean> {
    return lastValueFrom(this.http.get<boolean>(`/api/email-exists?email=${email}`))
  }

  async emailExistsValidatorAsync():  Promise<{ emailExists: boolean | null }> {
      await lastValueFrom(timer(500))
      const exists = await this.checkEmailExists(control.value);
      return exists ? { emailExists: true } : null;
  }
}

Le lastValueFrom est une fonction de la bibliothèque rxjs qui est utilisée pour convertir un Observable en une Promesse. Il attend que l'Observable soit terminé (c'est-à-dire qu'il ait émis toutes ses valeurs et complété) puis renvoie la dernière valeur émise par cet Observable comme le résultat de la Promesse.

Cela permet d'utiliser des Observables dans un contexte qui attend des Promesses, comme les fonctions asynchrones avec await.

Comprendre facilement lastValueFrom

Imaginez que vous avez une radio qui joue plusieurs chansons l'une après l'autre, et que vous voulez connaître la dernière chanson jouée. L'Observable serait comme cette radio, et lastValueFrom serait comme un ami qui écoute toute la radio pour vous, puis vous dit quelle était la dernière chanson jouée une fois que la radio s'arrête.

En d'autres termes, lastValueFrom vous permet d'attendre qu'un Observable ait terminé et de récupérer sa dernière valeur, le tout dans un format de Promesse qui est souvent plus facile et plus familier pour les développeurs travaillant avec des opérations asynchrones en JavaScript et TypeScript.