フロントエンドでテーブルに検索機能を実装する

:christmas_tree: Happy Holiday :santa:
これはROXXアドベントカレンダー25日目の投稿です。(12時過ぎましたがクリスマス気分のうちはまだクリスマス!)

業務ではサーバー (Laravel製) に対して、 フロントエンド (Nuxt.js製) からリクエストを送る構成になっています。
データ内の検索機能などは基本的にはサーバー側で処理することが多いのですが、最近フロントエンドで同機能を実装する機会がありました
フロントエンドだけで処理を行うため、処理速度は速くなり、 API へのアクセスも削減することができます🚀
この記事ではその実装方法について書きます。

## 実装ページ (CodePen)
https://codepen.io/shohei-japan/pen/rNaweRb

## 実装概要
- Vue.js
- 使用するAPI: [PokéAPI](https://pokeapi.co/)
    - ポケモン情報が取れる API
- ページ作成時に API リクエストを行う
- 検索をフロントエンドで処理する

### 検索機能詳細
- テキストに含まれる文字列で検索
- 検索文字列をスペースで分けることで、 AND 検索 (もしくは OR 検索) が可能

## コード説明

### Template

template はこんな感じで書きました。
取得したデータのうち、モンスターの名前だけをリストにします。

```html:index.html
<div id="app">
  <input
    v-model="searchText"
    type="text"
    placeholder="Enter text"
  >
  <div
    v-for="(monster, key) in monstersList"
    :key="key"
  >
    <p>{{ monster.name }}</p>
  </div>
</div>
```

### Script - データ取得

axios で API からデータを取得します。

data 内の `monsters` に取得したデータを代入します。オブジェクトで返ってくるので `Object.freeze()` で凍結させてもいいかもしれません。

`searchText` には検索キーワードが入ります。実際の処理はこのあと書きます。

```js:main.js
new Vue({
  el: '#app',
  data:() => ({
    monsters: null,
    searchText: '',
    url: 'https://pokeapi.co/api/v2/berry?limit=200'
  }),
  async created() {
    const res = await axios.get(this.url)
    this.monsters = res.data.results
  }
});
```

### Script - 検索キーワードの処理

このあとモンスター名と照合させるために、検索キーワード内の文字列を先に処理しましょう。

例えば `leppa` を検索する際、 `le ppa` というように文字列にスペースが入ったときに、 AND 検索か OR 検索を行えるのは自然かと思います。
そのための方法として、ここでは 1 文字ずつ配列の要素に入れる実装にします。

```js:main.js
computed: {
  splittedSearchText() {
    return this.searchText.split(/[\s]+/)
  }
}
```

こうすることで先ほどの `le ppa` という文字列を `["le", "ppa"]` という配列に置き換えます。

### Script - 検索実行

検索機能とは、入力された文字列と同じ文字列を含むモンスター名を抽出することです。

まずは `入力された文字列と同じ文字列を含む` という処理を書きます。

monsters の配列に対して、条件を満たすかどうかというメソッドを加えます。
AND 検索の場合は [Array.every](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/every) を、 OR 検索の場合は [Array.some](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/some) を使うことでこれを実現します。
サンプルコードでは AND 検索を実装します。

モンスター名の文字列中に、検索キーワード (splittedSearchText) が含まれるかを見ていくので、モンスターを 1 つずつ関数に渡し、照合して返すメソッドを書きます。

```js:main.js
methods: {
  filteredMonsters(monster) {
    if (this.splittedSearchText.every(el =>
        monster.name.includes(el)
      )
    ) {
      return true
    }
  }
}
```

上記のメソッドを呼び出しましょう。
computed に記述し、テキストが入力されたと同時に処理が走るようにします。

```js:main.js
// computed に追加
monstersList() {
  if (!this.monsters) { // モンスターが存在しないときは空配列を返す
    return []
  }
  if (!this.searchText.length) { // 検索文字列が存在しないときは monsters をそのまま返す
    return this.monsters
  }
  return this.monsters.filter(monster => {
    return this.filteredMonsters(monster)
  })
}
```

## 完成 + おまけ

いささか簡単なコードですが、フロントエンドで検索機能を実装することができました 🎉

![pokemon.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230058/28134e09-6e6c-757d-b961-8a0dff0b34eb.gif)

喜びたいところなんですが、同じ検索機能を日本語の文字列で行う際には少し注意が必要です。アルファベットを入力中にも検索が走ってしまうので、リストのモンスターが増えたり減ったりとても見づらい挙動を起こします。
そんなときは、 [compositionstart](https://developer.mozilla.org/ja/docs/Web/Events/compositionstart) と [compositionend](https://developer.mozilla.org/ja/docs/Web/Events/compositionend) を使って検索するタイミングを調整しましょう。


```html:index.html
<div id="app">
  <input
    v-model="searchText"
    type="text"
    placeholder="Enter text"
    @compositionstart="isComposing = true"
    @compositionend="isComposing = false"
  >
  <div
    v-for="(monster, key) in monstersList"
    :key="key"
  >
    <p>{{ monster.name }}</p>
  </div>
</div>
```

```js:main.js
data: () => ({
  isComposing: false
}),
// computed に追加
monstersList() {
  if (!this.monsters) {
    return []
  }
  if (!this.searchText.length) {
    return this.monsters
  }
  /* 追加コード */
  if (this.isComposing) { // isComposing が true (日本語入力変換中) のとき、 monsters を返す
    return this.monsters
  }
  return this.monsters.filter(monster => {
    return this.filteredMonsters(monster)
  })
}
```

このように記述することで、日本語を変換しているときには `isComposing` が true となるので、処理を走らないよう制御することができました！

フロントエンドでできることがいっぱいあって嬉しいですね。

Happy Programming! 🌟 Happy Holiday! ☃️