2
Help us understand the problem. What are the problem?

posted at

updated at

Nuxt.jsでインクリメンタルサーチ

はじめに

ある案件で名前検索を実装することになり、文字を入力するたびに検索候補が現れるインクリメンタルサーチを作ってみたいと思い、実装してみました。ちなみにサーバーサイドはexpress、フロントはNuxt.js×TypeScriptです。expressやcss、その他細かい処理は省略していますが、悪しからず。
動きはこんな感じ。これに似た何かを作る際には参考になると思います。
画面収録_2022-05-17_2_17_40_AdobeCreativeCloudExpress.gif

画面収録_2022-05-17_2_24_49_AdobeCreativeCloudExpress.gif

導入

仕様としては、文字を入力するたびにAPIをコールすると操作性が悪いと思い、入力が止まって0.5秒後にAPIをコールするようにした。
検索するテーブルのデータは下記のようにする。

id name
1 シーツ
2 ラロッカ
3 キラ
4 エルドレッド

完成コード

下記コードで実装できた。

nuxt.js/pages/index.vue
<template>
  <div>
    <div>
      <label>名前</label>
      <input 
        v-model="inputName"
        type="text"
        @focus="isOpenNameDropdown = true"
        @input="incrementalSearch"
      />
    </div>
    <div v-if="isOpenNameDropdown">
      <ul v-if="suggestNameList.length">
        <li
          v-for="suggest in suggestNameList"
          :key="suggest"
          @click="selectedNameFilter(suggest)"
        >
          {{ suggest }}
        </li>
      </ul>
    </div>
  </div>
</template>

<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
  data() {
    const inputName: string = ""               // input
    const isOpenNameDropdown: boolean = false  // インクリメンタルサーチの検索結果表示フラグ
    const suggestNameList: string[] = []       // インクリメンタルサーチの検索結果
    const timerId: number = 0                  // setTimeoutで実行するタイマー処理
    return {
      inputName,
      isOpenNameDropdown,
      suggestNameList,
      timerId
    }
  },
  methods: {
    incrementalSearch() {
      clearTimeout(this.timerId) // タイマー処理を終了
      this.timerId = setTimeout(async () => {
        this.suggestNameList = await this.$axios({method: 'post', url: '/api/search', data: {name: inputName}})
      }, 500)
    }
  }
})
</script>
node.js/controller.js
const express = require("express");
const router = express.Router();
const Sequelize = require("sequelize");
const Op = Sequelize.Op;
const Name = require("../../models").Name;

router.post("/api/search", doGetIncrementalSearch)

doGetIncrementalSearch: async function (req, res,next) {
  const searchName = req.body.name ? `%${req.body.name}%` : ""; // あいまい検索するために
  let suggestNameList = await Name.findAll({
    where: {
      name: {
        [Op.like]: searchName
      },
    }
  })
  suggestNameList = suggestNameList.map((s) => s.name); // 配列を作成
  return res.json({
    suggetNameList,
  })
}

nuxt.jsでの処理

nuxt.js/pages/index.vue
<template>
<!-- 省略 -->
  <input 
    v-model="inputName"
    type="text"
    @focus="isOpenNameDropdown = true"
    @input="incrementalSearch"
  />
<!-- 省略 -->
</template>

変数inputNameをバインドさせ、focusイベントでこのinputがfocusされた時に検索候補の一覧を表示するフラグをtrueにし、inputイベントでincrementalSearchのメソッドを発火させています。

nuxt.js/pages/index.vue
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
// 省略
  data() {
    const suggestNameList: string[] = []       // インクリメンタルサーチの検索結果
    const timerId: number = 0                  // setTimeoutで実行するタイマー処理
    return {
      suggestNameList,
      timerId
    }
  },
  methods: {
    incrementalSearch() {        // inputイベントで呼び出されているメソッド
      clearTimeout(this.timerId) // タイマー処理を終了
      this.timerId = setTimeout(async () => {
        this.suggestNameList = await this.$axios({method: 'post', url: '/api/search', data: {name: inputName}})
      }, 500)
    }
  }
// 省略
})
</script>

まず、dataでtimerIdという数値型の変数を定義する。このtimeIdはsetTimeoutで呼び起こされるタイマー処理のIDを格納するためのものです。そしてincrementalSearchメソッドの中でtimerIDにタイマー処理IDが格納されていれば、それをclearTimeout(this.timerId)で終了させることによって、文字を入力されるたびにAPIをコールしないようにしています。そして、最後の入力はclearTimeoutされないので、setTimeoutが働き、0.5秒後にAPIがコールされます。そして変数suggestNameListにインクリメンタルサーチの検索結果が返却されます。

Expressでの処理(簡潔)

node.js/controller.js
const express = require("express");
const router = express.Router();
const Sequelize = require("sequelize");
const Op = Sequelize.Op;
const Name = require("../../models").Name;

router.post("/api/search", doGetIncrementalSearch)

doGetIncrementalSearch: async function (req, res,next) {
  const searchName = req.body.name ? `%${req.body.name}%` : ""; // あいまい検索するために
  let suggestNameList = await Name.findAll({
    where: {
      name: {
        [Op.like]: searchName
      },
    }
  })
  suggestNameList = suggestNameList.map((s) => s.name); // 配列を作成
  return res.json({
    suggetNameList,
  })
}

渡ってきたデータをもとに、DBからあいまい検索でデータを取得しています。取得したデータを名前だけの配列に変更し、Nuxt側に返却しています。

最後に

実装前は面倒くさそうな処理だなと思ってたが、意外と簡単にいけた。さすがNuxt.jsだと思った。
画面収録_2022-05-17_2_17_40_AdobeCreativeCloudExpress.gif

画面収録_2022-05-17_2_24_49_AdobeCreativeCloudExpress.gif

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
2
Help us understand the problem. What are the problem?