MrVoland
MrVoland

Reputation: 283

Custom autocomplete validation in React

I created a custom autocomplete dropdown list, but I need to force the user to select something from the list, keeping at the same time the possibility of typing something in the input field.

How can I create such a validation? Autocomplete component is a list with the possibility to select a thing from the API, but the user still can type anything in the input field and send it as well. I want the user to be able submit the form ONLY with the suggested item. Could you please help me with that one?

Basically the question is: how can I check if the input value is the same as the item taken from the autocomplete component?

export type AutocompletePropsType = {
  inputValue: string;
  currentIndex: number;
  onSelect: (selectedItem: string, slug?: string | null) => void;
  hasError: boolean;
  list: AutocompleteResponse['data'];
};

type PropsType = {
  onChange: (value: string, type: string, isValid?: boolean, slug?: string) => void;
  inputName: string;
  type: InputType;
  label: string;
  autocompleteComponent: React.ComponentType<AutocompletePropsType>;
  validator?: UseFieldValidatorType;
};

const SearchInput = (props: PropsType): JSX.Element => {
  const { onChange, inputName, type, label, autocompleteComponent: AutocompleteComponent, validator } = props;
  const autocompleteRef = useRef<HTMLDivElement>(null);
  const inputRef = useRef<HTMLInputElement>(null);
  const classes = useStyles();
  const router = useRouter();

  const dispatch = useDispatch();
  const [currentIdx, setCurrentIdx] = useState(-1);

  const [state, { handleInputBlur, handleInputChange, handleInputFocus }] = useField({
    inputValidator: validator,
  });

  const { isFocused, isValid, value } = state;

  const [list, { hasError }] = useAutocomplete<AutocompleteResponse['data']>(value, async (query: string) =>
    searchApi.autocomplete[type](query).then((res) => (res.data as unknown) as AutocompleteResponse['data']),
  );

  const handleSelect = (alias: string): void => {
    handleInputChange(alias);
    onChange(alias, type);
    handleInputBlur();
  };

  const handleFocus = (): void => {
    handleInputFocus();
    inputRef.current.focus();
  };

  const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
    handleInputChange(event);
    const isValueValid = validator ? validator(event.target.value).success : true;

    onChange(event.target.value, event.target.name, isValueValid);
  };

  useClickOutside(autocompleteRef, () => {
    handleInputBlur();
  });

  useEffect(() => {
    (async (): Promise<void> => {
      if (router.pathname !== '/' && !value) {
        const { alias, slug } = await getInitialInputValuesFromQuery(router, type);

        handleInputChange(alias);
        onChange(alias, type, isValid, slug);
      }
    })();
  }, []);

  return (
    <div className={classes['search-input']} ref={autocompleteRef}>
      <div onClick={handleFocus}>
        <label
          className={cx(classes['search-input__input__label'], {
            [classes['search-input__input__label--active']]: isFocused || !!value,
          })}
        >
          {label}
        </label>
        <input
          id={inputName}
          onChange={handleChange}
          onFocus={handleInputFocus}
          name={inputName}
          value={value}
          className={classes['search-input__input__field']}
          autoComplete="off"
          checked={false}
          ref={inputRef}
          maxLength={25}
        />
        <div
          className={cx(classes['input__decorator'], {
            [classes['input__decorator--focused']]: isFocused,
          })}
        />
      </div>
      {transitions(
        ({ transform, opacity }, item) =>
          item && (
            <animated.div className={classes['search-input__dropdown']} style={{ opacity: opacity as any, transform }}>
              {value.length > 2 && (
                <AutocompleteComponent
                  onSelect={handleSelect}
                  inputValue={value}
                  currentIndex={currentIdx}
                  hasError={hasError}
                  list={list}
                />
              )}
            </animated.div>
          ),
      )}
    </div>
  );
};

export default SearchInput;

Upvotes: 0

Views: 838

Answers (1)

Jorge Kunrath
Jorge Kunrath

Reputation: 1006

Disclaimer: i don't fully read the code because there's a few things in there that I don't understand yet (typescript). But considering this:

Autocomplete component is a list

how can I check if the input value is the same as the item taken from the autocomplete

You can use Array.prototype.includes() to match the user input with your list before sending the form.

let userInput = "foo"
let userInput2 = "opsIAmInvalid"

let apiList = ["foo", "doo", "boo"]

apiList.includes(userInput) // true
apiList.includes(userInput2) // false

Another broad approach could be done with a more complex component. Two ideas:

  1. Two inputs, one is your user text that filters a list, other is the actual item selected from the list. The first one you ignore when sending the form and you can use some focus validation to change styles. (I don't like this idea tho :x)
  2. One input that your user type anything that filters a list. When they clicks in something from the list you save it as variable/state somewhere. You ignore the input on submit, just validates if the variable/state exists.

Upvotes: 1

Related Questions