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:

  1. 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'].
  2. .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 akumulator acc).
    • Dla każdego klucza (np. 'name’, ’email’, 'message’) robimy coś w ciele funkcji reduce.
  3. …acc, [key]: validateField(key, formValues[key]):
    • ...acc to sposób na zachowanie dotychczas zebranych danych w akumulatorze acc. 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.
  4. Wynik:
    • Po przejściu przez wszystkie klucze, reduce zwróci obiekt newErrors, 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 '@’, to newErrors będzie wyglądało tak: { name: 'Name is required', email: 'Email is invalid' }. Ale jak pole name będzie poprawne zwróci: { name: '', email: 'Email is invalid' }

Następny fragment kodu:

const formIsValid = !Object.values(newErrors).some(error => error);

Jak to działa krok po kroku:

  1. 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śli newErrors to { name: 'Name is required', email: '' }, to Object.values(newErrors) zwróci ['Name is required', ''].
  2. .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 zwraca true 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óci true.
  3. ! (operator negacji):
    • Operator ! neguje wynik. Czyli, jeśli .some() zwróci true (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óci false, ponieważ żaden błąd nie spełnia warunku bycia niepustym), całe wyrażenie staje się true.

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>
  );
}

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *