4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Vue.js+Vuetifyで無限スクロール可能なドロップダウンを作成してみる

Last updated at Posted at 2021-02-16

動的に項目を増やせるドロップダウンが欲しかったのですが、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します
src/components/parts/InfiniteScrollDropDown.vue
<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>
  • 親のコンポーネントで作成したコンポーネントを配置します
    • 必要なデータを渡してあげます
src/components/HelloWorld.vue
<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件を読めるようになります

スクリーンショット 2021-02-16 18.28.47.png

無限スクロール対応

リストの中身を任意のタイミングで追加することができるようになったので、あとはリスト末尾までスクロールしたときにnextイベントをemitしてやれば無限スクロールの完成です。

実際に無限スクロールに対応させたものが↓になります

src/components/parts/InfiniteScrollDropDown.vue
<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します

まとめ

これで無限スクロール対応のドロップダウンができました。
思ったより簡単に実現できたので、いろんなところで活用できそうです^^

4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?