Valerie Hosler
Valerie Hosler

Reputation: 85

How can I validate an Autocomplete multiple TextField using React Hook Form and Yup?

I am using Material UI's Autocomplete multiple TextField, React Hook Form, and Yup to validate the form inputs.

When I use Yup.string() for the daysOfWeek, even if I have values selected, it shows the error message. However, if I change it to Yup.array(), the following error is displayed...

daysOfWeek must be a array type, but the final value was: null (cast from the value ""). If "null" is intended as an empty value be sure to mark the schema as .nullable()

Is there a way I can use Yup to validate Material UI's Autocomplete multiple TextField? Thank you in advance!

This is my relevant code...

    const [selected, setSelected] = useState([]);

    const validationSchema = Yup.object().shape({
        daysOfWeek: Yup.string()
            .required("Days of the week are required")
    });

    const {
        formState: {errors},
        handleSubmit,
        register
    } = useForm({
        resolver: yupResolver(validationSchema)
    });

   <Autocomplete
       disableClearable
       disablePortal
       filterSelectedOptions
       multiple
       getOptionDisabled={(option) => option.disabled ? true : false}
       getOptionLabel={(option) => option.label}
       id="days-autocomplete"
       onChange={(event, value) => onOptionsChange(event, value)}
       options={daysOfWeekSuggestions}
       renderInput={(params) => <TextField
           required
           error={errors.daysOfWeek ? true : false}
           id="daysOfWeek"
           label="Days of the week"
           name="daysOfWeek"
           type="search"
           {...params}
           {...register("daysOfWeek")}
       />}
       value={selected}
   />

   <Typography color="error" sx={errorSx} variant="inherit">{errors.daysOfWeek?.message}</Typography>

Upvotes: 3

Views: 11837

Answers (2)

knoefel
knoefel

Reputation: 6949

I made a small Sandbox and changed a few things to make it work:

  • you should use <Controller /> instead of register for external controlled components like MUI's <Autocomplete />
  • as you have to pass a defaultValue for <Controller/> (in your case an empty array) - you can then use the Yup.min method for arrays to test if the user has at least selected one day of the week and get rid of the Yup.required test as they both do more or less the same in your situation
  • you can use the helperText prop of <Autocomplete /> to show the error message
  • you can pass the ref from <Controller /> to the inputRef prop of <TextField /> so that when you submit without selecting a day of the week it will automatically focus the field for the user
const validationSchema = Yup.object().shape({
  daysOfWeek: Yup.array()
    .of(
      Yup.object().shape({
        value: Yup.string(),
        label: Yup.string()
      })
    )
    .min(1, "Days of the week are required")
});
<Controller
  name="daysOfWeek"
  control={control}
  defaultValue={[]}
  render={({ field: { ref, ...field }, fieldState: { error } }) => (
    <Autocomplete
      {...field}
      disableClearable
      disablePortal
      filterSelectedOptions
      multiple
      getOptionDisabled={(option) => option.disabled}
      getOptionLabel={(option) => option.label}
      id="days-autocomplete"
      onChange={(event, value) => field.onChange(value)}
      options={daysOfWeekSuggestions}
      renderInput={(params) => (
        <TextField
          required
          error={!!error}
          helperText={error?.message}
          id="daysOfWeek"
          label="Days of the week"
          name="daysOfWeek"
          type="search"
          inputRef={ref}
          {...params}
        />
      )}
    />
  )}
/>

Edit ComboBox Material Demo (forked)

Upvotes: 6

Samira
Samira

Reputation: 2733

I made an async ,dynamicly and formik autocomplete.

this is my form file:

        const FormSimple = (props) => {
    
    
    
        const validationSchema = Yup.object().shape({
            name: Yup.string()
                .min(2, 'Too Short!')
                .max(50, 'Too Long!')
                .required('Required'),
            nationalId: Yup.number("Invalid Number")
                .min(2, 'Too Short!')
                .max(9999999999, 'Too Long!')
                .required('Required'),
            correspEmail: Yup.string().email('Invalid email'),
            financeNationalNo: Yup.number(),
        });
        const formik = useFormik({
            initialValues: details,
            validationSchema: validationSchema,
            onSubmit: (values) => {
                if (id !== "new")
                    editData(values)
                else
                    sendData(values)
            },
        });
    
    return (<>
     <form onSubmit={formik.handleSubmit}>
                <div className="row">
                    
                    <InputSize>
                        <Asynchronous getListData={getListCountry} onChange={(value)=> {
                            formik.setFieldValue("registrationCountryName",value.nameEn)
                            formik.setFieldValue("registrationCountryId",value.id)
                        }} name="nameEn"
                                      defaultValue={formik.values?.registrationCountryName ? {
                                          nameEn: formik.values?.registrationCountryName,
                                          id: formik.values?.registrationCountryId
                                      } : null}
                                      value={formik.values?.registrationCountryName ? {
                                          nameEn: formik.values?.registrationCountryName,
                                          id: formik.values?.registrationCountryId
                                      } : null}
            
                                      required={true}
                                      showName="Country Of Registration" label="nameEn" id="id"/>
                    </InputSize>

                    <div className="col-12 mt-4">
                        
                            <Button variant="contained" color="primary" type="submit"
                                    disabled={loading}>Submit</Button>
                       
                    </div>
                </div>
            </form>
</>)
    
    }

and this is my autocomplete file :

    import * as React from 'react';
import TextField from '@mui/material/TextField';
import Autocomplete from '@mui/material/Autocomplete';
import CircularProgress from '@mui/material/CircularProgress';
import {useRef} from "react";

export default function Asynchronous(props) {
    const {onChange, name, getListData, label = "name", id = "id", showName,defaultValue,disabled,required,value,noOption="No Option"} = props;
    const [open, setOpen] = React.useState(false);
    const [options, setOptions] = React.useState([]);
    const [filters, setFilters] = React.useState(null);
    const [loadingApi, setLoadingApi] = React.useState(false)
    const loading = open && options.length === 0;
    let timeOut = useRef(null);

    const getData = async (search = "") => {
        setLoadingApi(true)
        const data = await getListData(search); // For demo purposes.
        // console.log(data)
        setLoadingApi(false);
        // console.log(data)
        if(data)
        setOptions([...data]);
    }

    React.useEffect(() => {

        if (!loading) {
            return undefined;
        }
        getData();

    }, [loading]);

    React.useEffect(() => {
        if (filters !== null) {
            if (timeOut.current !== null)
                clearTimeout(timeOut.current);

            timeOut.current = setTimeout(() => getData(filters), 500);
        }
    }, [filters]);

    React.useEffect(() => {
        if (!open) {
            setOptions([]);
        }
    }, [open]);

    return (
        <Autocomplete
            disabled={disabled}
            id={name}
            name={name}
            sx={{width: "100%"}}
            open={open}
            onOpen={() => {
                setOpen(true);
            }}
            onClose={() => {
                setOpen(false);
            }}
            defaultValue={defaultValue}
            value={value}
            isOptionEqualToValue={(option, value) => option?.[id] === value?.[id]}
            getOptionLabel={(option) => option?.[label]}
            options={options}
            onChange={(e, value) => onChange(value)}
            loading={loadingApi}
            noOptionsText={noOption}
            renderInput={(params) => (
                <TextField
                    name={name}
                    required={required}
                    variant="standard"
                    {...params}
                    label={showName}
                    onChange={(e) => setFilters(e.target.value)}
                    InputProps={{
                        ...params.InputProps,
                        onblur:() => {},
                        endAdornment: (
                            <React.Fragment>
                                {loadingApi ? <CircularProgress color="inherit" size={20}/> : null}
                                {params.InputProps.endAdornment}
                            </React.Fragment>
                        ),
                    }}
                />
            )}
        />
    );
}

Upvotes: 2

Related Questions