Search code examples
arraysreactjsvalidationantd

Validating array of strings in React


I have a form that contains an array of emails. The array has no size limits. I need to check every element in the array to be a valid email. The user should be able to add new elements to the array and there should be at least one email and each new element should have a valid email and each email should be unique. I want the validations to be worked only after the user submits the form for the first time. What should be the proper way to validate list of emails?

I'm using Ant Design components and I keep list of index of invalid emails as invalidArrayIndexes so that I can show error on each invalid line. When I add a new element, I cannot get the required message ("Please enter your email!") and the list of validated indexes are getting mixed when I add or delete new elements. I'm not sure whether this is the correct way to validate list of strings in react. Here is what I have done so far:

import { Button, Form, Input } from "antd";
import { useState } from "react";

const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

const isValidEmail = (str) => {
  return emailRegex.test(str);
};

const MyForm = () => {
  const [emails, setEmails] = useState([""]);
  const [invalidArrayIndexes, setInvalidArrayIndexes] = useState([]);
  const [firstSubmit, setFirstSubmit] = useState(false);

  const addEmail = () => {
    const updatedEmails = [...emails];
    updatedEmails.push("");
    setEmails(updatedEmails);
  };

  const removeEmail = (index) => {
    const updatedEmails = [...emails];
    updatedEmails.splice(index, 1);
    setEmails(updatedEmails);
  };

  const formSubmitted = () => {
    if (!firstSubmit) {
      setFirstSubmit(true);
    }
    const notValidEmails = emails.filter((email) => {
      return !isValidEmail(email);
    });
    const invalidEmailExist = notValidEmails.length > 0;
    if (!invalidEmailExist) {
      console.log("now submitting");
      console.log(emails);
    }
  };

  const valChanged = (e, index) => {
    const updatedEmails = [...emails];
    updatedEmails[index] = e.target.value;
    if (firstSubmit) {
      const isValid = isValidEmail(e.target.value);
      if (isValid) {
        if (invalidArrayIndexes.indexOf(index) > -1) {
          const updatedInvalidArrayIndexes = [...invalidArrayIndexes];
          updatedInvalidArrayIndexes.splice(
            updatedInvalidArrayIndexes.indexOf(index),
            1
          );
          setInvalidArrayIndexes(updatedInvalidArrayIndexes);
        }
      } else {
        if (invalidArrayIndexes.indexOf(index) < 0) {
          const updatedInvalidArrayIndexes = [...invalidArrayIndexes];
          updatedInvalidArrayIndexes.push(index);
          setInvalidArrayIndexes(updatedInvalidArrayIndexes);
        }
      }
    }
    setEmails(updatedEmails);
  };

  const emailList = emails.map((email, index) => {
    return (
      <Form.Item
        key={index}
        name="email"
        label="email"
        rules={[{ required: true, message: "Please enter your email!" }]}
        validateStatus={invalidArrayIndexes.includes(index) && "error"}
        help={invalidArrayIndexes.includes(index) ? "not a valid email" : " "}
      >
        <Input
          style={{ width: 300 }}
          placeholder="enter email"
          value={email}
          onChange={(e) => valChanged(e, index)}
        />
        <Button type="label" onClick={() => removeEmail(index)}>
          remove email
        </Button>
      </Form.Item>
    );
  });

  return (
    <div>
      {emailList}
      <Button type="label" onClick={addEmail}>
        add new email
      </Button>
      <div style={{ marginTop: 20 }}>
        <Button type="primary" onClick={formSubmitted}>
          send emails
        </Button>
      </div>
    </div>
  );
};

export default MyForm;

Solution

  • The reason that you cannot see the required error message is that validation rules of ant design work when your element is wrapped with a Form component and after the form is submitted. However, that will not solve your problem because this usage only supports single form items each with a unique name. I would suggest you to validate your list of strings by using a React form validation library react-validatable-form, because it makes good abstraction of the validation workflow and validation results from the bindings of the DOM elements. First you should wrap your App with ReactValidatableFormProvider like:

    import { ReactValidatableFormProvider } from "react-validatable-form";
    import "antd/dist/antd.css";
    import MyForm from "./MyForm";
    
    export default function App() {
      return (
        <ReactValidatableFormProvider>
          <MyForm />
        </ReactValidatableFormProvider>
      );
    }
    

    And then you can use useValidatableForm hook with the set of rules with a listPath like:

    import { Button, Form, Input } from "antd";
    import { useValidatableForm } from "react-validatable-form";
    import get from "lodash.get";
    
    const initialFormData = {
      emails: [""]
    };
    const rules = [
      { listPath: "emails", ruleSet: [{ rule: "required" }, { rule: "email" }] }
    ];
    
    const MyForm = () => {
      const {
        isValid,
        validationError,
        formData,
        setPathValue,
        setFormIsSubmitted
      } = useValidatableForm({
        rules,
        initialFormData,
        hideBeforeSubmit: true
      });
    
      const addEmail = () => {
        const updatedEmails = get(formData, "emails");
        updatedEmails.push("");
        setPathValue("emails", updatedEmails);
      };
    
      const removeEmail = (index) => {
        const updatedEmails = get(formData, "emails");
        updatedEmails.splice(index, 1);
        setPathValue("emails", updatedEmails);
      };
    
      const formSubmitted = () => {
        setFormIsSubmitted();
        if (isValid) {
          console.log("now submitting");
          console.log(get(formData, "emails"));
        }
      };
    
      const emailList = get(formData, "emails").map((email, index) => {
        return (
          <Form.Item
            key={index}
            validateStatus={get(validationError, `emails{${index}}`) && "error"}
            help={get(validationError, `emails{${index}}`) || " "}
          >
            <Input
              style={{ width: 300 }}
              placeholder="enter email"
              value={get(formData, `emails[${index}]`)}
              onChange={(e) => setPathValue(`emails[${index}]`, e.target.value)}
            />
            <Button type="label" onClick={() => removeEmail(index)}>
              remove email
            </Button>
          </Form.Item>
        );
      });
    
      return (
        <div>
          {emailList}
          <Button type="label" onClick={addEmail}>
            add new email
          </Button>
          <div style={{ marginTop: 20 }}>
            <Button type="primary" onClick={formSubmitted}>
              send emails
            </Button>
          </div>
        </div>
      );
    };
    
    export default MyForm;
    

    You can take a look at this sandbox for a live working example.