4

I am building a form using Material UI's Autocomplete feature and using recoil for state management. I am also using react-hook-form. I am needing the following criteria met:

  1. Need typeahead that allows first letter typed to return a list of options from an API to show in Autocomplete. Each letter will return a different list of options to select from.
  2. Need to also allow freeSolo so that user can enter a value manually
  3. Needs to be required and follow a pattern to validate the form.

I am using react-hook-form's <Controller> to control the input and allow for features like validation, displaying error messages in the helper text, etc.

The Problems: I am having issues with the typeahead filtering out options based on what I type, along with allowing freeSolo. As I type a new value, the list of options do not filter. The popup just stays open. I also need to validate on change of input for the pattern validation. I have tried with the following example with onInputChange to make use of react-hook-form's useForm and setValue to manually set the value of the field and validate the form. ({shouldValidate: true}). The below example is a custom, reusable component I created for Autocomplete, as well as using that custom component in other parent components. I hope I included as much details as possilbe, but if not, please let me know if you need anything more. Any assistance would be very appreciative!

Parent Component:

  const setTrainType = useSetRecoilState(TrainType)
  // Trains would return a list of trains based on letter of train type that was passed from input
  const trainsList = useRecoilValue(Trains)
  const trainOptions = useMemo(() => trainsList.map(trainIDFormatted), [
    trainsList,
  ])

const handleInputChange = useCallback(
    (_e: unknown, option: string, reason: string) => {
      const capitalized =
        option === capitalize(option) ? option : capitalize(option)
      setValue('trainID', capitalized, {shouldValidate: true})
      if (['input', 'reset'].includes(reason) && capitalized !== '') {
        setTrainType(capitalized.charAt(0))
      } else {
        setTrainType(undefined)
      }
    },
    [setTrainType, setValue],
  )

<Autocomplete
                autoSelect
                freeSolo
                disabled={disabled}
                helperText=" "
                label="Select a train"
                name="trainID"
                options={trainOptions}
                rules={{
                  pattern: {
                    message: 'Must match train ID pattern',
                    value: /^(?:[A-Z]-?[A-Z ]{6}-?[0-9 ]-?[0-9 ]{2}[A-Z ])?$/,
                  },
                  required: 'Train is required',
                }}
                onInputChange={handleInputChange}
              />

Custom autocomplete component:

import {
  AutocompleteProps,
  Autocomplete as MuiAutocomplete,
} from '@material-ui/lab'
import {get} from 'lodash'
import React, {ReactNode, useCallback} from 'react'
import {
  Controller,
  ControllerProps,
  FieldError,
  useFormContext,
} from 'react-hook-form'
import {useRenderInput} from './hooks'

interface Props
  extends Pick<ControllerProps<'select'>, 'rules'>,
    Omit<
      AutocompleteProps<string, false, false, true>,
      'error' | 'onChange' | 'required' | 'renderInput'
    > {
  helperText?: ReactNode
  label?: string
  name: string
}

/**
 * Render controlled autocomplete. Use react-form-hook's FormProvider.
 * @param props Component properties
 * @param props.helperText Default helper text for error
 * @param props.label Input label
 * @param props.name Name identifier for react-hook-form
 * @param props.required If true then item is required
 * @param props.rules Select rules
 * @return React component
 */
export const Autocomplete = ({
  helperText,
  label,
  name,
  rules,
  ...props
}: Props) => {
  // eslint-disable-next-line @typescript-eslint/unbound-method
  const {control, errors, watch} = useFormContext()
  const error: FieldError | undefined = get(errors, name)
  const required = get(rules, 'required') !== undefined
  const value = watch(name)
  const renderAutocompleteInput = useRenderInput({
    error: error !== undefined,
    helperText: get(error, 'message', helperText),
    label,
    required,
  })
  const handleOnChange = useCallback(
    (_e: unknown, option: string | null) => option,
    [],
  )
  const renderAutocomplete = useCallback(
    params => (
      <MuiAutocomplete
        {...props}
        {...params}
        renderInput={renderAutocompleteInput}
        onChange={handleOnChange}
      />
    ),
    [handleOnChange, props, renderAutocompleteInput],
  )

  return (
    <Controller
      control={control}
      defaultValue={value ?? ''}
      name={name}
      render={renderAutocomplete}
      rules={rules}
    />
  )
}

What it looks like:

enter image description here

npabs18
  • 167
  • 2
  • 10

0 Answers0