Ten post jest trzecią częścią serii o obsłudze formularzy w React:
Spis Treści
Wprowadzenie
Formik to jedna z najbardziej uznanych bibliotek do zarządzania formularzami w React. Jej autor, Jared Palmer, jest autorem wielu projektów których my jako programiści używamy na codzień.
Formik upraszcza codzienną pracę z formularzami, ułatwiając zarządzanie wartościami w formularzu, walidacją i jego stanem wewnętrznym. Dzięki bibliotece możesz w prosty sposób:
- podpiąć customowe walidacje które podepniemy tak prosto jak walidacje natywne,
- podpiąć walidacje asynchroniczne
- sygnalizować użytkownikowi, które pola zostały już przez niego wypełnione
- obsługiwać tablice i obiekty jako pola formularza
Oraz wiele innych, jednocześnie używając prostego i czytelnego API.
Formik wspiera integrację z TypeScript. Obsługuje również React Native, dzięki czemu raz zdobyta wiedza może być użyta w aplikacji internetowej i aplikacji natywnej.
Wiedza jaką zdobędziesz czytając ten wpis pozwoli Ci używać biblioteki Formik w codziennej pracy, oszczędzając Ci wielu nieprzyjemności ręcznej obsługi formularzy.
Zahaczymy również o CSS GRID – jeśli chcesz dowiedzieć się o nim czegoś więcej, możesz obejrzeć prezentację jaką miałem podczas webinaru dla lokalnej grupy Tarnów Devs:
W opisie filmu znajdziesz link do slajdów na Google Drive oraz link do repo na Githubie do playgroundu którego w prezentacji używałem.
Prosty przykład
Formika używać można klasycznie jak i za pomocą hooków.
Zainstalujmy bibliotekę poleceniem
npm install formik
Baza do dalszych prac
Wersja klasyczna
Stwórzmy bazę do dalszych prac zawierającą jedno pole, najpierw w wersji klasycznej:
import React from 'react';
import { Formik } from 'formik';
function App() {
return (
<Formik
initialValues={{userName: ''}}
onSubmit={values => alert(JSON.stringify(values))}
>
{
({
values,
handleChange,
handleSubmit,
}) => (
<form onSubmit={handleSubmit}>
<input
onChange={handleChange}
name="userName"
value={values.userName}
/>
<button type="submit">
Submit
</button>
</form>
)
}
</Formik>
);
}
export default App;
Po zaimportowaniu głównego komponentu używamy go, przekazując parametr initialValues
jako wartości startowe dla pól oraz funkcję onSubmit
która jako parametr dostanie dane wysłane w formularzu.
Do wyświetlenia formularza używany jest wzorzec FACC – Function As Child Components, który umożliwia przekazywanie danych od komponentu biblioteki do naszego kodu w prosty sposób, jako parametry funkcji.
W kolejnej wersji formularza użyjemy większej ilości komponentów Formik więc kodu będzie mniej – natomiast na ten moment, aby dane mogły być zbierane, pole musi posiadać nazwę w atrybucie name
oraz podpiętą w parametrze onChange
funkcję handleChange
, którą dostajemy od biblioteki jako parametr.
Dzięki komponentowi Field
kod wygląda prościej:
<Field name="userName" /> //Powyższy kod jest równoważny do: <input onChange={handleChange} name="userName" value={values.userName} />
Niestety komponent Field
nie działa z prezentowanym poniżej Hookiem useFormik
.
Wersja z hookami
Użyjmy hooka useFormik
:
import React from 'react'; import {useFormik} from "formik"; function App() { // dzięki destrykturyzacji API jest czytelniejsze const { values, handleChange, handleSubmit, } = useFormik({ initialValues: { userName: '', }, onSubmit: values => alert(JSON.stringify(values)), }) return ( <form onSubmit={handleSubmit}> <input onChange={handleChange} name="userName" value={values.userName} /> <button type="submit"> Submit </button> </form> ); } export default App;
W tej wersji wszystkie potrzebne nam elementy / funkcje zwracane nam są z hooka useFormik
.
Kod wygląda nieco czytelniej i jest zdecydowanie mniej zagnieżdżony – choć musimy pamiętać, że sami autorzy biblioteki przestrzegają przed nadużywaniem tego hooka. Nie wszystkie komponenty będą z nim działać.
Walidacja
Prosta walidacja
Oznaczenie pola jako wymagane możemy zaimplementować przekazując do komponentu Formik
bądź hooka useFormik
funkcję validate
:
validate={values => { const errors = {}; if (!values.userName) { errors.userName = true; } return errors; }}
Następnie obiekt zwracany przez tą funkcję dostępny będzie jako parametr errors
, używamy również parametru touched
by nie wyświetlać błędów dla pól, których użytkownik jeszcze nie zmodyfikował:
{touched.userName && errors.userName && ( <div>Provide user name</div> )}
Biblioteka Formik dostarcza komponent ErrorMessage
który upraszcza wyświetlanie błędów. By kod pozostał prosty, musimy nieco zmienić funkcję walidującą, tak by ustawiać nie boolean
a string
z wiadomością walidacji:
validate={values => { const errors = {}; if (!values.userName) { errors.userName = 'Provide user name'; } return errors; }}
Teraz używamy komponentu ErrorMessage
, który automatycznie sprawdzi czy pole o danym name
zostało zmodyfikowane i czy jest błąd z nim zwiazany:
<ErrorMessage name="userName" component="div" /> //Równoważne do: {touched.userName && errors.userName && ( <div>{errors.userName}</div> )}
Asynchroniczna walidacja
Walidacja może być asynchroniczna. Dodajmy, jak w poprzednim wpisie, pole captcha:
<Field name="captcha" validate={validateCaptcha} /> <ErrorMessage name="captcha" component="div" />
Skorzystaliśmy tutaj z faktu, ze walidację podpinać możemy nie tylko pod cały formularz, ale również pod konkretne pole. Funkcja validateCaptcha może wyglądać w następujący sposób:
// Symulacja zapytania const sendRequest = value => new Promise((resolve, reject) => { setTimeout(() => { const valid = value === 'test'; resolve(valid) }, 5000) }) const validateCaptcha = async value => { const valid = await sendRequest(value); return valid? true : 'Please provide valid captcha'; }
Walidacja asynchroniczna pola opóźni również walidację formularza – jeżeli nie podasz ani name
ani captcha
, wykona się zapytanie i dopiero wtedy zobaczysz błędy dotyczące obu pól.
Walidacja z pomocą biblioteki Yup i tworzenie schematów
Biblioteka Yup pozwala w czytelny sposób walidować nawet zaawansowane obiekty. Zainstalujmy ją
npm install yup
Następnie naszą funkcję walidującą:
validate={values => {
const errors = {};
if (!values.userName) {
errors.userName = 'Provide user name';
}
return errors;
}}
Zastąpmy validationSchema
używającym biblioteki Yup:
validationSchema={yup.object().shape({
userName: yup.string().required('Provide user name')
})}
Co ważne, również przy użyciu validationSchema
walidacja poczeka na walidację asynchroniczną, która może odbywać się np na którymś z pól.
Pełny kod walidacji
Zobacz pełny kod
import React from 'react';
import {ErrorMessage, Field, Formik} from 'formik';
const sendRequest = value => new Promise((resolve, reject) => {
setTimeout(() => {
const valid = value === 'test';
resolve(valid)
}, 5000)
})
const validateCaptcha = async value => {
const valid = await sendRequest(value);
return valid? true : 'Please provide valid captcha';
}
function App() {
return (
<Formik
initialValues={{userName: '', captcha: ''}}
validate={values => {
const errors = {};
if (!values.userName) {
errors.userName = 'Provide user name';
}
return errors;
}}
onSubmit={values => alert(JSON.stringify(values))}
>
{
({
handleSubmit,
}) => (
<form onSubmit={handleSubmit}>
<Field
name="userName"
/>
<ErrorMessage name="userName" component="div" />
<br />
<Field
name="captcha"
validate={validateCaptcha}
/>
<ErrorMessage name="captcha" component="div" />
<br />
<button type="submit">
Submit
</button>
</form>
)
}
</Formik>
);
}
export default App;
Customowe komponenty z hookiem useField
Hook useField udostępnia API pozwalające podpięcie customowego pola pod Formik:
const [field, meta, helpers] = useField(props);
Stwórzmy więc komponent, który umieścimy w formularzu:
<CustomCheckbox name="checkbox" />
Zaimplementujmy ten komponent jako dwa przyciski gdzie jeden ustawia wartość na true
a drugi na false
. Dodatkowo, jeżeli użytkownik nie wykonał jeszcze żadnej akcji wyświetla się podpowiedź by kliknął jeden z przycisków, a jeżeli akcję już wykonał – możliwość wyczyszczenia obecnego wyboru.
Element zostanie automatycznie oznaczony jako touched
w momencie wysłania formularza, my jednak chcemy dokonywać tego po każdym wyborze.
Komponent może wyglądać następująco:
const CustomCheckbox = props => { const [field, meta, helpers] = useField(props); return ( <> {!meta.touched && <div>Hint: click one of buttons:</div>} {meta.touched && ( <div> <button type="button" onClick={() => { helpers.setTouched(false); helpers.setValue(false) }}> Clear field status </button> </div> )} <button type="button" onClick={() => { helpers.setValue(true); helpers.setTouched(true); }}> Set to true {field.value && '(current)'} </button> <button type="button" onClick={() => { helpers.setValue(false); helpers.setTouched(true); }}> Set to false {!field.value && '(current)'} </button> </> ); };
Przykład zaawansowany
Zaawansowany przykład zawierał będzie, jak w poprzednim poście, galerię zdjęć – dajmy jednak więcej możliwości konfiguracji sposobu wyświetlania i za pomocą CSS GRID poukładamy te zdjęcia nieco ładniej.
Co ważne, Formik na ten moment nie wspiera obsługi plików, stąd nie możemy użyć np <Field type="file" />
, musimy napisać nasz własny komponent.
Brak obsługi plików w Formik
Posłużymy się wiedzą z poprzedniego rozdziału artykułu:
const FileUpload = (props) => {
const [field, meta, helpers] = useField(props);
return (
<input
name={props.name}
type="file"
onChange={e => helpers.setValue(URL.createObjectURL(e.target.files[0]))}
/>
);
}
Dzięki hookowi useField
możemy ustawiać wartość na polu formularza.
Komponent FieldsArray
Komponent FieldsArray pomoże nam w obsłudze tablicy jako pola formularza:
<FieldArray
name="images"
render={helpers => (
<div>
{
values.images.map((image, index) => (
<div>
<Field
name={`images[${index}].name`}
placeholder={`Image ${index + 1} name`}
/>
<br />
<FileUpload
name={`images[${index}].file`}
/>
</div>
))
}
<button
type="button"
onClick={() => helpers.push({name: ''})}
>
Add image
</button>
</div>
)}
/>
Komponent ten udostępnia metody pomocne w obsłudze pól, np metodą push
dodasz kolejny obiekt do tablicy pól. Zauważ, że pola nazwane są w konwencji images[index].field
, ponieważ Formik obsługuje zarówno tablice jak i obiekty zagnieżdżone mamy możliwość zapisywania danych od razu do tablicy obiektów, bez konieczności żadnych przekształceń.
Wyświetlanie za pomocą CSS GRID
Dane zapisujemy do lokalnej zmiennej stanu:
const [images, setImages] = useState([]) // W reakcji na wysłanie formularza onSubmit={(values, {resetForm}) => { setImages([ ...images, ...values.images, ]); resetForm(); }}
Same obrazki wyświetlimy na siatce CSS GRID:
Images: <div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 20%) 50px 3em 1fr', }} > {images.map(image => ( <div> <strong> {image.name} </strong> <br /> <img style={{maxWidth: '100%'}} alt="" src={image.file} /> </div> ))} </div>
Stworzyliśmy siatkę 5 wierszy na 5 kolumn, gdzie mamy 2 kolumny zajmujące 20% przestrzeni, jedną zajmującą 50px, jedną zajmującą 3em i ostatnią zajmującą pozostałą przestrzeń.
Jeśli chcesz dowiedzieć się więcej o CSS GRID, polecam obejrzeć moją prezentację z webinaru Tarnów Devs:
W opisie filmu znajdziesz link do prezentacji na Google Drive oraz adres repo Githuba dla playgroundu którego użyłem w prezentacji.
Pełny kod
Pokaż pełny kod
import React, {useState} from 'react';
import {Field, FieldArray, Formik, useField} from 'formik';
const FileUpload = (props) => {
const [field, meta, helpers] = useField(props);
return (
<input
name={props.name}
type="file"
onChange={e => helpers.setValue(URL.createObjectURL(e.target.files[0]))}
/>
);
}
function App() {
const [images, setImages] = useState([])
return (
<>
<Formik
initialValues={{images: []}}
onSubmit={(values, {resetForm}) => {
setImages([
...images,
...values.images,
]);
resetForm();
}}
>
{
({
handleSubmit,
values,
}) => (
<form onSubmit={handleSubmit}>
<FieldArray
name="images"
render={helpers => (
<div>
{
values.images.map((image, index) => (
<div>
<Field
name={`images[${index}].name`}
placeholder={`Image ${index + 1} name`}
/>
<br />
<FileUpload
name={`images[${index}].file`}
/>
</div>
))
}
<button
type="button"
onClick={() => helpers.push({name: ''})}
>
Add image
</button>
</div>
)}
/>
<button type="submit">
Submit
</button>
</form>
)
}
</Formik>
<p>
Images:
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(2, 20%) 50px 3em 1fr',
gridTemplateRows: 'repeat(5, 20%)',
}}
>
{images.map(image => (
<div>
<strong>
{image.name}
</strong>
<br />
<img style={{maxWidth: '100%'}} alt="" src={image.file} />
</div>
))}
</div>
</p>
</>
);
}
export default App;
Podsumowanie
Choć zaskoczył mnie brak obsługi plików przez Formik, brak ten da się dość łatwo załatać – jednocześnie sama biblioteka jest przyjemna w użyciu. Prosta rejestracja customowych komponentów czy helpery do pracy z tablicami i możliwość łatwej pracy z zagnieżdżonymi obiektami czynią ją dobrym wyborem – dzięki przeczytaniu tego posta masz silne podstawy, by użyć biblioteki Formik w kolejnym projekcie.