Help us understand the problem. What is going on with this article?

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

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

今回作成したもの

(0).gif

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

前提

・Node.jsはインストール済(バージョンは現時点でv11.12.0)
・Vue.js/Nuxt.jsはある程度理解している
(私はVue.js & Nuxt.js超入門で勉強していました。また、こちらの記事も参考にしていただければと思います)

作成の手順

(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

(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

(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を開いてください。
こちらの記事でクロスオリジン問題について少し書いてます)

実装の流れ

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

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

CiNii Books 図書・雑誌検索OpenSearch

CiNii Booksは、全国の大学図書館等が所蔵する本(図書や雑誌等)の情報を検索できるサービスです。
登録不要で使用することができます。

今回は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

今回のアプリでは、配列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メソッドでは、データが取得できたら
取得データの中の配列itemslistに追加・ページ番号を1つ増やしていきます。

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

(5).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

spiral spiral.gif

circles circles.gif

bubbles bubbles.gif

waveDots waveDots.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と検索すると----- 検索結果はありません-----と表示されると思います。

おわりに

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

参考

vue-infinite-loading
Nuxt.jsとvue-infinite-loadingを使って無限スクロールを実装する
↑今回初めてvue-infinite-loadingを勉強するにあたって、こちらの記事がとてもわかりやすかったです。
CiNii Books - メタデータ・API - CiNii Books 図書・雑誌検索のOpenSearch
Vue.js & Nuxt.js超入門
【Nuxt.js】todoアプリを作成してみた①
【Nuxt.js】todoアプリを作成してみた②

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away