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:
@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:
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:
<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:
@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:
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.