Home > Software engineering >  Validating array of strings in React
Validating array of strings in React

Time:11-02

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;

CodePudding user response:

I recommend using a library similar to yup. With yup you can define a schema for a form, something like this:

const RegisterUserValidationSchema = yup.object().shape({
    name: yup.string()
        .min(2, "No way,too short!")
        .max(200, "So long,are you serious?")
        .required("Required"),
    email: yup.string().email("Please enter an email").required("Required"),
    password: yup.string().required("Password is required"),
    confirmPassword: yup.string()
        .oneOf([yup.ref('password'), null], "Passwords must match"),
    phoneNumber: yup.number("Please enter a valid phone number"),
})

Then , you can compare your values with your schema

CodePudding user response:

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.

  • Related