Add focus to AutoComplete/Autosuggest input component issue

364 Views Asked by At

I want to set focus to Autosuggest input element (function components) automatically in React/TypeScript. I tried different solutions but they all failed for me. I added the full code without any modification.

NumberAutoForm.tsx

    import React, {useState, useEffect} from 'react';
    import {useDispatch, useSelector} from 'react-redux';
    import useForm from 'react-hook-form';
    import {useId} from 'react-id-generator';
    import {getInfoByType} from '../../core/typeAuto';
    import {useStepsContext} from '../../utils/StepsProvider';
    import {hasCityZone} from '../../redux-data/city/cityReducer';
    import {AutoComplete} from '../MainPage/AutoComplete/AutoComplete';
    import {getPrivelege} from '../../redux-data/insurance/insuranceSelector';
    import SquareLoader from 'react-spinners/SquareLoader';
    import {config} from '../../assets/config';
    import {getVehicle, getVehicleData, getVehicleError, getVehicleLoading} from '../../redux-data/vehicle/vehicleReducer';
    import serviceOsago from '../../service/serviceOsago';
    import {setOfferMark, setOfferModel} from '../../redux-data/offer/offerReducer';
    import {setDataOrder} from '../../redux-data/insurance/insuranceReducer';
    import CustomField from '../MainPage/CustomField';
    import Select from "react-select";
    import {getOptionsByKind} from "../../core/typeAuto";
    
    const NumberAutoForm = (props) => {
      const isCityZone = useSelector(hasCityZone);
      const vehicle = useSelector(getVehicleData);
      const isLoading = useSelector(getVehicleLoading);
      const privilege = useSelector(getPrivelege);
      const regError = useSelector(getVehicleError);
      const [vehicleEngineOptions, setVehicleEngineOptions] = useState([{}]);
      const [engineType, setEngineType] = useState("");
    
      const {register, handleSubmit, errors} = useForm({
        reValidateMode: 'onChange',
        mode: 'onChange',
        defaultValues: {
          privilege
        }
      });
    
      const handleChangeEngineType = (e: any) => {
        vehicle.type = e.value;
        setEngineType(e);
      };
    
      const [checkboxId] = useId(1, 'checkbox');
      const [formId] = useId(1, 'form');
    
      const dispatch = useDispatch();
      const {goTo} = useStepsContext() as any;
    
      const onSubmit = async (data: any) => {
        if (!vehicle || (vehicle && !vehicle.type)) {
          await dispatch(getVehicle(data.register_auto));
        } else {      
          if (isCityZone) {
            dispatch(setDataOrder(data));
            goTo(2);
          }
        }
      };
    
      useEffect(() => {
        const handle = async () => {
          const {modelName, brandName} = vehicle;
    
          if (brandName) {
            const marks = await serviceOsago.getMarkCode(brandName);
            const mark = marks && marks.length > 0 ? marks.find((m:any)=>m.id === vehicle.id) : null;
            dispatch(setOfferMark(mark));
    
            if (modelName && mark) {
              const models = await serviceOsago.mapGetModelCode(mark.id);
              const findModel = models.find((item: any) => item.label.toLowerCase() === modelName.toLowerCase());
              dispatch(setOfferModel(findModel));
            }
          }
        };
    
        const validateVehicleType = (kind) => {
          setVehicleEngineOptions(getOptionsByKind(kind));
        };
    
        if (vehicle) {
            handle();
    
            if (!vehicle.type) {
                validateVehicleType(vehicle.kind);
            }
        }
      }, [vehicle]);
    
      return (
        <form onSubmit={handleSubmit(onSubmit)} id={formId} noValidate>
          {(!vehicle || (vehicle && !vehicle.id)) && (
            <>
              <div className="form-group">
                <CustomField
                  className="vehicle-number"
                  register={register({
                    required: true,
                    pattern: {
                      value: /[A-zА-я-І-і-Ї-ї]{2}\d{4}[A-zА-я-І-і-Ї-ї]{2}/,
                      message: 'Реєстраційний номер ТЗ не відповідає правилам перевірки'
                    },
                    maxLength: {
                      value: 8,
                      message: 'Реєстраційний номер ТЗ не відповідає правилам перевірки',
                    },
                    minLength: {
                      value: 8,
                      message: 'Реєстраційний номер ТЗ не відповідає правилам перевірки',
                    }
                  })}
                  errors={errors}
                  name={"register_auto"}
                  label={''}
                  placeholder={"АА 00 00 АА"}
                  onChange={props.handleChange}
                  defaultValue={props.vehicleNumber}
                  autoFocus
                />
                <p>{regError || (vehicle && !vehicle.id && 'Нiчого не знайдено')}</p>
                <SquareLoader loading={isLoading} size={20} color={config.color} css={config.css} />
              </div>
              <button type="submit" className="btn btn-primary" disabled={isLoading}>
                Розрахувати вартість
              </button>
            </>
          )}
          {vehicle && (
            <div>
              <div className="vehicle-info">
                <p>
                  Марка
                  <br />
                  <strong>{vehicle.brandName || 'Н/Д'}</strong>
                </p>
                <p>
                  Модель
                  <br />
                  <strong>{vehicle.modelName || 'Н/Д'}</strong>
                </p>
                <p>
                  Номер авто
                  <br />
                  <strong>{vehicle.regNumber}</strong>
                </p>
                <p>
                  {vehicle.type ? getInfoByType(vehicle.type).name : "Обсяг"}
                  <br />
                  {
                    vehicle.type ? <strong>{getInfoByType(vehicle.type).label}</strong> : <Select options={vehicleEngineOptions} isSearchable={false}
                    styles={{
                      control: (provide: any) => ({
                        ...provide,
                        border: "0.2rem solid #FF0000"
                      }),
                      valueContainer: (provide: any) => ({
                        ...provide,
                        flexWrap: "nowrap"
                      }),
                      placeholder: () => ({
                        color: "#000000",
                      }),
                    }}
                    theme={theme => ({
                      ...theme,
                      borderRadius: 0,
                      colors: {
                        ...theme.colors,
                        primary25: '#1BA876',
                        primary: '#B4C9C1'
                      }
                    })}
                    onChange={handleChangeEngineType}
                    value={engineType}
                    placeholder={"Обсяг двигуна"}
                    />
                  }
                </p>
              </div>
              <div className="checkbox-car">
              <div className="form-group form-check" style={{marginRight: '2rem'}}>
                <input ref={register} name="privilege" id={checkboxId} type="checkbox" hidden />
                <label htmlFor={checkboxId}>Є пільга.</label>
              </div>
              <div className="form-group form-check">
                <input ref={register} name="taxi" id="taxi" type="checkbox" hidden />
                <label htmlFor="taxi">Таксі</label>
              </div>
              </div>
              <AutoComplete />
              <button type="submit" className="btn btn-primary">
                Далі
              </button>
            </div>
          )}
        </form>
      );
    };
    
    export default NumberAutoForm;

AutoComplete.tsx

import React, { useState } from 'react';
import Autosuggest from 'react-autosuggest';
import {useDispatch, useSelector} from 'react-redux';
import {
  getErrorCity,
  getLoadingCity,
  getRegisterCity,
  setData,
  setError
} from '../../../redux-data/city/cityReducer';
import {getInstanceError} from '../../../utils/getInstanceError';
import theme from './AutoComplete.module.css';
import SquareLoader from 'react-spinners/SquareLoader';
import {config} from '../../../assets/config';
import serviceOsago from '../../../service/serviceOsago';

const getSuggestions = (value, cities) => {    
  if (!value) return [];
  const inputValue = value.trim().toLowerCase();
  const inputLength = inputValue.length;

  const res = inputLength === 0 ? [] : cities
  return res;
};

const renderSuggestion = suggestion => (
  <div>
    {suggestion.nameFull}
  </div>
);

const getSuggestionValue = suggestion => suggestion.nameFull;

export const AutoComplete = () => {
  const [touch, setTouch] = useState(false);
  const dispatch = useDispatch();
  const regCity = useSelector(getRegisterCity);
  const loading = useSelector(getLoadingCity);
  const [suggestions, setSuggestions] = useState([] as any[]);
  const [value, setValue] = useState(regCity);
  const [cities, setCities] = useState([])

  const error = useSelector(getErrorCity);
  const errors = {
    regCity: error
  };
  const {
    getClassError,
    getMessageError
  } = getInstanceError(errors);
  const mes = getMessageError('regCity');

  const onSuggestionsFetchRequested = ({ value }) => {
    setSuggestions(getSuggestions(value, cities));
  };

  const onSuggestionsClearRequested = () => {
    setSuggestions([]);
  };

  const onChange = (event, {newValue}) => {    
    setValue(newValue);
    serviceOsago.getCityCode(value).
    then((value:any)=>setCities(value)
    )
  };

  const onBlur = () => {
    setTouch(true);
    if (value === '') {
      dispatch(setError({
        message: 'Це поле обов\'язкове'
      }));
    }
  };

  const inputProps = {
    placeholder: 'Місто реєстрації автомобіля',
    value,
    onChange,
    onBlur,
    disabled: loading
  };

  return (
    <div className={getClassError('regCity', touch)}>
      <Autosuggest 
        suggestions={suggestions}
        onSuggestionsFetchRequested={onSuggestionsFetchRequested}
        onSuggestionsClearRequested={onSuggestionsClearRequested}
        getSuggestionValue={getSuggestionValue}
        renderSuggestion={renderSuggestion}
        inputProps={inputProps}
        onSuggestionSelected={(event, data) => {
          dispatch(setData(data.suggestion));
        }}
        theme={theme}
      />
      {mes && <p>{mes}</p>}
      <SquareLoader loading={loading} size={20} color={config.color} css={config.css}/>
    </div>
  );
};

Screenshot: enter image description here

This project uses the following dependencies (package.json):

{
  "name": "my-web-site",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@kunukn/react-collapse": "^2.2.7",
    "@rehooks/window-scroll-position": "^1.0.1",
    "@types/dotenv": "=6.1.1",
    "@types/express": "=4.17.1",
    "@types/js-base64": "=2.3.1",
    "@types/lodash": "=4.14.149",
    "@types/morgan": "=1.7.37",
    "@types/node": "10.17.17",
    "@types/node-fetch": "=2.5.0",
    "@types/node-sass": "=4.11.0",
    "@types/react": "^16.9.16",
    "@types/react-autosuggest": "^9.3.13",
    "@types/react-dom": "16.8.5",
    "@types/react-fontawesome": "=1.6.1",
    "@types/react-helmet": "=5.0.3",
    "@types/react-input-mask": "^2.0.4",
    "@types/react-redux": "^7.1.5",
    "@types/react-router-config": "=5.0.0",
    "@types/react-router-dom": "^5.1.3",
    "@types/react-select": "=3.0.2",
    "@types/uuid": "^3.4.6",
    "@types/whatwg-fetch": "^0.0.33",
    "app-root-path": "^2.0.1",
    "axios": "^0.21.1",
    "babel-loader": "^8.0.6",
    "babel-plugin-import": "^1.12.1",
    "body-parser": "^1.18.2",
    "bootstrap": "^4.3.1",
    "cross-env": "^5.1.3",
    "crypto": "^1.0.1",
    "customize-cra": "^0.6.0",
    "dotenv": "^8.1.0",
    "dotenv-webpack": "^1.7.0",
    "es6-promise": "^4.2.2",
    "express": "^4.17.1",
    "fetch-everywhere": "^1.0.5",
    "file-loader": "^3.0.1",
    "ignore-loader": "^0.1.2",
    "ignore-styles": "^5.0.1",
    "js-base64": "^2.4.3",
    "less": "^3.9.0",
    "less-loader": "^5.0.0",
    "lodash": "^4.17.4",
    "lodash.throttle": "^4.1.1",
    "morgan": "^1.9.1",
    "node-fetch": "^2.6.0",
    "node-sass": "^4.12.0",
    "nodemon": "^1.19.2",
    "odata-query": "6.0.0-1",
    "rc-steps": "^3.5.0",
    "re-reselect": "^3.4.0",
    "react": "^17.0.2",
    "react-app-rewire-less": "^2.1.3",
    "react-app-rewired": "^2.1.3",
    "react-autosuggest": "^9.4.3",
    "react-bootstrap": "^1.0.0-beta.15",
    "react-countdown": "^2.3.2",
    "react-dom": "^17.0.2",
    "react-dropdown-date": "^2.2.7",
    "react-ga": "^2.6.0",
    "react-helmet": "^5.2.0",
    "react-hook-form": "^3.28.2",
    "react-hotjar": "^2.0.2",
    "react-id-generator": "^3.0.0",
    "react-input-mask": "^2.0.4",
    "react-lines-ellipsis": "^0.14.1",
    "react-modal": "^3.11.1",
    "react-modern-calendar-datepicker": "^3.1.6",
    "react-owl-carousel2": "^0.3.0",
    "react-redux": "7.1.3",
    "react-router-config": "4.4.0-beta.8",
    "react-router-dom": "^5.1.2",
    "react-scripts": "3.0.1",
    "react-scripts-ts": "2.13.0",
    "react-select": "=3.0.8",
    "react-spinners": "=0.7.1",
    "redux": "=4.0.4",
    "redux-actions": "=2.6.5",
    "redux-thunk": "=2.3.0",
    "reselect": "=4.0.0",
    "serve-favicon": "=2.4.5",
    "ts-loader": "=6.0.4",
    "ts-node": "=8.3.0",
    "use-media": "=1.4.0",
    "use-react-router": "=1.0.7",
    "uuid": "=3.3.3",
    "webpack-cli": "=3.3.8",
    "webpack-node-externals": "=1.7.2",
    "whatwg-fetch": "=3.0.0"
  },
  "scripts": {
    "prestart": "yarn run build",
    "start": "yarn start:server",
    "start:dev": "yarn run build && yarn start:server:dev",
    "start:server": "cross-env NODE_ENV=production ts-node build_server/server.js",
    "start:server:dev": "cross-env NODE_ENV=development ts-node build_server/server.js",
    "start:client": "react-app-rewired start",
    "build:server": "ts-node node_modules/webpack/bin/webpack.js --config webpack.server.config.js",
    "build:client": "react-app-rewired build",
    "build": "yarn run build:client && yarn run build:server",
    "test": "react-app-rewired test --env=jsdom",
    "eject": "react-app-rewired eject"
  },
  "eslintConfig": {
    "extends": "react-app"
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  },
  "devDependencies": {
    "concurrently": "=3.5.1",
    "style-loader": "=1.0.0",
    "typescript": "3.5.3"
  }
}

Any ideas how to set focus on such component? Thank you.

3

There are 3 best solutions below

9
Pluto On

You can use the inputProps prop of the react-autosuggest component to pass a ref object to the input element, and then use the useEffect hook to call the focus method on the ref when the component mounts.

For example:

// Form Component

const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
    if (inputRef.current) {
      inputRef.current.focus();
    }
}, [inputRef]);


// AutoComplete Component

const inputProps: InputProps = {
    value,
    onChange: (event, { newValue }) => {
      setValue(newValue);
    },
    ref
};

<Autosuggest<Suggestion, InputProps>
    {...restProps}
    inputProps={inputProps}
/>

You can see the whole example here: codesandbox.io

14
Starcc On

Hope this one help you

import React, { useState, useRef, useEffect } from 'react';
import Autosuggest from 'react-autosuggest';
import { useDispatch, useSelector } from 'react-redux';
import {
  getErrorCity,
  getLoadingCity,
  getRegisterCity,
  setData,
  setError
} from '../../../redux-data/city/cityReducer';
import {getInstanceError} from '../../../utils/getInstanceError';
import theme from './AutoComplete.module.css';
import SquareLoader from 'react-spinners/SquareLoader';
import {config} from '../../../assets/config';
import serviceOsago from '../../../service/serviceOsago';

const getSuggestions = (value, cities) => {    
  if (!value) return [];
  const inputValue = value.trim().toLowerCase();
  const inputLength = inputValue.length;

  const res = inputLength === 0 ? [] : cities
  return res;
};

const renderSuggestion = suggestion => (
  <div>
    {suggestion.nameFull}
  </div>
);

const getSuggestionValue = suggestion => suggestion.nameFull;

export const AutoComplete = () => {
  const [touch, setTouch] = useState(false);
  const dispatch = useDispatch();
  const regCity = useSelector(getRegisterCity);
  const loading = useSelector(getLoadingCity);
  const [suggestions, setSuggestions] = useState([] as any[]);
  const [value, setValue] = useState(regCity);
  const [cities, setCities] = useState([])
  const inputRef = useRef(null);

  const error = useSelector(getErrorCity);
  const errors = {
    regCity: error
  };
  const {
    getClassError,
    getMessageError
  } = getInstanceError(errors);
  const mes = getMessageError('regCity');

  const onSuggestionsFetchRequested = ({ value }) => {
    setSuggestions(getSuggestions(value, cities));
  };

  const onSuggestionsClearRequested = () => {
    setSuggestions([]);
  };

  const onChange = (event, {newValue}) => {    
    setValue(newValue);
    serviceOsago.getCityCode(value).
    then((value:any)=>setCities(value)
    )
  };

  const onBlur = () => {
    setTouch(true);
    if (value === '') {
      dispatch(setError({
        message: 'Це поле обов\'язкове'
      }));
    }
  };

  useEffect(() => {
    if (inputRef.current) {
      inputRef.current.focus();
    }
  }, []);

  const inputProps = {
    placeholder: 'Місто реєстрації автомобіля',
    value,
    onChange,
    onBlur,
    disabled: loading,
    ref: inputRef
  };

  return (
    <div className={getClassError('regCity', touch)}>
      <Autosuggest 
        suggestions={suggestions}
        onSuggestionsFetchRequested={onSuggestionsFetchRequested}
        onSuggestionsClearRequested={onSuggestionsClearRequested}
        getSuggestionValue={getSuggestionValue}
        renderSuggestion={renderSuggestion}
        inputProps={inputProps}
        onSuggestionSelected={(event, data) => {
          dispatch(setData(data.suggestion));
        }}
        theme={theme}
      />
      {mes && <p>{mes}</p>}
      <SquareLoader loading={loading} size={20} color={config.color} css={config.css}/>
    </div>
  );
};
0
Hunter91151 On

I have fixed this issue by using renderInputComponent (https://github.com/moroshko/react-autosuggest#render-input-component-prop)

NumberAutoForm.tsx

I set isAutoFocus={true} and id="requiredField" to AutoComplete component:

<AutoComplete isAutoFocus={true} id="requiredField" />

AutoComplete.tsx

const inputProps = {
    placeholder: 'Місто реєстрації автомобіля',
    value,
    onChange,
    onBlur,
    disabled: loading,
    id: props.id
  };

  const renderSuggestionInput = (inputProps) => (
    props.isAutoFocus ? <input {...inputProps} autoFocus /> : <input {...inputProps} />
  );

  return (
    <div className={getClassError('regCity', touch)}>
      <Autosuggest 
        suggestions={suggestions}
        onSuggestionsFetchRequested={onSuggestionsFetchRequested}
        onSuggestionsClearRequested={onSuggestionsClearRequested}
        getSuggestionValue={getSuggestionValue}
        renderSuggestion={renderSuggestion}
        inputProps={inputProps}
        onSuggestionSelected={(event, data) => {
          dispatch(setData(data.suggestion));
        }}
        theme={theme}
        renderInputComponent={renderSuggestionInput}
      />
      {mes && <p>{mes}</p>}
      <SquareLoader loading={loading} size={20} color={config.color} css={config.css}/>
    </div>
  );

In const inputProps object I set id: props.id just to apply some styling in scss file for AutoSuggest input, for example:

#requiredField {
   border: 0.2rem solid #FF0000;
}

I set the focus on AutoSuggest input in the renderSuggestionInput function. I added additional checks props.isAutoFocus ? <input {...inputProps} autoFocus /> : <input {...inputProps} /> to make sure it will set the focus on the required field otherwise it will set focus on all Autosuggest inputs on the website. Finally, I call renderInputComponent={renderSuggestionInput} on Autosuggest component.

Screenshot: enter image description here

Everything works well. This issue is resolved. Thank you.