スマートフォンでの閲覧をターゲットにしたサイト等に良く見られるリストの無限スクロールですが、Vue.js で実装する際は、vue-infinite-loading というライブラリを使うと簡単に実装することが可能です。
ここでは vue-infinite-loading 使って無限スクロールの実装方法を解説していきます。
npm もしくは yarn で vue-infinite-loading をインストールします。
vue-infinite-loading (opens new window)
npm install vue-infinite-loading # or yarn add vue-infinite-loading
まず、無限スクロールを実装する前に、リストの表示を実装します。
ここでは、ブログの記事一覧を取得して、タイトルを表示させる想定です。
<template>
<div class="vue-infinite-loading">
<ul>
<li v-for="item in list" :key="`list${item.id}`">
{{ item.title }}
</li>
</ul>
</div>
</template>
<script>
export default {
data() {
return {
list: [],
}
},
}
</script>
まだ記事を取得していないので、何も表示されませんが、これで記事のタイトルを表示するコンポーネントができました。
ここから vue-infinite-loading を導入して、記事の取得まで実装していきます。
まずは vue-infinite-loading をインポートし、コンポーネントを設置します。
<template>
<div class="vue-infinite-loading">
...
<infinite-loading></infinite-loading>
</div>
</template>
<script>
import InfiniteLoading from 'vue-infinite-loading'
export default {
components: {
InfiniteLoading,
},
data() {
return {
list: [],
}
},
}
</script>
次にinfinite-loading
コンポーネントがスクロール等で画面に表示された際に、記事を取得する仕組みを実装します。
<template>
<div class="vue-infinite-loading">
...
<infinite-loading @infinite="infiniteHandler"></infinite-loading>
</div>
</template>
<script>
import InfiniteLoading from 'vue-infinite-loading'
export default {
...
methods: {
// infinite-loadingが表示された際の処理
async infiniteHandler($state) {
// 記事データの取得
const data = await this.fetchData()
if (!data) {
$state.error()
} else if (data.length) {
this.list.push(...data)
$state.loaded()
} else if (data.length === 0) {
$state.complete()
}
},
// 記事データ取得処理
async fetchData() {
// axios等で記事を取得する 以下、モック用データを生成
let data = []
let num = this.list.length
if (num < 200) { // 最大200件まで
for (let i = 1; i <= 20; i++) {
data.push({ id: num + i , title: `記事タイトル${num + i}` })
}
}
return data
},
}
}
</script>
まず、infinite-loading
コンポーネントに@infinite="infiniteHandler"
を追記し、infinite-loading
が表示された際にinfiniteHandler
メソッドが呼び出されるようにします。
infiniteHandler
メソッドではfetchData
メソッドで記事データを取得しています。本来であれば、axios
等で記事を取得しますが、今回はサンプルデータを最大 200 件まで作成し、20 件ずつ記事データを返すようにしています。
データを取得した後は、infinite-loading
のハンドリングです。
infiniteHandler
が呼ばれた際は$state
が戻り値となります。この$state
を使ってinfiniteHandler
が呼び出された後にinfinite-loading
に結果がどうなったか、通知してあげる必要があります。
記事の取得に失敗した場合、data
にnull
が入ってくる事を想定しています。
エラーの際は$state.error()
でエラーの通知をします。
記事データが取得できた場合、list
に記事データを追加して、$state.loaded()
で読み込みが完了して事を通知します。
記事データが全て取得し終えた場合は、$state.complete()
で完了を通知します。
この状態によって、infinite-loading
の表示が変わることが確認できるかと思います。
spinner
プロパティを設定することで、ローディングのアイコンを変える事が可能です。
default
spiral
circles
bubbles
waveDots
が指定できる他、slot
で独自で用意したローディングコンポーネント等を表示することも可能です。
spinner (opens new window)
<infinite-loading>
<div slot="spinner">記事を取得中...</div>
</infinite-loading>
表示するデータが無い場合、デフォルトではNo results :(
と表示されますが、slot
でno-results
を定義するとカスタマイズ可能です。
<infinite-loading>
<div slot="no-results">記事がありませんでした。</div>
</infinite-loading>
データを全て取得し終えた場合、デフォルトではNo more data :)
と表示されますが、slot
でno-more
を定義するとカスタマイズ可能です。
<infinite-loading>
<div slot="no-more">全ての記事が表示されました。</div>
</infinite-loading>
エラーの場合、デフォルトではOpps, something went wrong :(
と表示され、Retry ボタンが表示されますが、slot
でerror
を定義するとカスタマイズ可能です。
<infinite-loading>
<div slot="error">記事の取得中にエラーが発生しました。</div>
</infinite-loading>
<template>
<div class="vue-infinite-loading">
<ul>
<li v-for="item in list" :key="`list${item.id}`">
{{ item.title }}
</li>
</ul>
<infinite-loading @infinite="infiniteHandler" spinner="bubbles">
<div slot="no-results">記事がありませんでした。</div>
<div slot="no-more">全ての記事が表示されました。</div>
<div slot="error">記事の取得中にエラーが発生しました。</div>
</infinite-loading>
</div>
</template>
<script>
import InfiniteLoading from "vue-infinite-loading";
export default {
components: {
InfiniteLoading
},
data() {
return {
list: []
};
},
methods: {
async infiniteHandler($state) {
const data = await this.fetchData();
if (!data) {
$state.error();
} else if (data.length) {
this.list.push(...data);
$state.loaded();
} else if (data.length === 0) {
$state.complete();
}
},
async fetchData() {
await this.sleep(1500);
let data = [];
let num = this.list && this.list.length;
if (num < 200) {
for (let i = 1; i <= 20; i++) {
data.push({ id: num + i, title: `記事タイトル${num + i}` });
}
}
return data;
},
sleep(time) {
return new Promise(resolve => {
setTimeout(() => {
resolve();
}, time);
});
}
}
};
</script>
<style lang="stylus" scoped>
.vue-infinite-loading
max-height: 240px
padding 16px
overflow-y scroll
</style>