動的に項目を増やせるドロップダウンが欲しかったのですが、Vuetifyに見当たらなかったので作ってみることにしました。
(2021/12/23追記)
実際に動かせるサンプルを公開しました
https://teracy164.github.io/infinite-scroll-drop-down/
コードは以下にあります。
https://github.com/teracy164/infinite-scroll-drop-down
バージョン
$ vue --version
@vue/cli 4.5.11
環境構築
# Vueプロジェクトを作成
vue create infinite-scroll-drop-down
? Please pick a preset: Default ([Vue 2] babel, eslint)
cd infinite-scroll-drop-down
# Vuetifyをインストール
vue add vuetify
? Choose a preset: Default (recommended)
※執筆時点でVuetifyがVue3に対応していないため、Vue2で構築します
まずは手動で項目読み込みできるセレクトボックスを作る
※v-select
の基本的な使い方はリファレンスを参照してください
https://vuetifyjs.com/ja/components/selects/
- append-itemでリスト末尾に読み込みボタンを配置します
- 読み込みボタン押下時は親コンポーネントに
next
イベントをemitします
<template>
<v-select
:items="items"
:label="label"
item-text="label"
item-value="value">
<template v-if="!loadCompleted" v-slot:append-item>
<v-divider class="mb-2"></v-divider>
<v-list-item v-if="!loading" @click="onclickload">
load
</v-list-item>
<v-list-item v-else>
loading...
</v-list-item>
</template>
</v-select>
</template>
<script>
export default {
props: {
items: {
type: Array,
default: () => [],
},
label: {
type: String,
default: '',
},
loadCompleted: {
type: Boolean,
default: false,
},
loading: {
type: Boolean,
default: false,
},
},
methods: {
onclickload() {
this.$emit('next')
}
}
}
</script>
- 親のコンポーネントで作成したコンポーネントを配置します
- 必要なデータを渡してあげます
<template>
<v-container>
<v-row class="text-center">
<v-col cols="12">
<InfiniteScrollDropDown
:items="items"
:loadCompleted="data.length === loadedItemNum"
:loading="loading"
@next="onnext()" />
</v-col>
</v-row>
</v-container>
</template>
<script>
import InfiniteScrollDropDown from './parts/InfiniteScrollDropDown'
export default {
name: 'HelloWorld',
components: {
InfiniteScrollDropDown,
},
mounted() {
this.onnext()
},
data: () => {
return {
// MAX50個のリスト
data: new Array(50).fill().map((v, i) => ({value: i + 1, label: `item${i + 1}`})),
items: [],
loadedItemNum: 0,
loading: false,
}
},
methods: {
onnext() {
this.loading = true
// 非同期での動作確認のため少し待ってから追加する
setTimeout(() => {
const step = 10;
this.items.push(...this.data.slice(this.loadedItemNum, this.loadedItemNum + step));
this.loadedItemNum += step;
this.loading = false;
}, 500);
}
}
}
</script>
この段階で↓のようにリスト末尾のloadを押下すると次の10件を読めるようになります
無限スクロール対応
リストの中身を任意のタイミングで追加することができるようになったので、あとはリスト末尾までスクロールしたときにnext
イベントをemitしてやれば無限スクロールの完成です。
実際に無限スクロールに対応させたものが↓になります
<template>
<v-select
:items="items"
:label="label"
item-text="label"
item-value="value"
:menu-props="{'content-class': listClass}"
@click="onclick">
<template v-if="!loadCompleted" v-slot:append-item>
<v-divider class="mb-2"></v-divider>
<v-list-item v-if="!loading" @click="onclickload">
load
</v-list-item>
<v-list-item v-else>
loading...
</v-list-item>
</template>
</v-select>
</template>
<script>
export default {
props: {
items: {
type: Array,
default: () => [],
},
label: {
type: String,
default: '',
},
loadCompleted: {
type: Boolean,
default: false,
},
loading: {
type: Boolean,
default: false,
},
},
data() {
return {
listClass: 'my-select-list',
};
},
methods: {
onclick() {
let cnt = 0;
const getList = () => {
if (++cnt > 20) return;
const elements = document.getElementsByClassName(this.listClass);
if (elements.length) {
const menu = elements[0];
menu.addEventListener('scroll', () => {
if (menu.scrollHeight - (menu.clientHeight + menu.scrollTop) < 5) {
this.scrollend()
}
});
} else {
setTimeout(() => getList(), 100);
}
};
getList();
},
scrollend() {
if (!this.loadCompleted) {
this.$emit('next');
}
},
onclickload() {
this.$emit('next');
}
}
}
</script>
ポイント
-
:menu-props="{'content-class': listClass}"
で展開されるリストにclassを設定しておきます- ドロップダウンのリストはv-menuで実現されているため、探せるように明示的に名前をつけています
- なお、同一画面内に複数のドロップダウンを配置したい場合はユニークになるように工夫が必要です
- クリック時に上記のクラス名でv-menuを検索します
- クリック指定から表示されるまでに時間がかかるため、表示されるのを待ちます
- リストは
this.$el
下にはなく、bodyのほぼ直下に作成されるため、document.get〜
で探します
- scrollイベントを監視し、末尾までスクロールされたかチェックします
- 末尾までスクロールされた際にまだ読み込み可能なデータがあればnextイベントを親コンポーネントにemitします
まとめ
これで無限スクロール対応のドロップダウンができました。
思ったより簡単に実現できたので、いろんなところで活用できそうです^^