Ten post jest ostatnim artykułem w serii o obsłudze formularzy w React:
Spis Treści
Wprowadzenie
Biblioteka Final Form jest biblioteką bazującą na modelu subskrypcyjnym, pozwalającą subskrybować zmiany wartości, walidacji czy aktywności danego pola. Biblioteka nie jest związana z żadnych konkretnym frameworkiem, natomiast posiada wrapper dla Reacta – React Final Form i to on omówiony zostanie w tym artykule.
Dlaczego powinno zależeć Ci na poznaniu biblioteki Final Form? Ponieważ znacząco ułatwia takie rzeczy jak:
- przekazywanie danych z i do formularza
- skomplikowane, customowe walidacje
- walidacja asynchroniczna
- zarządzanie stanem formularza (by np pokazywać użytkownikowi których pól jeszcze nie “dotknął”)
- podpinanie customowych komponentów do zautomatyzowanej logiki biblioteki
Przykład prosty
Zainstalujmy bibliotekę poleceniem:
npm install --save final-form react-final-form
Za pomocą komponentów Form
i Field
zbudujmy pierwszy przykład użycia biblioteki:
import React from 'react'; import { Form, Field } from 'react-final-form' function App() { return ( <Form onSubmit={data => alert(JSON.stringify(data))} > { ({ handleSubmit }) => ( <form onSubmit={handleSubmit}> <Field name="userName" component="input" /> <button type="submit"> Submit </button> </form> ) } </Form> ); } export default App;
Zarówno do komponentu Form
jak i Field
musimy przekazać to, co ma zostać wyrenderowane, mamy trzy możliwości w jaki sposób to zrobić:
- Za pomocą wspomnianego również w poprzednich postach wzorca FACC (Function As Child Component), gdzie jako props
children
przekazujemy funkcję, która dostanie od biblioteki parametry takie jakonSubmit
. - Przekazując taką samą funkcję do komponentu
Form
za pomocą propsarender
. - Za pomocą propsa
component
.
Walidacja
Walidacja prosta
Walidować możemy na dwóch poziomach, całego formularza bądź konkretnego pola. Jeżeli walidujemy na poziomie formularza, wtedy otrzymujemy obiekt zawierający wszystkie pola, i zwracamy również obiekt gdzie wiadomość błędu musi być pod takim samym kluczem jak nazwa pola:
<Form onSubmit={data => alert(JSON.stringify(data))} validate={data => { const errors = {} if (!data.userName) { errors.userName = 'Please provide user name' } return errors }} > //formularz </Form>
Jeżeli walidujemy na poziomie pola, dostajemy wartość i jeżeli chcemy zaznaczyć błąd, zwracamy tekst który może zostać wyświetlony użytkownikowi:
<Field name="lastName" validate={value => !value && 'Please provide last name'} > //input </Field>
Błąd wyświetlić możemy w następujący sposób:
<Field name="lastName" validate={value => !value && 'Please provide last name'} > { ({input, meta}) => ( <> <input {...input} type="text" /> {meta.error && meta.touched && <span>{meta.error}</span>} </> ) } </Field>
Od biblioteki dostajemy treść błędu poprzez props meta.error
, dodatkowo przez meta.touched
sprawdzamy czy formularz był już wysłany – by uniknąć pokazywania błędów zanim użytkownik wysłał formularz.
Walidacja asynchroniczna
Dodajmy znaną z poprzednich postów funkcję walidacji asynchronicznej wraz z symulacją zapytania na serwer:
// 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? undefined : 'Please provide valid captcha'; }
Podepnijmy walidację na pole captcha
:
<Field name="captcha" validate={validateCaptcha}> //input </Field>
Widzimy że z funkcji walidującej musimy zwrócić albo wartość undefined
dla braku błędu lub treść błędu, albo funkcję które resolvuje się do undefined
bądź treści błędu. Co istotne, wysyłanie formularza poczeka na zakończenie asynchronicznej walidacji i dopiero wtedy wykona akcję onSubmit
.
Pełny kod przykładu prostego
Poniższy kod nadaje się do skopiowania i uruchomienia, oczywiście po zainstalowaniu zależności
Kliknij tutaj, by zobaczyć pełny kod
import React from 'react';
import { Form, Field } from 'react-final-form'
// 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? undefined : 'Please provide valid captcha';
}
function App() {
return (
<Form
onSubmit={data => alert(JSON.stringify(data))}
validate={data => {
const errors = {}
if (!data.userName) {
errors.userName = 'Please provide user name'
}
return errors
}}
>
{
({
handleSubmit
}) => (
<form onSubmit={handleSubmit}>
<Field name="userName">
{
({input, meta}) => (
<>
<input {...input} type="text" />
{meta.error && meta.touched && <span>{meta.error}</span>}
</>
)
}
</Field>
<br />
<Field name="lastName" validate={value => !value && 'Please provide last name'}>
{
({input, meta}) => (
<>
<input {...input} type="text" />
{meta.error && meta.touched && <span>{meta.error}</span>}
</>
)
}
</Field>
<br />
<Field name="captcha" validate={validateCaptcha}>
{
({input, meta}) => (
<>
<input {...input} type="text" />
{meta.error && meta.touched && <span>{meta.error}</span>}
</>
)
}
</Field>
<br />
<button type="submit">
Submit
</button>
</form>
)
}
</Form>
);
}
export default App;
Repozytorium przykładu prostego na Github
Kod możesz również uruchomić korzystając z repozytorium Github:
https://github.com/radek-anuszewski/react-final-form-simple-example
Przykład zaawansowany
W przykładzie zaawansowanym, jak w poprzednich postach, napiszemy prostą galerię zdjęć. Ponieważ, podobnie jak Formik, React Final Form nie wspiera plików wprost, napiszemy naszą własną obsługę.
Problemy wersji 6.4.0 z walidacją asynchroniczną
Ponieważ React Final Form w wersji 6.4.0 ma problemy z walidacją asynchroniczną: https://github.com/final-form/react-final-form/issues/780 użyłem wersji 6.3.5:
"react-final-form": "6.3.5",
Taka wersja ustawiona będzie w repozytorium przykładu na Github.
CSS GRID
Z poprzedniego postu wykorzystamy sposób wyświetlania obrazków w galerii:
<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>
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.
Dodawanie zdjęć do galerii za pomocą customowego komponentu
Dużym ułatwieniem jest to, że do zarejestrowania komponentu wystarczy tylko podać name
i referencję do naszego komponentu w propsie component
. Biblioteka wspiera tablice – jeżeli chcemy by pole było elementem tablicy jego nazwę podajemy wraz z indeksem, przykładowo images[0]
:
{ids.map((id, i) => (
<Field
key={id}
name={`images[${i}]`}
component={FormElement}
validate={validateImage}
/>
))}
Walidacja rozmiarów obrazka
Dzięki temu, że React Final Form wspiera walidację asynchroniczną, możemy użyć adresu pliku, poczekać aż się załaduje i wtedy zresolvować odpowiednią wartość:
const validateImageSize = async file => new Promise(resolve => {
const image = new Image()
image.onload = () => {
const valid = image.width <= 1000 && image.height <= 1000;
resolve(valid? undefined : 'Image size is at most 1000 x 1000')
}
image.src = file;
})
const validateImage = async image => {
if (!image) {
return;
}
if (!image.name || !image.file) {
return 'Specify image and file';
}
return validateImageSize(image.file);
}
Pełny kod przykładu zaawansowanego
Poniższy kod nadaje się do skopiowania i uruchomienia, oczywiście po zainstalowaniu zależności
Kliknij tutaj, by zobaczyć pełny kod
import React, {useEffect, useState} from 'react';
import { Form, Field } from 'react-final-form'
const FormElement = (props) => {
const [name, setName] = useState(null)
const [file, setFile] = useState(null)
useEffect(() => {
props.input.onChange({
name,
file,
});
//did mount, should run only once
// eslint-disable-next-line
}, [])
return (
<>
<input onChange={e => {
const name = e.target.value;
props.input.onChange({
name,
file,
});
setName(name);
}} />
<input type="file" onChange={e => {
const file = URL.createObjectURL(e.target.files[0]);
props.input.onChange({
name,
file,
});
setFile(file);
}} />
{props.meta.touched && props.meta.error && (
<div>
{props.meta.error}
</div>
)}
<br />
</>
)
}
const validateImageSize = async file => new Promise(resolve => {
const image = new Image()
image.onload = () => {
const valid = image.width <= 1000 && image.height <= 1000;
resolve(valid? undefined : 'Image size is at most 1000 x 1000')
}
image.src = file;
})
const validateImage = async image => {
if (!image) {
return;
}
if (!image.name || !image.file) {
return 'Specify image and file';
}
return validateImageSize(image.file);
}
function App() {
const [images, setImages] = useState([]);
const [ids, setIds] = useState([]);
return (
<>
<Form
onSubmit={data => {
setImages([...images, ...data.images]);
setIds([]);
}}
>
{
({
handleSubmit
}) => (
<form onSubmit={handleSubmit}>
{ids.map((id, i) => (
<Field
key={id}
name={`images[${i}]`}
component={FormElement}
validate={validateImage}
/>
))}
<button
type="button"
onClick={() => setIds([...ids, new Date().getTime()])}
>
Add image
</button>
<br />
<button type="submit">
Submit
</button>
</form>
)
}
</Form>
<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;
Repozytorium przykładu zaawansowanego na Github
Kod możesz również uruchomić korzystając z repozytorium Github:
https://github.com/radek-anuszewski/react-final-form-advanced-example
Podsumowanie
React Final Form jest bardzo popularnym, szeroko przetestowanym rozwiązaniem. Zaskoczeniem było dla mnie to, że występują problemy z asynchroniczną walidacją, jednak jak wynika z opisu zgłoszenia wiadomo co je powoduje. Stąd, gdy wszystko już będzie działać, biblioteka ta będzie godna polecenia dzięki łatwemu podpięciu customowych komponentów, uproszczeniu skomplikowanej walidacji czy obsłudze tablic.