【Nuxt.js】本検索アプリで学ぶvue-infinite-loading

業務で携わっている案件でvue-infinite-loadingが使われていたので、自分でも実装できるように勉強しました。

# 今回作成したもの
![(0).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/388011/6b5023ca-5177-e481-dc84-82949077a425.gif)

検索したいワードを入れてボタンを押すと、
ヒットした図書・雑誌のタイトルと著者名を表示する簡単なアプリです。

## 前提
・Node.jsはインストール済（バージョンは現時点でv11.12.0）
・Vue.js/Nuxt.jsはある程度理解している
（私は<a href="https://www.amazon.co.jp/Vue-js-Nuxt-js%E8%B6%85%E5%85%A5%E9%96%80-%E6%8E%8C%E7%94%B0%E6%B4%A5%E8%80%B6%E4%B9%83/dp/4798056596">Vue.js & Nuxt.js超入門</a>で勉強していました。また、<a href="https://qiita.com/ayapon/items/d93807e7699434279531">こちらの記事</a>も参考にしていただければと思います）


# 作成の手順
(1)Nuxt.jsのプロジェクトを作成する
(2)vue-infinite-loadingをインストールする
(3)templateを作成する
(4)vue-infinite-loadingを使わずに検索結果を表示してみる
(5)vue-infinite-loadingを使って無限スクロールにする
(6)ローディング時のspinnerを変更してみる
(7)読み込むデータがない場合の表示を変更してみる


## (1)Nuxt.jsのプロジェクトを作成する
プロジェクトを作成したいディレクトリに移動し、以下のコマンドを実行します。
私はDesktopのpracticeというフォルダに`sample-vue-infinite-loading-app`という名前のプロジェクトを作成しました。

```ターミナル
$ cd Desktop/practice
$ npx create-nuxt-app sample-vue-infinite-loading-app
```

ProjectNameなど確認する表示が出てきますが、
`Choose features to install`以外は初期設定のままEnterを押していって大丈夫です。
`Choose features to install`については`Axios`を選択してEnterを押してください。

installが完了したら、以下のように作成したプロジェクトに移動して実行します。

```ターミナル
$ cd sample-vue-infinite-loading-app
$ npm run dev
```

localhost:3000にアクセスして、以下の画面になっていればプロジェクトの作成は完了です。
![スクリーンショット 2019-04-25 11.02.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/388011/80c78897-97ac-4030-9944-47cd63d50a0e.png)


## (2)vue-infinite-loadingをインストールする
`Ctrl + C`で一旦実行しているプロジェクトを終了させて、
以下のコマンドを実行して`vue-infinite-loading`をインストールします。

```ターミナル
$ npm install vue-infinite-loading -S
```

インストールが完了したら、`npm run dev`コマンドで再度プロジェクトを起動します。


## (3)templateを作成する
作成したプロジェクトのpagesフォルダのindex.vueファイルを開いて、template部分を作成します。

```pages/index.vue
<template>
  <section class="container">
    <div class="input-area">
      <input type="text" />
      <button>検索</button>
    </div>
    <div class="result-display-area">
      <p>---ここに検索結果を表示---</p>
    </div>
  </section>
</template>

<script>
</script>
```

こんな感じです。
![スクリーンショット 2019-04-25 11.40.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/388011/7044a4de-7764-f69a-d429-8fecc5a2c6fb.png)


## (4)vue-infinite-loadingを使わずに検索結果を表示してみる
まずはvue-infinite-loadingは使わず、単純に検索結果を表示してみます。

```pages/index.vue
<template>
  <section class="container">
    <div class="input-area">
      <p>{{message}}</p>
      <input type="text" v-model="inputWord" @focus="focus"/>
      <button @click="getData">検索</button>
    </div>
    <div class="result-display-area">
      <div v-for="(item, $index) in list" :key="$index">
        {{item.title}} ( {{item['dc:creator']}} )
      </div>
    </div>
  </section>
</template>

<script>
import axios from 'axios';

let url = 'https://ci.nii.ac.jp/books/opensearch/search?title=';

export default {
  data() {
    return {
      inputWord: '',
      page: 1,
      list: [],
      message: ''
    }
  },
  methods: {
    getData() {
      if(!this.inputWord) {
        this.message = 'Please input some word.'
        return;
      }
      axios.get((url + this.inputWord), {
        params: {
          p: this.page,
          format: 'json'
        }
      }).then(({data}) => {
        const items = data['@graph'][0]['items'];
        this.list.push(...items)
      }).catch((error) => {
        this.message = 'error'
      })
    },
    focus() {
      this.inputWord = '';
      this.page = 1;
      this.list = [];
      this.message = '';
    }
  }
}
</script>
```

### エラーが出た場合
もし`Module not found`のエラーが出たら、ターミナルで`$npm install axios`コマンドを実行して、
再度`$npm run dev`してみてください。

また、`Access to XMLHttpRequest at〜`のようなエラーが出たら、一旦Chromeを終了して、
`$open /Applications/Google\ Chrome.app/ --args --disable-web-security --user-data-dir`コマンドでChromeを開いてください。
（<a href="https://qiita.com/ayapon/items/91c00b2ef40cd12f4e0a">こちらの記事</a>でクロスオリジン問題について少し書いてます）

### 実装の流れ
dataプロパティに`inputWord`を設定して、入力されたワードとバインドし
ボタンが押されたら`getData`メソッドを呼び出して、検索結果を`list`に追加します。
検索結果は`result-display-area`の部分で`本のタイトル`と`著者名`を表示します。

また、検索結果が表示されている状態で入力欄をクリックした場合は、
`focus`メソッドでdataプロパティを初期化しています。

### CiNii Books 図書・雑誌検索OpenSearch
<a href="https://support.nii.ac.jp/ja/cib/cinii_books">CiNii Books</a>は、全国の大学図書館等が所蔵する本（図書や雑誌等）の情報を検索できるサービスです。
登録不要で使用することができます。

今回はCiNii BooksのOpenSearchを使用しています。OpenSearchは、
`https://ci.nii.ac.jp/books/opensearch/search?title=検索したいワード&format=json`
にアクセスすると検索結果がjson形式で取得できます。
また、`p=数字`と付け加えるとページ番号も指定できます。
例えば`https://ci.nii.ac.jp/books/opensearch/search?title=vue.js&format=json&p=1`
にアクセスすると、`vue.js`というワードで検索した結果がこのように表示されます。
![スクリーンショット 2019-04-25 13.30.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/388011/0fe64b36-7dd9-3dda-5da7-2c6e61022461.png)

今回のアプリでは、配列`items`をdataプロパティの`list`に追加し、`title` `dc:creator`を検索結果として表示しています。


## (5)vue-infinite-loadingを使って無限スクロールにする
vue-infinite-loadingを使って、検索結果をページ毎に表示していきます。

```pages/index.vue
<template>
  <section class="container">
    <div class="input-area">
      <p>{{message}}</p>
      <input type="text" v-model="inputWord" @focus="focus"/>
      <button @click="changeButtonStatus">検索</button>
    </div>
    <div class="result-display-area">
      <div v-for="(item, $index) in list" :key="$index">
        {{item.title}} ( {{item['dc:creator']}} )
      </div>
    </div>
    <infinite-loading @infinite="infiniteHandler" v-if="buttonStatus"></infinite-loading>
  </section>
</template>

<script>
import axios from 'axios';
import InfiniteLoading from 'vue-infinite-loading';

let url = 'https://ci.nii.ac.jp/books/opensearch/search?title=';

export default {
  components: {
    InfiniteLoading
  },
  data() {
    return {
      inputWord: '',
      buttonStatus: false,
      page: 1,
      list: [],
      message: ''
    }
  },
  methods: {
    changeButtonStatus() {
      if(!this.inputWord) {
        this.message = 'Please input some word.'
        return;
      }
      this.buttonStatus = true
    },
    infiniteHandler($state) {
      axios.get((url + this.inputWord), {
        params: {
          p: this.page,
          format: 'json'
        }
      }).then(({data}) => {
        const items = data['@graph'][0]['items'];
        if(items) {
            this.page += 1;
            this.list.push(...items)
            $state.loaded();
          } else {
            $state.complete();
          }
      }).catch((error) => {
        this.message = 'error'
      })
    },
    focus() {
      this.inputWord = '';
      this.buttonStatus = false,
      this.page = 1;
      this.list = [];
      this.message = '';
    }
  }
}
</script>
```

### 実装の流れ
(4)ではボタンを押したときに`getData`メソッドを呼び出していましたが、ボタンを押したらまず
`changeButtonState`メソッドを呼び出して、dataプロパティの`buttonStatus`をfalse→trueに変更します。
そして、`buttonStatus`がtrue（ボタンが押された）の場合に`infiniteHandler`メソッドを呼び出し、
データを取得していきます。
`infiniteHandler`メソッドでは、データが取得できたら
取得データの中の配列`items`を`list`に追加・ページ番号を1つ増やしていきます。

これでvue-infinite-loadingを使って無限ローディングにすることができました。

![(5).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/388011/5ac17760-5769-0984-cff7-df6b9f9774aa.gif)


## (6)ローディング時のspinnerを変更してみる
vue-infinite-loadingでは、spinner（ローディングしているときのくるくる）を変更することができます。
spinnerがわかりやすいように、`setTimeOut`メソッドを使って1秒毎にデータを読み込むようにしています。

```pages/index.vue
<template>
  <section class="container">
    <div class="input-area">
      <p>{{message}}</p>
      <input type="text" v-model="inputWord" @focus="focus"/>
      <button @click="changeButtonStatus">検索</button>
    </div>
    <div class="result-display-area">
      <div v-for="(item, $index) in list" :key="$index">
        {{item.title}} ( {{item['dc:creator']}} )
      </div>
    </div>
    <infinite-loading spinner="spiral" @infinite="infiniteHandler" v-if="buttonStatus"></infinite-loading>
  </section>
</template>

<script>
import axios from 'axios';
import InfiniteLoading from 'vue-infinite-loading';

let url = 'https://ci.nii.ac.jp/books/opensearch/search?title=';

export default {
  components: {
    InfiniteLoading
  },
  data() {
    return {
      inputWord: '',
      buttonStatus: false,
      page: 1,
      list: [],
      message: ''
    }
  },
  methods: {
    changeButtonStatus() {
      if(!this.inputWord) {
        this.message = 'Please input some word.'
        return;
      }
      this.buttonStatus = true
    },
    infiniteHandler($state) {
      axios.get((url + this.inputWord), {
        params: {
          p: this.page,
          format: 'json'
        }
      }).then(({data}) => {
        const items = data['@graph'][0]['items'];
        setTimeout(() => {
          if(items) {
            this.page += 1;
            this.list.push(...items)
            $state.loaded();
          } else {
            $state.complete();
          }
        }, 1000)
      }).catch((error) => {
        this.message = 'error'
      })
    },
    focus() {
      this.inputWord = '';
      this.buttonStatus = false,
      this.page = 1;
      this.list = [];
      this.message = '';
    }
  }
}
</script>
```

`infinite-loading`タグの中に、`spinner="spinnerの種類"`を加えることで変更できます。
spinnerの種類は以下の通りです。

***default***　![default.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/388011/a5385fd8-7c7f-de5f-c0af-61aaf66fcaf4.gif)

***spiral***　![spiral.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/388011/dd8bab60-8381-c94b-aa74-5982f1bcbdb2.gif)

***circles***　![circles.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/388011/fd91d179-7853-988e-e9bb-42c2afa8be79.gif)

***bubbles***　![bubbles.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/388011/11cae147-aa6c-4969-0f15-a1e4144aaebc.gif)

***waveDots***　![waveDots.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/388011/5c6022ba-961d-eec1-6770-5f28104b0c68.gif)


## (7)読み込むデータがない場合の表示を変更してみる
読み込むデータがこれ以上ない場合は`No more data :)`、検索結果がなかった場合は`No results :(`と
defaultで表示されますが、`slot`を指定して変更することができます。

```pages/index.vue
<template>
  <section class="container">
    <div class="input-area">
      <p>{{message}}</p>
      <input type="text" v-model="inputWord" @focus="focus"/>
      <button @click="changeButtonStatus">検索</button>
    </div>
    <div class="result-display-area">
      <div v-for="(item, $index) in list" :key="$index">
        {{item.title}} ( {{item['dc:creator']}} )
      </div>
    </div>
    <infinite-loading spinner="spiral" @infinite="infiniteHandler" v-if="buttonStatus">
      <span slot="no-more">----- 検索結果は以上です-----</span>
　　　 <span slot="no-results">----- 検索結果はありません-----</span>
    </infinite-loading>
  </section>
</template>

<script>
import axios from 'axios';
import InfiniteLoading from 'vue-infinite-loading';

let url = 'https://ci.nii.ac.jp/books/opensearch/search?title=';

export default {
  components: {
    InfiniteLoading
  },
  data() {
    return {
      inputWord: '',
      buttonStatus: false,
      page: 1,
      list: [],
      message: ''
    }
  },
  methods: {
    changeButtonStatus() {
      if(!this.inputWord) {
        this.message = 'Please input some word.'
        return;
      }
      this.buttonStatus = true
    },
    infiniteHandler($state) {
      axios.get((url + this.inputWord), {
        params: {
          p: this.page,
          format: 'json'
        }
      }).then(({data}) => {
        const items = data['@graph'][0]['items'];
        setTimeout(() => {
          if(items) {
            this.page += 1;
            this.list.push(...items)
            $state.loaded();
          } else {
            $state.complete();
          }
        }, 1000)
      }).catch((error) => {
        this.message = 'error'
      })
    },
    focus() {
      this.inputWord = '';
      this.buttonStatus = false,
      this.page = 1;
      this.list = [];
      this.message = '';
    }
  }
}
</script>
```

`nuxt.js`と検索すると3件のデータのあとに`----- 検索結果は以上です-----`、
`aaaaaaaaaaaaa`と検索すると`----- 検索結果はありません-----`と表示されると思います。


# おわりに
引き続き勉強がんばりたいです。
もし間違った記載などあればご指摘いただけると幸いです。

# 参考
・<a href="https://peachscript.github.io/vue-infinite-loading/">vue-infinite-loading</a>
・<a href="https://qiita.com/yakiniku0220/items/1b2e960a09a9f085820e">Nuxt.jsとvue-infinite-loadingを使って無限スクロールを実装する</a>
↑今回初めてvue-infinite-loadingを勉強するにあたって、こちらの記事がとてもわかりやすかったです。
・<a href="https://support.nii.ac.jp/ja/cib/api/b_opensearch">CiNii Books - メタデータ・API - CiNii Books 図書・雑誌検索のOpenSearch</a>
・<a href="https://www.amazon.co.jp/Vue-js-Nuxt-js%E8%B6%85%E5%85%A5%E9%96%80-%E6%8E%8C%E7%94%B0%E6%B4%A5%E8%80%B6%E4%B9%83/dp/4798056596">Vue.js & Nuxt.js超入門</a>
・<a href="https://qiita.com/ayapon/items/d93807e7699434279531">【Nuxt.js】todoアプリを作成してみた①</a>
・<a href="https://qiita.com/ayapon/items/31b44a43cac73f29f811">【Nuxt.js】todoアプリを作成してみた②</a>