Vue.jsで非同期検索の実装。

# はじめに
オリジナルアプリを作成中、ストックしたレシピの検索を非同期で実装したいと思い挑戦。
その中でいくつか躓いた点があったため記録として残します。

以下が最終的な形です。
完成の動きは`Qiitaのストック一覧の検索`を目指しました。
![c1ed4252c1d1d32a04bad4dd96342256.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1379469/91d8af15-0643-208f-1645-a738cf1507ad.gif)

# もくじ
[1. 検索フォーム設置](#1-検索フォーム設置)
[2. リレーション関係であるデータの取得](#2-リレーション関係であるデータの取得)
[3. 非同期検索機能の実装](#3-非同期検索機能の実装)
[4. aタグのリンク](#4-aタグのリンク)
[5. 日付のformatの表示方法](#5-日付のformatの表示方法)
[6. レンダリングの並び順を変える](#6-レンダリングの並び順を変える)


# 条件

```
macOS: "11.2.3 Big Sur"
Laravel Framework: "6.20.27"
PHP: "8.0.7"
nginx: "1.18"
mysql: "8.0"
vue: "2.6.14"
```

## 使用するファイル
`note.blade.php`　ストック一覧のページ
`AsyncSearch.vue`　非同期の検索処理

# 1. 検索フォーム設置 
検索に使用するフォームを作成します。

```vue:AsyncSearch.vue
<template>
  <div>
    <input type="text" class="search_note" v-model="keyword" placeholder="ノート内を検索">
  </div>

<!-- 一部省略 -->
</template>

<script>
  data() {
    return {
      keyword: '',
    }
  },
</script>
```

# 2. リレーション関係であるデータの取得

blade側からストックしたレシピのデータを`AsyncSearch.vue`へ渡します。

```php:note.blade.php
<async-search
  :stock-recipes = '@json($recipes)' // ストックされたレシピ情報が格納
> 
</async-search>
```

vue側では、`blade`から送られてきた、「ストックされている全レシピの情報の配列データ（`$recipes`）」を`v-for`で１つずつ表示する処理を記述します。

```vue:AsyncSearch.vue
<template>
<!-- 一部省略 -->

  <div class="post-recipe-card" v-for="recipe in stockRecipes" :key="recipe.id">
    <h3 class="card-title"> {{ recipe.title }} </h3>
  </div>

<!-- 一部省略 -->
</template>


<script>
  export default {
  props: {
    StockRecipes: {
      type: Array,
      default: [],
    },
  },
  data() {
    return {
      keyword: '',
      stockRecipes: this.StockRecipes,
    }
  },
</script>
```

例えば、`レシピのタイトル`を表示させたい場合は、以下のように記述することで表示することができます。

```vue:AsyncSearch.vue
{{ recipe.title }}
```

ただ、レシピに紐づく`作成者の名前`や`タグ`なども上記のようなノリで書こうとしたら以下のエラーが...。

```vue:AsyncSearch.vue
<!-- レシピの作成者の名前を表示させたい -->
{{ recipe.user.name }}さん

<!-- Console上で以下のエラー -->
[Vue warn]: Error in render: "TypeError: Cannot read property 'name' of undefined"
```

どうしたものか...となった時に、同じ境遇の方がいました。

https://teratail.com/questions/247618

ということで、Controllerでデータの取得の記述を少し修正。
N+1問題をすでに解消しているならば、ここは問題ないかなと思います。

```php:Controller
$recipes = $user->note->sortByDesc('created_at')
->load('user', 'stocks', 'tags', 'mealType', 'mealClass');
```

これでリレーション関係であるデータの表示も実装完了！

# 3. 非同期検索機能の実装

メインの実装！
`computed`に非同期検索の処理を記述します。

```vue:AsyncSearch.vue
<script>
  computed: {
    // フォームに入力された文字に該当するデータの表示
    filterRecipes() {
      let filtered = [];
      for (let i in this.stockRecipes) {
          let recipe = this.stockRecipes[i];
          if (
             recipe.title.indexOf(this.keyword) !== -1 ||
             recipe.meal_type.name.indexOf(this.keyword) !== -1 ||
             recipe.meal_class.name.indexOf(this.keyword) !== -1
           ) {
              filtered.push(recipe);
          }
        }
      return filtered;
    },
  },
</script>
```

`if`文で、各データに検索対象の文字が含まれていないかをチェックしています。
（以下は、`title`、`meal_type.name`、`meal_class.name`をチェックの対象としています。）

```vue
<script>
  if (
    recipe.title.indexOf(this.keyword) !== -1 ||
    recipe.meal_type.name.indexOf(this.keyword) !== -1 ||
    recipe.meal_class.name.indexOf(this.keyword) !== -1
  ) 
</script>
```

[indexOfについて](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String/indexOf)

非同期検索の記述は以下を参考にしました。

https://blog.capilano-fw.com/?p=648



# 4. aタグのリンク

`レシピのタイトルまたは画像をクリック　→　そのレシピの詳細画面に遷移`
という実装がしたく記述方法に少し悩んでいたところ、またまた同じ境遇の方がいたためこちらを参考にしました。

https://teratail.com/questions/239090

・　URLをvue.js側で定義する。
・　Bladeでは、　`{{ route('') }}` で記述していたが、Vueではhttpを直接記述。
・　URL中にある`id`など、動的に変化するものは`${}`で囲む。

```vue:AsyncSearch.vue
<a :href="`http://localhost/recipes/${recipe.id}`">{{ recipe.title }}</a>
```

これで画面遷移の実装は完了！


# 5. 日付のformatの表示方法

## 現状
`{{ recipe.created_at }}`と記述しているため、以下のような日付の表記になっています。
![スクリーンショット 2021-07-10 16.04.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1379469/33353df3-99e5-8ccc-f7c7-0ca8408cd832.png)
このままではどこか味気ないので、以下のように年月日表記にしたいところ...。
![スクリーンショット 2021-07-10 16.28.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1379469/385d240e-486d-2564-e1e3-a57cc72307cf.png)


`blade`では以下の記述で日付フォーマットを変更していましたが、`vue`ではこの記述はできません。

```php:blade
{{ $recipe->created_at->format('Y年m月d日') }}に投稿
```

探してみると、どうやら`vue-moment`というライブラリで変更できるそう。

https://www.kabanoki.net/5949/

https://www.npmjs.com/package/vue-moment

早速、`vue-moment`をインストール！

```terminal
npm install vue-moment
```

Vue.jsにインポート

```vue:AsyncSearch.vue
<script>
// vue-momentをインポート
import moment from "moment"

// 省略

</script>
```

ただ、これだけではフォーマットの変更はできないため以下のように
`{{ recipe.created_at | createdDate }}`
と記述することで、`filters`に送られてきたデータを指定した日付のformatに変換して表示することができます。

```vue:AsyncSearch.vue
<template>

{{ recipe.created_at | createdDate }}に投稿

// 省略

</template>


<script>
import moment from "moment"

  // 日付のフォーマットを変更
  filters: {
    createdDate: function (date) {
      return moment(date).format("YYYY年MM月DD日");
    }
  },

// 省略

</script>
```

`filters`については以下を参考にしました。

https://v3.ja.vuejs.org/guide/migration/filters.html#%E6%A6%82%E8%A6%81


https://qiita.com/torikatsuPG/items/299b4bbc2771d222f260


# 6. レンダリングの並び順を変える
## 現状
`v-for`で表示されるものは昇順で表示されるため、最新のストックしたレシピが下に下に表示されるようになっています。
降順にしたい場合、`computed`で順番を入れ替えたデータを渡してあげることで解決できます。

```vue:AsyncSearch.vue
<script>
// 一部省略

computed: {
  reverseRecipes() {
      let filtered = [];
      for (let i in this.stockRecipes) {
          let recipe = this.stockRecipes[i];
          if (recipe.title.indexOf(this.keyword) !== -1 ||
          recipe.meal_type.name.indexOf(this.keyword) !== -1 ||
          recipe.meal_class.name.indexOf(this.keyword) !== -1) {
              filtered.push(recipe);
          }
        }
      return filtered.slice().reverse(); // 変更
  },
},
</script>
```

# おわり
Vue.jsの基本的なところで躓いたところが多かったですが、Vue.jsの記述やデータの渡し方などかなり勉強になりました。
まだまだ記述は学習するところが多いので引き続き学習を続けます！

# 【参考】

実装にあたり以下のサイトも参考にさせて頂きました。

https://hatoblog.net/first-vuejs/#Vuejs-4

https://www.codegrid.net/articles/2017-light-vue-1/

https://jp.vuejs.org/v2/guide/forms.html

https://www.monster-dive.com/blog/web_creative/20181019_001812.php