Dzisiaj wpis na temat asynchronicznej walidacji formularzy. O ile implementacja asynchronicznej walidacji w Angularze jest bardzo prosta, to już poprawne zakodowanie takiego walidatora wymaga pewnej wiedzy z RxJS.
Zanim przejdziemy do kodowania, zastanówmy się, kiedy może się przydać taki typ walidacji?
Na pewno w każdej sytuacji, kiedy chcemy mieć natychmiastowo feedback z serwera, czy dana wartość wpisana do formularza, istnieje już w bazie danych. Dzięki temu użytkownik może od razu poprawić wartość, zamiast czekać np. na informację po kliknięciu przycisku submit.
Rozważymy przykład, w którym użytkownik tworzy projekt i musi mu nadać unikalny klucz. W sytuacji, kiedy klucz jest już zajęty w bazie, chcemy od razu użytkownikowi wyświetlić informację o błędzie.
Asynchroniczny walidator – nieco teorii
Asynchroniczny walidator jest funkcją, która:
- przyjmuje jako parametr instancję FormControl lub FormGroup
- zwraca obiekt Observable
- może przyjmować dodatkowe parametry (np. usługę), w takiej sytuacji tworzymy funkcję, która zwraca funkcję, parametr z kontrolką umieszczamy w funkcji zagnieżdżonej, a parametr z usługą w funkcji nadrzędnej
- jeśli asynchroniczny walidator zwróci NULL, oznacza, że błąd nie wystąpił i wszystko jest OK, czyli identycznie jak w synchronicznych walidatorach
Oraz trzeba pamiętać, że:
- Angular najpierw wykona synchroniczną walidację
- w przypadku, jeśli synchroniczna walidacja zwróciła nam błędy, to asynchroniczne walidatory się NIE uruchomią
Implementacja walidatora
Poznaliśmy niezbędną teorię, więc czas zabrać się za mięsko 😉 Zbierzmy najpierw wymagania dla naszego walidatora. Czego oczekujemy?
- w przypadku jak użytkownik zacznie wpisywać literki, chcemy opóźniać request do servera o 500ms, aby nie robić requesta co każdy znak (użyjemy timer z RxJS), co mogłoby być niezbyt dobre, zwłaszcza jak użytkownik bardzo szybko wpisuje znaki
- w sytuacji, gdy poprzedni request się jeszcze nie skończył i został już wysłany nowy, to chcemy poprzedni anulować (użyjemy switchMap z RxJS)
- walidator ma przyjmować usługę (serwis) jako parametr
Znamy już wymagania, przygotujmy jeszcze elementy, dzięki którym przetestujemy nasz walidator na StackBlitz.
Tworzę prosty serwis, udający zwrotkę z serwera, czy dany klucz jest już zajęty:
1 2 3 4 5 6 7 |
@Injectable() export class ProjectService { checkKey(key: string) { // tutaj http.get... return of({ data: { exists: key === 'angularlove123' }}).pipe(delay(300)); } } |
Naszym zajętym kluczem w bazie danych jest wartość „angularlove123”. Jeśli użytkownik spróbuje wpisać taki klucz do formularza, ma natychmiastowo otrzymać błąd. Opóźniamy Observable o 300ms, udając, że idzie odpowiedź z serwera. Oczywiście w tym miejscu, powinien iść prawdziwy strzał do API, który np. zwróci nam obiekt z wartością exists: true lub false.
Skoro mamy już serwis, czas zabrać się za walidator:
1 2 3 4 5 6 7 8 |
export const keyExistsAsyncValidator = (projectService: ProjectService) => { return (control: FormControl) => { return timer(500).pipe( switchMap(() => projectService.checkKey(control.value)), map(response => (response.data.exists ? { keyExists: true } : null)) ) } } |
Po kolei:
- używamy timer(500), aby opóźniać kontakt z serwerem i nie strzelać do API co każdą wpisaną literkę
- następnie w switchMap sięgamy po metodę checkKey usługi ProjectService, dzięki switchMap poprzednie requesty są cancelowane, kiedy do strumienia wpadła już nowa wartość
- do checkKey przekazujemy control.value, czyli wartość, którą wpisuje użytkownik do inputa
- mapujemy odpowiedź, zwracamy obiekt z błędem jeśli klucz jest zajęty, a jeśli nie to zwracamy NULL
Podłączenie walidatora do kontrolki
Pozostaje nam teraz dodać walidator do kontrolki:
Templatka:
1 2 |
<input [formControl]="key"> <p class="error" *ngIf="key.hasError('keyExists')">Key is already taken</p> |
Powyżej, mamy dostęp do errora pod kluczem w jakim go dodaliśmy, czyli keyExists. Korzystamy z metody hasError aby sprawdzić, czy wystąpił.
Klasa komponentu:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
@Component({ selector: 'my-app', templateUrl: './app.component.html', styleUrls: [ './app.component.css' ] }) export class AppComponent { key: FormControl; constructor(private projectService: ProjectService) { this.key = new FormControl('', { asyncValidators: [ keyExistsAsyncValidator(this.projectService) ] }); } } |
Przekujemy do kontrolki nasz walidator pod kluczem asyncValidators (tablica asynchronicznych walidatorów). Można również go dodać jako trzeci parametr w new FormControl, od razu w postaci tablicy:
1 2 3 4 |
new FormControl('', [tutaj synchroniczne walidatory], [keyExistsAsyncValidator(this.projectService)] ); |
Jednak zapis przy użyciu tablic jest mniej elegancki, w naszym przypadku wymusza na nas dodanie pustej tablicy synchronicznych walidatorów i jest mniej czytelny. Sugeruję dodawać walidatory pod kluczami validators i asyncValidators.
Link do przykładu na StackBlitz:
Podsumowanie
Jak zauważyłeś, przy użyciu RxJS, możemy w bardzo łatwy sposób implementować wydajne, asynchroniczne walidatory. Framework Angular w przyjazny sposób umożliwia dodawanie ich do kontrolek lub całych grup kontrolek. Dodatkowo dzięki asynchronicznym walidatorom, nasza aplikacja staje się bardziej user friendly.
Dodaj komentarz