Search code examples
material-uiformikyupformik-material-ui

Duplicate Errors on Formik/Yup Validation


I'm using the following validation schema in Formik:


    validationSchema = {
      Yup.object({
        emails: Yup.array()
          .of(Yup.string().email('Please enter valid email addresses only.'))
          .min(1, 'At least one email address is required.')
      })
    }

It works very well, except that, since I'm using a Material UI AutoComplete component, when the user enters multiple invalid email addresses, they get to see the error message once per invalid email address.

Any way around this?

Here's a sandbox link: https://codesandbox.io/s/wild-sea-h2i0m?file=/src/App.tsx


Solution

  • What you want to do then is to make sure you filter out duplicated errors. You should make sure that each error message is unique. I wrote a function that help you do that.

    I updated your EmailsField component:

    import React from "react";
    import Autocomplete from "@material-ui/lab/Autocomplete";
    import Chip from "@material-ui/core/Chip";
    import CloseIcon from "@material-ui/icons/Close";
    import TextField from "@material-ui/core/TextField";
    import { FieldProps } from "formik";
    
    const isEmailValid = (email: string) =>
      /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
    
    const EmailsField = ({
      field,
      form: { errors, touched, setTouched, setFieldValue },
      ...props
    }: FieldProps) => {
      console.log('errors', errors)
      const name = field.name;
      const [value, setValue] = React.useState<string[]>([]);
      const [inputValue, setInputValue] = React.useState("");
    
      const handleChange = (event: React.ChangeEvent<{}>, emails: string[]) => {
        setTouched({ ...touched, [name]: true });
        setValue(emails);
        event.persist();
        setFieldValue(name, emails);
      };
    
      const handleInputChange = (
        event: React.ChangeEvent<{}>,
        newInputValue: string
      ) => {
        const options = newInputValue.split(/[ ,]+/);
        const fieldValue = value
          .concat(options)
          .map(x => x.trim())
          .filter(x => x);
    
        if (options.length > 1) {
          handleChange(event, fieldValue);
        } else {
          setInputValue(newInputValue);
        }
      };
    
      // 1. This function will help remove duplicated errors
      const getEmailErrors = (errors: any) => {
        return Array.isArray(errors)
          ?  errors.filter((email: string, i: number, arr: any) => arr.indexOf(email) === i)
          : errors;
      }
    
      return (
        <Autocomplete<string>
          multiple
          disableClearable={true}
          options={[]}
          freeSolo
          renderTags={(emails, getTagProps) =>
            emails.map((email, index) => (
              <Chip
                deleteIcon={<CloseIcon />}
                variant="default"
                label={email}
                color={isEmailValid(email) ? "primary" : "secondary"}
                {...getTagProps({ index })}
              />
            ))
          }
          value={value}
          inputValue={inputValue}
          onChange={handleChange}
          onInputChange={handleInputChange}
          renderInput={params => (
            <TextField
              {...params}
              name={name}
              error={touched[name] && Boolean(errors.emails)}
              //---------------------------------------->>>> Call it here
              helperText={touched[name] && errors.emails && getEmailErrors(errors.emails as any)}
              variant="outlined"
              InputProps={{ ...params.InputProps }}
              {...props}
            />
          )}
        />
      );
    };
    
    export default EmailsField;