🙆

Reactで入力候補の出るinput(type="text")フォーム作成

2022/07/16に公開

やったこと

Reactを使って、入力候補のでるinputフォーム(type="text")を作りました。
入力候補(autoComplete)を出すためにはHTML5で出てきたdatalistを使った方法ライブラリで簡単に実装できる方法など、いろいろな実装の仕方がありますが、そのようにデフォルトのものを使ってしまうと思い通りのスタイルに調整するのが面倒くさくなってしまいます。
また、私が現在作っているオリジナルアプリにはChakra-uiを使っているため、変にライブラリを使うなどしてそこだけ独自のcssを使ってしまうと統一感のないコードやスタイルになってしまうと感じたため、chakra-uiをそのまま使えるようにしました。

なぜselectや検索ではなくinput(type="text")フォームなのか

私が現在作っているアプリでは、クリエイターが自分の使えるツールを登録する機能があり、そこではデータベースに存在するツールであれば入力候補にテキストを出し、登録したツールのロゴ画像も同時に登録できるようになっています。そして、データベースに存在しないものはそのままフィールドに入った文字列を登録するという仕組みになっています。
入力候補のもののみを保存する場合はSelectBoxなどを選択すればいいですが、以上の例のように、入力候補以外のテキストも作成できるようにするためにはこのような入力候補が出るTextFieldを選択する必要があると思います。

完成物

作成手順

  1. プロジェクト作成
  2. chakra-uiを導入
  3. ダミーデータ作成
  4. inputフォーム作成
  5. 入力候補表示

1. プロジェクト作成

今回はapp01というプロジェクトをcreate-next-appで作成します。
※今回nextを使っていますがindexページしか使わないのでcreate-react-appのApp.jsを使って同じように使えます。

$ npx create-next-app app01

作成したらそのプロジェクトのディレクトリに進み、エディターを開いてください。

$ cd react-app
react-app $ code .

2. chakra-uiを導入

以下のchakra-uiの公式サイトにある通りにコマンドを実行します。

$ npm i @chakra-ui/react @emotion/react@^11 @emotion/styled@^11 framer-motion@^6

そして、全てのコンポーネントをChakraProviderで囲みます。そうすることでChakra-uiが使えるようになります。
nextjsだと_app.jsのComponentコンポーネントを囲みましょう。

_app.js
import { ChakraProvider } from "@chakra-ui/react";
import "../styles/globals.css";

function MyApp({ Component, pageProps }) {
  return (
    // ここに追加
    <ChakraProvider>
      <Component {...pageProps} />
    </ChakraProvider>
  );
}

export default MyApp;

3. ダミーデータ作成

ここからはルートページであるindex.jsに記述していきます。
※create-react-appの場合にはApp.jsに記述することになります。

まずはダミーデータを作成していきましょう。これは実際にはAPIやデータベースから取ってくるデータになります。今回は簡易的に作るので直書きしていきます。

index.js
import Head from "next/head";
import styles from "../styles/Home.module.css";

export default function Home() {
  // ここに追加
  const options = [
    { id: 1, text: "React" },
    { id: 2, text: "Ruby on Rails" },
    { id: 3, text: "JavaScript" },
    { id: 4, text: "TypeScript" },
    { id: 5, text: "Go" },
    { id: 6, text: "HTML" },
    { id: 7, text: "CSS" },
  ];

  return (
    <div className={styles.container}>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
    </div>
  );
}

以下の記述でダミーデータの配列を作成しました。

const options = [
    { id: 1, text: "React" },
    { id: 2, text: "Ruby on Rails" },
    { id: 3, text: "JavaScript" },
    { id: 4, text: "TypeScript" },
    { id: 5, text: "Go" },
    { id: 6, text: "HTML" },
    { id: 7, text: "CSS" },
];

ここに追加したoptionsのtextの値をfilterにかけて入力候補に表示されるようにします。idを入れたのは実際だとDBに入っているデータを取り出すためです。

4 inputフォーム作成

まずはコード全体を載せます。

index.js
import React from "react";
import Head from "next/head";
import { Box, Input, Text } from "@chakra-ui/react";
import styles from "../styles/Home.module.css";

export default function Home() {
  // inputに入れる値
  const [text, setText] = React.useState("");
  // inputにフォーカスしているかどうか
  const [isFocus, setIsFocus] = React.useState(false);

  const options = [
    { id: 1, text: "React" },
    { id: 2, text: "Ruby on Rails" },
    { id: 3, text: "JavaScript" },
    { id: 4, text: "TypeScript" },
    { id: 5, text: "Go" },
    { id: 6, text: "HTML" },
    { id: 7, text: "CSS" },
  ];

   // inputフィールドのonChangeイベント
  const handleChange = (text) => {
    setText(text);
  };

  return (
    <div className={styles.container}>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <Box w="100%" mt="40px">
        <Input
	   // inputにフォーカスしていたらisFocusにtrueを入れる
          onFocus={() => setIsFocus(true)}
          type="text"
          value={text}
	  // handleChangeにe.target.valueを入れる
          onChange={(e) => handleChange(e.target.value)}
          placeholder="text..."
        />
	// isFocusがtrueの場合、入力候補を表示する(今回の場合はfilterにかけていないためフォーカスすればoptionsの全ての値が入力候補に表示されます。)
        {isFocus && (
          <Box
            w="100%"
            h="100%"
            boxShadow="md"
            bg="white"
            mt="8px"
            borderRadius="lg"
          >
	      // optionsの配列を入力候補の部分に表示する
            {options?.map((option, i) => (
              <Text
                cursor="pointer"
                bg="white"
                _hover={{ bg: "gray.100" }}
                key={i}
                p="8px 8px"
		// 入力候補をクリックするとクリックした入力候補がinputフィールドに入力される。
		// isFocusをfalseにすることで入力候補を非表示にする
                onClick={async () => {
                  await setText(option.text);
                  await setIsFocus(false);
                }}
              >
                {option.text}
              </Text>
            ))}
          </Box>
        )}
      </Box>
    </div>
  );
}

上のコードを説明していきます。

  • textはinputのvalueに当たる内容になります。入力した値が入ります。入力候補を選んだ場合もこのtextに入るようにします。
  • inputにフォーカスしているかどうかを判定しています。これを指定することで入力候補の表示・非表示をコントロールできるようになります。
// inputに入れる値
const [text, setText] = React.useState("");
// inputにフォーカスしているかどうか
const [isFocus, setIsFocus] = React.useState(false);
  • inputに入力した値をtextにいれるための関数です。onChangeに使います。
// inputフィールドのonChangeイベント
const handleChange = (text) => {
  setText(text);
};
  • chakra-uiのInputを使いました。
  • chakra-uiのonFocusはinputをfocusした時にtrue、外れたらfalseになります。
  • onChange属性にhandleChangeを使っています。
<Input
  // inputにフォーカスしていたらisFocusにtrueを入れる
  onFocus={() => setIsFocus(true)}
  type="text"
  value={text}
  // handleChangeにe.target.valueを入れる
  onChange={(e) => handleChange(e.target.value)}
  placeholder="text..."
/>
  • isFocusがtrueの場合に入力候補が表示されるようになります。
  • 今回はisFocusがtrueになったらfilterをかけていないため、optionsにある値全てが表示されるようになっている。
  • 最終的には入力された値に基づいてfilterにかかった入力候補のみが表示されるようになる。
// isFocusがtrueの場合、入力候補を表示する(今回の場合はfilterにかけていないためフォーカスすればoptionsの全ての値が入力候補に表示されます。)
{isFocus && (
  • isFocusがtrueの場合、optionsをmapで回し入力候補として表示する。
  • 表示された入力候補の1つをクリックすると、その値がtextに入り、isFocusがfalseになって入力候補が非表示になる。
 // optionsの配列を入力候補の部分に表示する
{options?.map((option, i) => (
  <Text
	cursor="pointer"
	bg="white"
	_hover={{ bg: "gray.100" }}
	key={i}
	p="8px 8px"
	// 入力候補をクリックするとクリックした入力候補がinputフィールドに入力される。
	// isFocusをfalseにすることで入力候補を非表示にする
	onClick={async () => {
	  await setText(option.text);
	  await setIsFocus(false);
	}}
	>
		{option.text}
  </Text>
))}

5. 入力候補表示

最後にinputを入力した値をもとに、optionsにfilterをかけて入力候補を表示する。

index.js
import React from "react";
import Head from "next/head";
import { Box, Input, Text } from "@chakra-ui/react";
import styles from "../styles/Home.module.css";

export default function Home() {
  const [text, setText] = React.useState("");
  const [isFocus, setIsFocus] = React.useState(false);
  // フィルターにかけた配列をいれるためのステート
  const [suggestions, setSuggestions] = React.useState([]);

  const options = [
    { id: 1, text: "React" },
    { id: 2, text: "Ruby on Rails" },
    { id: 3, text: "JavaScript" },
    { id: 4, text: "TypeScript" },
    { id: 5, text: "Go" },
    { id: 6, text: "HTML" },
    { id: 7, text: "CSS" },
  ];

  const handleChange = (text) => {
    // 入力した値をもとにフィルターをかける。
    // 空の配列を用意
    let matches = [];
    // 入力する値が0文字より大きければ処理を行う
    if (text.length > 0) {
      matches = options.filter((opt) => {
          // new RegExp = パターンでテキストを検索するために使用
        const regex = new RegExp(`${text}`, "gi");
        return opt.text.match(regex);
      });
    }
    // フィルターをかけた配列をsuggestionsのステートに入れる
    setSuggestions(matches);
    setText(text);
  };

  return (
    <div className={styles.container}>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <Box w="100%" mt="40px">
        <Input
          onFocus={() => setIsFocus(true)}
          type="text"
          value={text}
          onChange={(e) => handleChange(e.target.value)}
          placeholder="text..."
        />
        {isFocus && (
          <Box
            w="100%"
            h="100%"
            boxShadow="md"
            bg="white"
            mt="8px"
            borderRadius="lg"
          >
	    // フィルターをかけた配列を回す
            {suggestions?.map((suggestion, i) => (
              <Text
                cursor="pointer"
                bg="white"
                _hover={{ bg: "gray.100" }}
                key={i}
                p="8px 8px"
                onClick={async () => {
		    // textにフィルターをかけた入力候補の値を入れる
                  await setText(suggestion.text);
                  await setIsFocus(false);
                }}
              >
                {suggestion.text}
              </Text>
            ))}
          </Box>
        )}
      </Box>
    </div>
  );
}

上のコードを説明します。

  • フィルターによって絞った配列をsuggestionsに入れる。これが入力候補として表示されるようにする。
// フィルターにかけた配列をいれるためのステート
const [suggestions, setSuggestions] = React.useState([]);
  • 空の配列matchesにfilterで文字列に当てはまった値を入れていく。
  • RegExpオブジェクトはパターンでテキストを検索するために使用するものです。詳しくは説明しないので公式サイトから見てみてください。
  • テキスト検索してフィルターにかけられたものが入ったmatches配列をsuggestionsに入れる。→これを入力候補として表示する。
const handleChange = (text) => {
    // 入力した値をもとにフィルターをかける。
    // 空の配列を用意
    let matches = [];
    // 入力する値が0文字より大きければ処理を行う
    if (text.length > 0) {
      matches = options.filter((opt) => {
          // new RegExp = パターンでテキストを検索するために使用
        const regex = new RegExp(`${text}`, "gi");
        return opt.text.match(regex);
      });
    }
    // フィルターをかけた配列をsuggestionsのステートに入れる
    setSuggestions(matches);
    setText(text);
};
  • 入力された値によって絞られた値が入ったsuggestionsが入力候補として表示される。
// フィルターをかけた配列を回す
{suggestions?.map((suggestion, i) => (
    <Text
	cursor="pointer"
	bg="white"
	_hover={{ bg: "gray.100" }}
	key={i}
	p="8px 8px"
	onClick={async () => {
	  // textにフィルターをかけた入力候補の値を入れる
	  await setText(suggestion.text);
	  await setIsFocus(false);
	}}
	>
		{suggestion.text}
    </Text>
))}

これで完成です。
以下が今回のリポジトリになります。
https://github.com/Matsushoooo12/react-input-autocomplete

Discussion