Компонент автозаполнения поиска с React и TypeScript

Компонент автозаполнения поиска с React и TypeScript

28 ноября 2022 г.

Как показать предложения данных из API Rest

В настоящее время одним из наиболее широко используемых компонентов веб-сайта являются поисковые системы с автозаполнением или подсказками.

Обычно это первый компонент, с которым взаимодействует пользователь, так как практичнее выполнить поиск и сразу перейти к тому, что нам нужно. Эти компоненты необходимы для удобства пользователей на таких сайтах, как интернет-магазины.

В этом руководстве мы создадим простой компонент поиска, который предлагает пользователям подсказки о том, что они печатают, без сторонних библиотек. п

Что такое автозаполнение поиска?

Автозаполнение – это шаблон, используемый для отображения предложений запроса.

Поиск с автозаполнением, также называемый "интеллектуальным поиском" или "автопредложением", представляет собой компонент, который пользователь вводит в поле ввода и предлагает различные прогнозы или возможные результаты того, как поиск может быть завершен.

Автозаполнение работает с поисковой системой, которая учится и улучшает предлагаемые результаты по мере того, как ее пользователи выполняют поиск.

В этом случае мы не будем больше говорить о поисковых системах, потому что это выходит за рамки учебника. Если вы хотите узнать больше об этой теме, посетите этот сайт. . Без лишних слов перейдем к программированию.

Настройка автозаполнения поиска

Мы создаем наше приложение с приглашением со следующими команда:

yarn create vite autocomplete-search --template react-ts

Устанавливаем зависимости, которые нам понадобятся в проекте:

yarn add @nextui-org/react

В этом случае я буду использовать стороннюю библиотеку только для стилей, которые вы можете использовать как хотите:

* nextui фреймворк Javascript/CSS

После этого мы создаем следующую структуру папок для проекта:

src/
├── components/
   └── ui/
       ├── Autocomplete.tsx
       ├── Autocomplete.tsx
       ├── index.ts
       └── ui.module.css
├── hooks/
   └── useAutocomplete.ts
├── ts/
   └──interfaces/
      └── Country.interface.ts
├── App.tsx
└── main.tsx

Компоненты

AutocompleteWrapper.tsx Этот компонент используется только как контейнер или оболочка для Autocomplete.tsx, и именно здесь мы будем запрашивать нужные нам данные.

Мы используем API restcountries для руководства и используем только англоязычные страны, поэтому запрос будет выглядеть следующим образом:

https://restcountries.com/v3.1/lang/eng

Autocomplete.tsx Это основной компонент, состоящий из двух разделов. Первый раздел — это элемент ввода, а второй — список предложений.

Обычно используется элемент <ul> или <ol>, но в этом руководстве мы будем использовать компоненты Rows внутри компонента Next UI Card.

import { Card, Col, Input, Row, Text, User } from "@nextui-org/react"
import { useEffect, useRef, useState } from "react"

import { Country } from "../../ts/interfaces/Country.interface"

import classes from "./ui.module.css"

interface Props {
  data: Country[];
}

const Autocomplete = ({ data }: Props) => {
  const inputSearchRef = useRef < HTMLInputElement > null

  const [searchedValue, setSearchedValue] = useState("")
  const [suggestions, setSuggestions] = useState < Country[]> []
  const [selectedSuggestion, setSelectedSuggestion] = useState("")
  const [activeSuggestion, setActiveSuggestion] = useState(0)

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

  const handleChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
    if (event.target.value !== "") {
      const filteredSuggestions = data.filter((itemData) => {
        const value = event.target.value.toUpperCase()
        const name = itemData.name.common.toUpperCase()

        return value && name.startsWith(value) && name !== value
      })
      setSearchedValue(event.target.value)
      setSuggestions(filteredSuggestions)
    }
  }

  const handleKeyDown = (
    event: React.KeyboardEvent<HTMLInputElement>
  ): void => {
    if (event.key === "ArrowDown" && activeSuggestion < suggestions.length) {
      setActiveSuggestion(activeSuggestion + 1)
    } else if (event.key === "ArrowUp" && activeSuggestion > 1) {
      setActiveSuggestion(activeSuggestion - 1)
    } else if (event.key === "Enter") {
      setSearchedValue(suggestions[activeSuggestion - 1].name.common)
      setSelectedSuggestion(suggestions[activeSuggestion - 1].name.common)
      setSuggestions([])
      setActiveSuggestion(0)
    }
  }

  const handleClick = (value: string) => {
    setSelectedSuggestion(value)
    setSearchedValue(value)
    setSuggestions([])
    setActiveSuggestion(0)
    //do something else
  }

  return (
    <div className={classes.autocomplete}>
      <Input
        bordered
        labelPlaceholder="Search your Country"
        size="xl"
        value={searchedValue}
        onChange={handleChange}
        onKeyDown={handleKeyDown}
        ref={inputSearchRef}
        color="secondary"
      />

      <Card css={{ marginTop: "0.5rem" }}>
        <Card.Body css={{ padding: "0" }}>
          {!suggestions.length &&
          searchedValue.length &&
          !selectedSuggestion.length ? (
            <Row className={classes.itemListNot}>
              <Col>
                <Text>Nothing to show :(</Text>
              </Col>
            </Row>
          ) : (
            <>
              {suggestions.map(({ name, flags }: Country, index) => (
                <Row
                  key={index}
                  className={`${classes.itemList} ${
                    index === activeSuggestion - 1 ? classes.activeItem : ""
                  }`}
                  onClick={() => handleClick(name.common)}
                >
                  <Col>
                    <User src={flags.svg} name={name.common} squared />
                  </Col>
                </Row>
              ))}
            </>
          )}
        </Card.Body>
      </Card>

      <Text size="$xs">Country selected: {selectedSuggestion}</Text>
    </div>
  )
}

export default Autocomplete

Сначала создадим нужные нам типы. API возвращает нам большое количество данных, которые мы не будем использовать, поэтому упрощение информации и типов будет выглядеть так:

export type Country = {
  name: Name
  flags: Flags
}
type Name = {
  common: string
}
type Flags = {
  png: string
  svg: string
}

После этого создадим следующие состояния:

searchedValue — Здесь мы будем хранить текст, который набирает пользователь.

suggestions — здесь мы будем хранить предложения, которые соответствуют тому, что пишет пользователь.

selectedSuggestion — Здесь мы будем хранить параметр, выбранный пользователем.

activeSuggestion — Здесь мы будем хранить индекс показанных предложений. Мы будем использовать его, чтобы узнать, какое предложение выбрано клавиатурой.

Теперь нам нужно создать функции, которые будут реагировать на события элемента ввода и списка результатов.

handleChange() Эта функция будет выполняться каждый раз, когда пользователь что-то вводит в элемент ввода. Мы проверим, не является ли введенное значение пустым. В противном случае мы установим состояния в их начальные значения.

Если значение, полученное в элементе ввода, не является пустым, функция будет выполнена и отобразит предложения, соответствующие введенному значению.

handleClick() Эта функция будет выполняться, когда пользователь выбирает предложение; мы сохраняем выбранное значение и устанавливаем остальные состояния в их начальные значения.

handleKeyDown() Эта функция будет выполняться при обнаружении события на клавиатуре, поэтому вы можете просмотреть предложения и выбрать одно из них.

Наконец, мы добавили useEffect, чтобы сосредоточиться на элементе ввода при монтировании компонента.

Это все! У нас уже есть поиск с автозаполнением, который мы можем использовать в любом проекте, передав входную ссылку и данные в фильтр.

В качестве дополнительного шага и хорошей практики мы перенесем функциональность в пользовательский хук, и наш компонент станет чище и читабельнее.

import { useEffect, useState } from "react";

import { Country } from "../ts/interfaces/Country.interface";

const useAutocomplete = (
  data: Country[],
  inputSearchRef: HTMLInputElement | null
) => {
  const [searchedValue, setSearchedValue] = useState("");
  const [suggestions, setSuggestions] = useState < Country[] > [];
  const [selectedSuggestion, setSelectedSuggestion] = useState("");
  const [activeSuggestion, setActiveSuggestion] = useState(0);

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

  const handleChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
    if (event.target.value !== "") {
      const filteredSuggestions = data.filter((itemData) => {
        const value = event.target.value.toUpperCase();
        const name = itemData.name.common.toUpperCase();

        return value && name.startsWith(value) && name !== value;
      });
      setSearchedValue(event.target.value);
      setSuggestions(filteredSuggestions);
    } else {
      setSearchedValue("");
      setSuggestions([]);
      setSelectedSuggestion("");
      setActiveSuggestion(0);
    }
  };

  const handleKeyDown = (
    event: React.KeyboardEvent<HTMLInputElement>
  ): void => {
    if (event.key === "ArrowDown" && activeSuggestion < suggestions.length) {
      setActiveSuggestion(activeSuggestion + 1);
    } else if (event.key === "ArrowUp" && activeSuggestion > 1) {
      setActiveSuggestion(activeSuggestion - 1);
    } else if (event.key === "Enter") {
      setSearchedValue(suggestions[activeSuggestion - 1].name.common);
      setSelectedSuggestion(suggestions[activeSuggestion - 1].name.common);
      setSuggestions([]);
      setActiveSuggestion(0);
    }
  };

  const handleClick = (value: string) => {
    setSearchedValue(value);
    setSuggestions([]);
    setSelectedSuggestion(value);
    setActiveSuggestion(0);
    //do something else
  };

  return {
    searchedValue,
    suggestions,
    selectedSuggestion,
    activeSuggestion,
    handleChange,
    handleKeyDown,
    handleClick,
  };
};

export default useAutocomplete;

import { useEffect, useRef } from "react";
import { Card, Col, Input, Row, Text, User } from "@nextui-org/react";

import { Country } from "../../ts/interfaces/Country.interface";

import useAutocomplete from "../../hooks/useAutocomplete";

import classes from "./ui.module.css";

interface Props {
  data: Country[];
}

const Autocomplete = ({ data }: Props) => {
  const inputSearchRef = useRef < HTMLInputElement > null;

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

  const {
    searchedValue,
    suggestions,
    selectedSuggestion,
    activeSuggestion,
    handleChange,
    handleKeyDown,
    handleClick,
  } = useAutocomplete(data, inputSearchRef.current);

  return (
    <div className={classes.autocomplete}>
      <Input
        bordered
        labelPlaceholder="Search your Country"
        size="xl"
        value={searchedValue}
        onChange={handleChange}
        onKeyDown={handleKeyDown}
        ref={inputSearchRef}
        color="secondary"
      />

      <Card css={{ marginTop: "0.5rem" }}>
        <Card.Body css={{ padding: "0" }}>
          {!suggestions.length &&
          searchedValue.length &&
          !selectedSuggestion.length ? (
            <Row className={classes.itemListNot}>
              <Col>
                <Text>Nothing to show :(</Text>
              </Col>
            </Row>
          ) : (
            <>
              {suggestions.map(({ name, flags }: Country, index) => (
                <Row
                  key={index}
                  className={`${classes.itemList} ${
                    index === activeSuggestion - 1 ? classes.activeItem : ""
                  }`}
                  onClick={() => handleClick(name.common)}
                >
                  <Col>
                    <User src={flags.svg} name={name.common} squared />
                  </Col>
                </Row>
              ))}
            </>
          )}
        </Card.Body>
      </Card>

      <Text size="$xs">Country selected: {selectedSuggestion}</Text>
    </div>
  );
};

export default Autocomplete;

Приложение выглядит так:

Autocomplete search

Посмотрите демо здесь.< /p>

Репозиторий здесь.

Заключение

Мы создали простой компонент поиска, применяя фильтры к полученным данным. Этот поиск может усложняться в зависимости от случая. После захвата выбранного значения можно добавить дополнительные функции, например отобразить сведения о выбранной стране.

Я надеюсь, что это руководство было для вас полезным и что вы узнали что-то новое при разработке этого приложения.

Первоначально опубликовано здесь.


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE