検索ボックスに入力した文字で、リストをその場で絞り込む機能は、Vueのcomputedを使うと簡潔に作れます。ただ、日本語のサイトでは、基本の書き方のままだと、検索にうまく引っかからなかったり、入力中に絞り込まれなかったりします。この記事では、Vue 3(<script setup>)を前提に、基本の作り方と、日本語向けの工夫を整理します。
基本の実装
入力値をrefに持ち、computedで絞り込んだ配列を作ります。各項目には、v-forのkeyのために、固有のidを持たせておきます。
<script setup>
import { ref, computed } from 'vue'
const keyword = ref('')
const items = ref([
{ id: 1, name: '山田太郎' },
{ id: 2, name: '田中花子' },
{ id: 3, name: '佐藤健' },
{ id: 4, name: '鈴木一郎' },
{ id: 5, name: '高橋愛' }
])
const filteredList = computed(() => {
const kw = keyword.value.trim().toLowerCase()
if (!kw) return items.value
return items.value.filter((item) => item.name.toLowerCase().includes(kw))
})
</script>
<template>
<input v-model="keyword" type="text" placeholder="名前で検索...">
<ul v-if="filteredList.length">
<li v-for="item in filteredList" :key="item.id">{{ item.name }}</li>
</ul>
<p v-else>該当する項目がありません。</p>
</template>
これで動きますが、日本語の検索では、次の3つの問題があります。
- 入力中(変換中)に絞り込まれない
- 全角と半角、ひらがなとカタカナの違いで、検索に引っかからない
- 検索ボックスにラベルがなく、結果の件数も読み上げられない
問題1:日本語の変換中は絞り込まれない
公式が注意しているとおり、IMEで入力する言語では、v-modelは変換中には更新されません。「やまだ」と打っている間は、リストが変わらず、変換を確定した時点で、初めて反映されます。変換中にも絞り込みたい場合は、v-modelを使わず、:valueと@inputで自前でバインドします。v-modelの仕組みはv-modelの仕組みを完全理解、フォームでの使い方は双方向データバインディングでフォームの入力値を反映させる方法をご覧ください。
問題2:全角・半角、ひらがな・カタカナの違いを吸収する
toLowerCase()で揃えられるのは、英字の大文字と小文字だけです。日本語のリストでは、全角と半角(ABCとABC、ヤマダとヤマダ)、ひらがなとカタカナ(やまだとヤマダ)の違いも吸収したいところです。次の関数で、検索用に文字列を揃えます。
// utils/normalize.js
// 検索用に、文字列を揃える
export function normalize(text) {
return text
.normalize('NFKC') // 全角英数→半角、半角カナ→全角カナ、全角スペース→半角
.toLowerCase() // 英字の大文字・小文字
.replace(/[ァ-ヶ]/g, // カタカナ→ひらがな
(ch) => String.fromCharCode(ch.charCodeAt(0) - 0x60))
}
// normalize('ABC アイウ カタカナ') → 'abc あいう かたかな'
// normalize('ヤマダ') と normalize('ヤマダ') → どちらも 'やまだ'
normalize('NFKC')は、全角の英数字を半角に、半角のカタカナを全角に、全角スペースを半角スペースにそろえます。- カタカナのコードポイントから
0x60を引くと、対応するひらがなになります。
注意点として、検索する側(入力値)だけでなく、リスト側の文字列にも、同じ正規化をかける必要があります。漢字の読みを引っかけたい場合は、読み仮名(kana)の項目を別に持たせ、名前と読みを合わせて検索対象にします。
完成コード
上の工夫をまとめた完成版です。ポイントは次のとおりです。
- 検索用のテキストを、
computedで先に1度だけ作っておき、入力のたびに正規化し直さない。 - スペースで区切った、すべての語を含む項目に絞り込む(AND検索)。
<label>とtype="search"、件数のrole="status"を付ける。- 0件のときは、次に何をすればよいかを示す。
<script setup>
import { ref, computed } from 'vue'
import { normalize } from './utils/normalize'
const keyword = ref('')
const items = ref([
{ id: 1, name: '山田太郎', kana: 'やまだたろう' },
{ id: 2, name: '田中花子', kana: 'たなかはなこ' },
{ id: 3, name: '佐藤健', kana: 'さとうけん' },
{ id: 4, name: '鈴木一郎', kana: 'すずきいちろう' },
{ id: 5, name: '高橋愛', kana: 'たかはしあい' }
])
// 検索用のテキストを、先に1度だけ作っておく(入力のたびに作り直さない)
const searchable = computed(() =>
items.value.map((item) => ({
...item,
text: normalize(`${item.name} ${item.kana}`)
}))
)
const filteredList = computed(() => {
// スペースで区切った、すべての語を含むものに絞り込む(AND検索)
const words = normalize(keyword.value).split(/\s+/).filter(Boolean)
if (words.length === 0) return searchable.value
return searchable.value.filter((item) => words.every((word) => item.text.includes(word)))
})
</script>
<template>
<label for="search">名前で検索</label>
<!-- v-model ではなく :value + @input:日本語の変換中も絞り込む -->
<input
id="search"
type="search"
:value="keyword"
placeholder="例:やまだ、田中 花子"
@input="keyword = $event.target.value"
>
<!-- 件数を、読み上げソフトにも伝える -->
<p role="status">{{ filteredList.length }}件</p>
<ul v-if="filteredList.length">
<li v-for="item in filteredList" :key="item.id">
{{ item.name }}({{ item.kana }})
</li>
</ul>
<p v-else>該当する項目がありません。別のキーワードをお試しください。</p>
</template>
computedの使い方はcomputedプロパティで動的なデータ処理を行う方法、keyにidを使う理由はv-forで配列やオブジェクトをループして表示する方法をご覧ください。
データが多いとき:デバウンスとサーバー側の検索
数千件を超えるデータでは、キーを押すたびに、全件を走査するのは重くなります。入力が止まってから絞り込む(デバウンス)と、無駄な計算を減らせます。
<script setup>
import { ref, watch } from 'vue'
const keyword = ref('') // 入力欄の値
const debounced = ref('') // 絞り込みに使う値(少し遅れて更新される)
let timer
watch(keyword, (value) => {
clearTimeout(timer)
timer = setTimeout(() => {
debounced.value = value
}, 300)
})
// filteredList の computed では、keyword の代わりに debounced を使う
</script>
データがさらに多い場合や、一部しか手元にない場合は、サーバー側で検索するのが現実的です。通信を伴う検索は、古い応答が新しい結果を上書きしないよう注意が必要です。詳しくは非同期APIの状態管理パターンをご覧ください。結果が多いときは、ページネーション付きリスト表示を実装する方法と組み合わせます。
よくある質問(FAQ)
v-modelが更新されないためです。変換中も絞り込むなら、v-modelの代わりに、:valueと@inputで自前でバインドしてください。0x60を引くと、ひらがなになります。全角・半角は、normalize('NFKC')で揃えられます。idを持たせて、keyにしてください。まとめ
検索フィルターは、computedで絞り込むだけなら、数行で作れます。日本語のサイトでは、変換中も絞り込むために:valueと@inputを使い、検索語とデータの両方を、全角・半角やひらがな・カタカナで揃えると、実用的になります。ラベルと件数の読み上げ、データが多いときのデバウンスにも気を配ってください。

