W każdej aplikacji www obsługa formularzy jest podstawową opcją.
Weźmy na przykład zwykły formularz kontaktowy(korzystam z React w wersji 18, oraz Mui):
Inicjalizacja stanu formularza
Kiedy tworzymy formularz, musimy zarządzać stanem jego pól. Używamy hooka useState
do inicjalizacji stanu formValues
przechowującego wartości pól, oraz errors
przechowującego komunikaty o błędach walidacji.
const [formValues, setFormValues] = useState({ name: '', email: '', message: '' });
const [errors, setErrors] = useState({});
- formValues przechowuje aktualne dane wprowadzane przez użytkownika.
- errors służy do wyświetlania informacji o błędach walidacji dla poszczególnych pól.
Obsługa zmian w formularzu
const handleChange = (event) => {
const { name, value } = event.target;
setFormValues(prevValues => ({ ...prevValues, [name]: value }));
setErrors(prevErrors => ({
...prevErrors,
[name]: validateField(name, value)
}));
};
Po każdej zmianie aktualizowany jest odpowiedni klucz w obiekcie formValues
.
Funkcja setErrors
aktualizuje stan błędów, wykorzystując wyniki funkcji validateField
, która zwraca odpowiedni komunikat błędu lub pusty string, jeśli pole jest prawidłowe.
Walidacja pól
Funkcja validateField
przyjmuje nazwę i wartość pola, a następnie zwraca komunikat o błędzie lub pusty string na podstawie sprawdzenia zasad walidacyjnych.
const validateField = (name, value) => {
switch (name) {
case 'name': return value ? '' : 'Name is required';
case 'email': return value.includes('@') ? '' : 'Email is invalid';
case 'message': return value ? '' : 'Message is required';
default: return '';
}
};
Przesyłanie formularza
Funkcja handleSubmit
jest wywoływana, gdy formularz jest wysyłany. Przed przesłaniem danych sprawdzane są ponownie wszystkie pola, czy nie zawierają błędów.
const handleSubmit = (event) => {
event.preventDefault();
const newErrors = Object.keys(formValues).reduce((acc, key) => ({
...acc, [key]: validateField(key, formValues[key])
}), {});
setErrors(newErrors);
const formIsValid = !Object.values(newErrors).some(error => error);
if (formIsValid) {
console.log('Form is valid:', formValues);
// Logika wysyłania formularza, np. API call
} else {
console.log('Form errors:', newErrors);
}
};
Ten fragment jest ciekawy:
const newErrors = Object.keys(formValues).reduce((acc, key) => ({
...acc, [key]: validateField(key, formValues[key])
}), {});
Jak to działa, krok po kroku:
- Object.keys(formValues): Ta funkcja bierze nasz obiekt
formValues
, który zawiera dane z formularza, np.{ name: '', email: '', message: '' }
, i zamienia go w listę kluczy. Dla tego przykładu zwróci to tablicę:['name', 'email', 'message']
. - .reduce((acc, key) => ({…}), {}):
- Metoda
reduce
przechodzi przez każdy element tej tablicy (czyli przez każde pole formularza) i wykonuje określoną operację. - Zaczynamy od pustego obiektu
{}
(to jest nasz akumulatoracc
). - Dla każdego klucza (np. 'name’, ’email’, 'message’) robimy coś w ciele funkcji reduce.
- Metoda
- …acc, [key]: validateField(key, formValues[key]):
...acc
to sposób na zachowanie dotychczas zebranych danych w akumulatorzeacc
. Na początku jest pusty, ale z każdym krokiem dodajemy do niego nowe informacje.[key]
to aktualnie przetwarzany klucz z tablicy kluczy.validateField(key, formValues[key])
to funkcja, która sprawdza, czy wartość dla danego klucza (np. wartość w polu 'name’ lub ’email’) jest poprawna. Jeśli coś jest nie tak, zwraca komunikat o błędzie, a jeśli wszystko jest w porządku, zwraca pusty ciąg.
- Wynik:
- Po przejściu przez wszystkie klucze,
reduce
zwróci obiektnewErrors
, który zawiera wszystkie błędy znalezione podczas walidacji. Na przykład, jeśli pole 'name’ było puste, a pole ’email’ nie miało znaku '@’, tonewErrors
będzie wyglądało tak:{ name: 'Name is required', email: 'Email is invalid' }
. Ale jak polename
będzie poprawne zwróci:{ name: '', email: 'Email is invalid' }
- Po przejściu przez wszystkie klucze,
Następny fragment kodu:
const formIsValid = !Object.values(newErrors).some(error => error);
Jak to działa krok po kroku:
- Object.values(newErrors):
- Ta funkcja bierze obiekt
newErrors
(gdzie klucze to nazwy pól formularza, a wartości to komunikaty o błędach) i zamienia go w tablicę zawierającą tylko wartości tych błędów. Na przykład, jeślinewErrors
to{ name: 'Name is required', email: '' }
, toObject.values(newErrors)
zwróci['Name is required', '']
.
- Ta funkcja bierze obiekt
- .some(error => error):
- Metoda
.some()
jest funkcją używaną na tablicach, która sprawdza, czy przynajmniej jeden element tablicy spełnia podany warunek (w tym przypadku czy element jest niepusty, co oznacza błąd). - Funkcja callback
error => error
zwracatrue
dla każdego elementu tablicy (błędu), który jest niepusty (czyli istnieje jakiś błąd walidacji). - Jeśli chociaż jeden błąd walidacji istnieje,
.some()
zwrócitrue
.
- Metoda
!
(operator negacji):- Operator
!
neguje wynik. Czyli, jeśli.some()
zwrócitrue
(czyli znajdzie jakiś błąd), całe wyrażenie!Object.values(newErrors).some(error => error)
stanie sięfalse
. - Jeśli jednak wszystkie błędy są puste (czyli
.some()
zwrócifalse
, ponieważ żaden błąd nie spełnia warunku bycia niepustym), całe wyrażenie staje siętrue
.
- Operator
Renderowanie komponentów
Komponenty TextField
z Material-UI są renderowane dynamicznie dla każdego klucza w formValues
. To pozwala na łatwe rozszerzenie formularza bez modyfikacji logiki renderowania.
<Box component="form" onSubmit={handleSubmit} noValidate sx={{ mt: 1 }}>
{Object.keys(formValues).map((key) => (
<TextField
key={key}
...
value={formValues[key]}
onChange={handleChange}
error={!!errors[key]}
helperText={errors[key]}
/>
))}
<Button type="submit" ...>Send</Button>
</Box>
Cały komponent
"use client";
import { Box, TextField } from "@mui/material";
import { useState } from "react";
import Button from "@mui/material/Button";
export default function ContactPage() {
const [formValues, setFormValues] = useState({ name: "", email: "", message: "" });
const [errors, setErrors] = useState({});
const handleChange = (event) => {
const { name, value } = event.target;
setFormValues((prevValues) => ({ ...prevValues, [name]: value }));
setErrors((prevErrors) => ({
...prevErrors,
[name]: validateField(name, value),
}));
};
const validateField = (name, value) => {
switch (name) {
case "name":
return value ? "" : "Name is required";
case "email":
return value.includes("@") ? "" : "Email is invalid";
case "message":
return value ? "" : "Message is required";
default:
return "";
}
};
const handleSubmit = (event) => {
event.preventDefault();
const newErrors = Object.keys(formValues).reduce(
(acc, key) => ({
...acc,
[key]: validateField(key, formValues[key]),
}),
{}
);
setErrors(newErrors);
const formIsValid = !Object.values(newErrors).some((error) => error);
if (formIsValid) {
console.log("Form is valid:", formValues);
// Logika wysyłania formularza, np. API call
} else {
console.log("Form errors:", newErrors);
}
};
return (
<Box component="form" onSubmit={handleSubmit} noValidate sx={{ mt: 1 }}>
{Object.keys(formValues).map((key) => (
<TextField
key={key}
variant="outlined"
margin="normal"
required
fullWidth
label={key[0].toUpperCase() + key.slice(1)}
name={key}
type={key === "email" ? "email" : "text"}
autoComplete={key}
multiline={key === "message"}
rows={key === "message" ? 4 : 1}
value={formValues[key]}
onChange={handleChange}
error={!!errors[key]}
helperText={errors[key]}
/>
))}
<Button type="submit" fullWidth variant="contained" sx={{ mt: 3, mb: 2 }}>
Send
</Button>
</Box>
);
}