Vue.jsでvue-multiselectを使用する方法

今回は、自作したプロ野球の投手VS野手の対戦成績検索画面を例に、Vue.jsでのvue-multiselectの導入方法と活用法を紹介します。

完成したアプリは以下のリンクからご覧いただけます。

[プロ野球｜投手 vs 野手 対戦成績検索](https://baseball-pitcher-vs-batter.com/)

このアプリでは、投手名や野手名を選択する際にvue-multiselectを使用しています。この記事では、実際の画面も交えながら、vue-multiselectの使い方を詳しく解説します。
## 操作動画

実際にイメージしやすいよう、はじめに操作動画を載せておきます。

動画：投手と野手を選択して対戦成績を検索するデモ
![プルダウン.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3933685/7a70ac40-e2af-1eac-4463-cbd8d2551220.gif)

## 目次

- [vue-multiselectの導入](#vue-multiselectの導入)
  - [インストール](#インストール)
  - [コンポーネントの登録](#コンポーネントの登録)
  - [スタイルのインポート](#スタイルのインポート)
- [実装例とオプションの解説](#実装例とオプションの解説)
  - [投手名と野手名の選択](#投手名と野手名の選択)
  - [主要なオプションの説明](#主要なオプションの説明)
  - [カスタム検索の実装](#カスタム検索の実装)
  - [スロットのカスタマイズ](#スロットのカスタマイズ)
- [その他の便利なオプション](#その他の便利なオプション)
  - [複数選択を可能にする](#複数選択を可能にする)
  - [タグの追加を許可する](#タグの追加を許可する)
  - [オプションのグループ化](#オプションのグループ化)
  - [カスタムテンプレート](#カスタムテンプレート)
  - [リモートデータのロード](#リモートデータのロード)
- [まとめ](#まとめ)
- [参考リンク](#参考リンク)

## vue-multiselectの導入

### インストール

まず、vue-multiselectをプロジェクトにインストールします。

```bash
# npmの場合
npm install vue-multiselect

# yarnの場合
yarn add vue-multiselect
```

### コンポーネントの登録

使用するVueコンポーネントでvue-multiselectをインポートし、登録します。

```javascript
import Multiselect from 'vue-multiselect';

export default {
  components: {
    Multiselect,
  },
  // その他のオプション
};
```

### スタイルのインポート

vue-multiselectのスタイルシートもインポートします。これはコンポーネント内で以下のように記述します。

```html
<style src="vue-multiselect/dist/vue-multiselect.min.css"></style>
```

## 実装例とオプションの解説

### 投手名と野手名の選択

以下は、投手名と野手名を選択するためのmultiselectコンポーネントの実装例です。

```vue
<template>
  <div>
    <!-- 投手名選択 -->
    <multiselect
      v-model="selectPitcherOptions"
      :options="filteredPitcherList"
      label="playerNm"
      track-by="playerId"
      placeholder="選手名を入力で絞り込み可能"
      :select-label="'選択'"
      :deselect-label="'解除'"
      @search-change="updatePitcherSearch"
      :filterable="false"
      :internal-search="false"
    >
      <template slot="noResult">
        <span>対象データがありません</span>
      </template>
    </multiselect>

    <!-- 野手名選択 -->
    <multiselect
      v-model="selectBatterOptions"
      :options="filteredBatterList"
      label="playerNm"
      track-by="playerId"
      placeholder="選手名を入力で絞り込み可能"
      :select-label="'選択'"
      :deselect-label="'解除'"
      @search-change="updateBatterSearch"
      :filterable="false"
      :internal-search="false"
    >
      <template slot="noResult">
        <span>対象データがありません</span>
      </template>
    </multiselect>
  </div>
</template>
```

### 主要なオプションの説明

- `v-model`: 選択された値をバインドします。
- `:options`: プルダウンに表示するオプションリストです。
- `label`: オプションの表示に使用するプロパティ名（ここでは`playerNm`）。
- `track-by`: オプションの一意性を識別するプロパティ名（ここでは`playerId`）。
- `placeholder`: 未選択時に表示されるテキスト。
- `:select-label`: オプション選択時のラベルをカスタマイズ。
- `:deselect-label`: オプション解除時のラベルをカスタマイズ。
- `@search-change`: ユーザーが検索入力を変更した際に発火するイベント。
- `:filterable="false"`: 内部のフィルタリング機能を無効化。
- `:internal-search="false"`: 内部検索を無効にし、カスタム検索を実装。
※今回はひらがな・漢字どちらの検索にも対応させ、尚且つ空白を許容したかったため、既存のフィルタリング機能や内部検索を無効化しました。

### カスタム検索の実装

`@search-change`イベントを使用して、ユーザーの入力に基づいたカスタム検索を実装します。
主な目的は、ユーザーが投手や野手を検索する際に、漢字やひらがな、スペースの有無を気にせずに直感的に入力できるよう工夫しています。


```javascript
export default {
  data() {
    return {
      selectPitcherOptions: null,
      selectBatterOptions: null,
      searchQueryPitcher: '',
      searchQueryBatter: '',
      localPitcherList: [], // 投手リスト
      localBatterList: [],  // 野手リスト
    };
  },
  computed: {
    filteredPitcherList() {
      if (!this.searchQueryPitcher) {
        return this.localPitcherList;
      }
      const query = this.searchQueryPitcher.toLowerCase().replace(/[\s\u3000]/g, '');
      return this.localPitcherList.filter(player => {
        const name = player.playerNm.toLowerCase().replace(/[\s\u3000]/g, '');
        const kana = player.playerNmKana.toLowerCase().replace(/[\s\u3000]/g, '');
        return name.includes(query) || kana.includes(query);
      });
    },
    filteredBatterList() {
      if (!this.searchQueryBatter) {
        return this.localBatterList;
      }
      const query = this.searchQueryBatter.toLowerCase().replace(/[\s\u3000]/g, '');
      return this.localBatterList.filter(player => {
        const name = player.playerNm.toLowerCase().replace(/[\s\u3000]/g, '');
        const kana = player.playerNmKana.toLowerCase().replace(/[\s\u3000]/g, '');
        return name.includes(query) || kana.includes(query);
      });
    },
  },
  methods: {
    updatePitcherSearch(query) {
      this.searchQueryPitcher = query;
    },
    updateBatterSearch(query) {
      this.searchQueryBatter = query;
    },
  },
};
```
#### 具体的な流れ

1.	ユーザー入力: ユーザーが検索ボックスに文字を入力すると、@search-changeイベントが発火し、対応するupdatePitcherSearchまたはupdateBatterSearchメソッドが呼び出され、検索クエリが更新されます。
2.	リストのフィルタリング: 検索クエリが更新されると、filteredPitcherListまたはfilteredBatterListが再評価されます。
3.	正規化と比較:
	•	検索クエリと選手名（漢字・カナ）の両方からスペースを削除します。
	•	正規化された文字列同士でincludesを用いて部分一致を確認します。
4.	結果の表示: フィルタリングされたリストが画面上に表示され、ユーザーは選択肢から目的の選手を選べます。

### スロットのカスタマイズ

データが存在しない場合の表示を`noResult`スロットでカスタマイズしています。

```vue
<template slot="noResult">
  <span>対象データがありません</span>
</template>
```

## その他の便利なオプション

vue-multiselectには他にも便利なオプションが多数あります。以下にいくつか紹介します。

### 複数選択を可能にする

`multiple`オプションを使用すると、複数のオプションを選択できます。

```vue
<multiselect
  v-model="selectedOptions"
  :options="options"
  :multiple="true"
  placeholder="複数のオプションを選択可能"
></multiselect>
```

### タグの追加を許可する

ユーザーが独自のオプションを追加できるようにします。

```vue
<multiselect
  v-model="selectedTags"
  :options="tags"
  :multiple="true"
  :taggable="true"
  placeholder="タグを追加可能"
  @tag="addTag"
></multiselect>

methods: {
  addTag(newTag) {
    this.tags.push(newTag);
    this.selectedTags.push(newTag);
  },
},
```

### オプションのグループ化

オプションをカテゴリー別にグループ化して表示します。

```vue
<multiselect
  v-model="selectedOption"
  :options="groupedOptions"
  :group-values="'items'"
  :group-label="'group'"
  placeholder="グループ化されたオプション"
></multiselect>
```

### カスタムテンプレート

オプションの表示方法をカスタマイズできます。
例）「背番号 - 選手名」などの表示を実現

```vue
<multiselect
  v-model="selectedOption"
  :options="options"
  :custom-label="customLabel"
  placeholder="カスタムテンプレート"
>
  <template slot="option" slot-scope="{ option }">
    <div class="option__image">
      <img :src="option.image" alt="">
    </div>
    <div class="option__info">
      <span class="option__name">{{ option.name }}</span>
      <span class="option__desc">{{ option.description }}</span>
    </div>
  </template>
</multiselect>

methods: {
  customLabel(option) {
    return `${option.name} - ${option.description}`;
  },
},
```

## まとめ

vue-multiselectを活用すると、ユーザーが使いやすい高度なセレクトボックスを簡単に実装できます。自作したプロ野球選手の対戦成績検索フォームを例に、導入方法や主要なオプションの使い方を紹介しました。
※Vue3では使用できない可能性が高いです。

## 参考リンク

- [vue-multiselect公式ドキュメント](https://vue-multiselect.js.org/)
- [プロ野球｜投手 vs 野手 対戦成績検索](https://baseball-pitcher-vs-batter.com/)

以上、vue-multiselectを使った実装例と活用法の紹介でした。ご質問やフィードバックがありましたら、コメント欄でお知らせください。