Spis treści
Wprowadzenie
Choć od dawna HTML5 udostępnia nam możliwość walidowania formularzy za pomocą atrybutów na polach takich jak required
, min
, max
, minlength
czy maxlength
, to rozwiązanie rzadko kiedy jest wystarczające. Nie mamy chociażby możliwości stylowania popupu informującego o błędzie walidacji, nie możemy zmienić też tekstu błędu. Dodatkowo, wygląda on różnie w różnych przeglądarkach.
Co prawda, CSS dostarcza pseudoklasy związane z walidacją, opisane np tutaj: Styling Form Inputs in CSS With :required, :optional, :valid and invalid i na ich podstawie, budując skomplikowane selektory możnaby kolorować pola, wyświetlać użytkownikowi błędy itd jednak nadal potrzebujemy walidacji w JS choćby wtedy, gdy chcemy sprawdzić coś niestandardowego jak UUID.
Biblioteka Yup służy do budowania schematów walidacji obiektów oraz parsowania jednych obiektów na drugie. Choć naturalnym miejscem jej użycia są walidacje formularzy, nie jest do tego ograniczona – walidować i parsować można np dane z API backendowego, ponieważ Yup waliduje bezpośrednio obiekt z danymi, nie ma powiązania z np DOMem.
Zastosowanie biblioteki
Prosta walidacja true / false
Jeżeli interesuje nas tylko to, czy wartość przechodzi walidację czy nie, możemy użyć metody isValid
.
Za pomocą tej metody możemy walidować zarówno proste wartości:
const schema = yup.number() await schema.isValid(4) // true await schema.isValid('s') // false
Jak i obiekty:
const schema = yup.object().shape({ name: yup.string().min(3), age: yup.number().min(18) }) await schema.isValid({name: 'R', age: 17}) // false await schema.isValid({name: 'Radek', age: 18}) //true
Funkcja isValid
zwraca Promise
, z wynikiem walidacji, w poście używana jest składnia async/await ponieważ wsparcie przeglądarek jest już wystarczające: https://caniuse.com/#search=await.
Tak wygląda porównanie ze składnią klasyczną:
const valid = await schema.isValid(...) schema.isValid(...).then(valid => {})
Walidacja ze zwrotką zawierającą błędy
Jeżeli chcemy dostać szczegółową informację które pola mają błędy i jakie to są błędy, należy użyć metody validate
. W przypadku gdy nie ma błędów zwróci ona obiekt który został przekazany.
Jeżeli występują błędy oraz przy założeniu że ustawimy opcję abortEarly
na false
aby wykonała się walidacja całego obiektu zamiast wyrzucać błąd przy pierwszym błędzie, zostanie rzucony błąd zawierający tablicę errors
z tekstami błędów oraz tablicę inner
, która zawierać będzie obiekty szczegółowo opisujące błąd:
try { await schema.validate({name: 'R', age: 17}, {abortEarly: false}); } catch (e) { const errors = e.inner.map(el => ({ fieldName: el.path, message: el.message, })) .reduce((acc, current) => ({ ...acc, [current.fieldName]: current.message, }), {}); console.log({errors}); }
Choć kod może wydać się nieco zawiły, robi prostą rzecz:
- Mapowanie wyciąga z rozbudowanej struktury zwróconej przez bibliotekę pola które nas interesują, czyli nazwę pola oraz treść wiadomości
- Reduce tworzy obiekt, którego kluczami są nazwy pól a wartościami treści wiadomości, tak by struktura ta dała się łatwo użyć przy wyświetlaniu wiadomości o błędzie
Rezultat wywołania tego kodu będzie następujący:
{ name:"name must be at least 3 characters", age:"age must be greater than or equal to 18" }
Własne teksty dla błędów walidacji
Biblioteka Yup dostarcza predefiniowane wiadomości dla błędów, można je jednak zmienić:
yup.setLocale({ string: { min: 'Provide at least ${min} characters, please' } });
Nie musimy nadpisywać wszystkich tekstów – jeżeli nie podaliśmy wiadomości dla jakiegoś błędu, zostanie użyta wartość domyślna dostarczona przez bibliotekę. Efekt wywołania powyższego kodu po aktualizacji tekstów będzie następujący:
{ name: "Provide at least 3 characters, please", age: "age must be greater than or equal to 18" }
Zastosowanie Yup w React
Czysty React bez bibliotek
Stwórzmy prosty formularz Reactowy:
<form onSubmit={onSubmit}> <input placeholder="Name" value={name} onChange={e => { setName(e.target.value); setErrors({}); }} /> {errors.name && <div style={{color: 'red'}}>{errors.name}</div>} <br /> <input placeholder="Age" type="number" value={age} onChange={e => { setAge(e.target.value); setErrors({}); }} /> {errors.age && <div style={{color: 'red'}}>{errors.age}</div>} <br /> <button>Submit</button> </form>
Formularz ten zawiera dwa pola, name
oraz age
. Poza ustawianiem wartości gdy użytkownik coś wpisuje, czyszczone są również błędy formularza – jest to dość popularna praktyka.
Weźmy istniejący schemat:
const schema = yup.object().shape({ name: yup.string().min(3), age: yup.number().min(18), });
I zaaplikujmy go do danych z formularza w momencie wysyłania:
const onSubmit = async e => { e.preventDefault(); const data = { name, age }; try { await schema.validate(data, {abortEarly: false}); alert(JSON.stringify(data));<br>}<br>catch (e) { const errors = e.inner.map(el => ({ fieldName: el.path, message: el.message, })).reduce((acc, current) => ({ …acc, [current.fieldName]: current.message, }), {}); setErrors(errors); } }
Jeżeli walidacja nie rzuci żadnym błędem, wtedy dane są wyświetlane w okienku. Jeżeli natomiast poleci błąd, dane są przetwarzane na przyjaźniejszą postać oraz na ich podstawie wyświetlane są wiadomości dla użytkownika:
//... {errors.name && <div style={{color: 'red'}}>{errors.name}</div>} //... {errors.age && <div style={{color: 'red'}}>{errors.age}</div>}
Pełny kod
Poniżej znajduje się pełny kod tego przykładu. Po zainstalowaniu zależności nadaje się on do skopiowania i uruchomienia:
import React, {useState} from 'react'; import * as yup from "yup"; const schema = yup.object().shape({ name: yup.string().min(3), age: yup.number().min(18), }); function App() { const [name, setName] = useState(''); const [age, setAge] = useState(0); const [errors, setErrors] = useState({}); const onSubmit = async e => { e.preventDefault(); const data = { name, age }; try { await schema.validate(data, {abortEarly: false}); alert(JSON.stringify(data)); } catch (e) { const errors = e.inner.map(el => ({ fieldName: el.path, message: el.message, })).reduce((acc, current) => ({ ...acc, [current.fieldName]: current.message, }), {}); setErrors(errors); } } return ( <form onSubmit={onSubmit} style={{padding: '20px'}}> <input placeholder="Name" value={name} onChange={e => { setName(e.target.value); setErrors({}); }} /> {errors.name && <div style={{color: 'red'}}>{errors.name}</div>} <br /> <input placeholder="Age" type="number" value={age} onChange={e => { setAge(e.target.value); setErrors({}); }} /> {errors.age && <div style={{color: 'red'}}>{errors.age}</div>} <br /> <button>Submit</button> </form> ); } export default App;
Kod na github
Możesz również uruchomić kod korzystając z repozytorium na Github: https://github.com/radek-anuszewski/react-yup-example
Wsparcie w bibliotekach do obsługi formularzy w React
Z popularnych bibliotek, biblioteka Formik najprościej integruje się z Yup, wystarczy przekazać schemat jako parametr validiationSchema
: https://frontcave.pl/biblioteki-do-formularzy-w-react-3-formik/#walidacja-yup .
Biblioteka React-hook-form zawiera w swej dokumentacji przykład jak podpiąć Yup za pomocą customowego hooka: https://react-hook-form.com/advanced-usage/#CustomHookwithResolver.
Dla biblioteki React Final Form natomiast dostępny jest Gist z przykładem integracji: https://gist.github.com/manzoorwanijk/5993a520f2ac7890c3b46f70f6818e0a
Podsumowanie
Biblioteka Yup w wydatny sposób pomoże nam z walidacją formularzy. Obsługa zagnieżdżeń uprości walidację skomplikowanych obiektów, przydatna jest też możliwość podmiany tekstów na własne.