Code Monkey home page Code Monkey logo

vue-virtual-collection's Introduction

vue-virtual-collection

npm version Build Status styled with prettier npm

Vue component for efficiently rendering large collection data. Inspired by react-virtualize.

Demo

Demo

Table of Contents generated with DocToc

Usage

Install

npm i vue-virtual-collection

Import

import Vue from 'vue'
import VirtualCollection from 'vue-virtual-collection'

Vue.use(VirtualCollection)

Use it!

vue-virtual-collection offers two ways of using a VirtualCollection.

Default Usage

The standard way of using a VirtualCollection is to to pass the entire collection of items to it as a single group. This is suitable for most cases, especially if all the items in the collection are either very similar or very different from one another.

In the sample below, the collection is instantiated as an Array and passed directly to the VirtualCollection in that form.

<template>
    <div>
        <VirtualCollection
            :cellSizeAndPositionGetter="cellSizeAndPositionGetter"
            :collection="items"
            :height="500"
            :width="330"
        >
            <div slot="cell" slot-scope="props">{{props.data}}</div>
        </VirtualCollection>
    </div>
</template>

<script>
    export default {
        data () {
            return {
                /**
                 * This will create 1000 items like:
                 * [
                 *   { data: '#0' },
                 *   { data: '#1' },
                 *   ...
                 *   { data: '#999' }
                 * ]
                 */
                items: Array.from({length: 1000}, (_, index) => ({ data: '#' + index }))
            }
        },
        methods: {
            cellSizeAndPositionGetter(item, index) {
                // compute size and position
                return {
                    width: 100,
                    height: 150,
                    x: (index % 2) * 110,
                    y: parseInt(index / 2) * 160
                }
            }
        }
    }
</script>

Item Grouping

Item grouping is an optional feature that can be used to potentially improve performance, and is ideally suited to cases where the collection of items to render are logically divisable into a small number of different categories that may be updated, added or removed at different rates.

Instead of passing the entire collection as an array to the VirtualCollection, the collection is passed as an array of "groups" - objects that each contain a subset of the entire collection's items that should logically be handled the same way.

An example of where this might be appropriate is using VirtualCollection to represent a chess game. In this scenario, there might be separate items within the collection to represent the chessboard tiles and the chess pieces. Chessboard tiles and chess pieces are logically distinct categories of items, and each category would update at different rates; the collection of chessboard tiles would never change after being initially rendered, whereas the collection of pieces would change after every move.

Without grouping, changing the position of a piece (or adding/removing one) would cause everything in the collection to be re-processed and potentially re-rendered, including the chessboard tiles. However, if we pass the pieces and the tiles as separate groups, then VirtualCollection will only reprocess the collection of chess pieces after each turn, and skip reprocessing the chessboard tiles.

<template>
    <div>
        <VirtualCollection
            :cellSizeAndPositionGetter="cellSizeAndPositionGetter"
            :collection="collectionGroups"
            :height="500"
            :width="500"
        >
            <div class="grid-cell" slot="cell" slot-scope="{data}">
                <ChessTile v-if="data.isTile" />
                <ChessPiece v-else />
            </div>
        </VirtualCollection>
    </div>
</template>

<script>
    export default {
        data () {
            return {
                chessTiles: [ { data: { isTile: true } }, /* ... */ ],
                chessPieces: [ { data: { isPiece: true } }, /* ... */ ],
                collectionGroups: [ { group: [] }, { group: [] } ]
            }
        },
        methods: {
            cellSizeAndPositionGetter(item, itemIndex, groupIndex) {
                switch (groupIndex)
                {
                    case 0:
                        // Compute size and position for chessboard tile
                        return { width: 0, height: 0, x: 0, y: 0 }
                    
                    case 1:
                        // Compute size and position for chess piece
                        return { width: 0, height: 0, x: 0, y: 0 }

                    default:
                        throw new Error("Not a tile or piece")
                }
            }
        },
        watch: {
            chessTiles: function (chessTiles) {
                this.collectionGroups[0].group = chessTiles;
            },
            chessPieces: function (chessPieces) {
                this.collectionGroups[1].group = chessPieces
            }
        }
    }
</script>

Props

cellSizeAndPositionGetter

Type: Function

(item: object, index: number) -> ({ height: number, width: number, x: number, y: number })

Required: ✓

Callback responsible for returning size and offset/position information for a given cell

function cellSizeAndPositionGetter(item, index) {
    return {
        width: item.width,
        height: item.height,
        x: item.x,
        y: item.y
    }
}

Alternate usage

If using the item grouping feature, the second parameter is the index of the item within the group it belongs to and a third parameter represents the index of the group within the collection of groups passed to VirtualCollection.

(item: object, itemIndex: number, groupIndex: number) -> ({ height: number, width: number, x: number, y: number })

function cellSizeAndPositionGetter(item, itemIndex, groupIndex) {
    return {
        width: item.width,
        height: item.height,
        x: item.x,
        y: item.y
    }
}

collection

Type: Array

Required: ✓

The data for cells to render. Each object in array must contain a data property, which will be passed into slot scope.

const collection = [
    { data: { text: "#1" } },
    { data: { text: "#2" } },
    { data: { text: "#3" } },
    // ...
]

Alternate usage

If using the item grouping feature, each element of the collection should be an object with a group property representing a group of logically-related items. Each item of each group must contain a data property, which will be passed into slot scope.

const collection = [
    {
        group: [
            { data: { text: "#1" } },
            { data: { text: "#2" } },
            { data: { text: "#3" } },
            // ...
        ]
    },
    {
        group: [
            { data: { text: "#A" } },
            { data: { text: "#B" } },
            { data: { text: "#C" } },
            // ...
        ]
    },
    // ...
]

width

Type: number

Required: ✓

The width of collection viewport

height

Type: number

Required: ✓

The height of collection viewport

scrollToBottomRange

Type: number

Default: undefined

When present the component will emit scrolled-to-bottom-range when the bottom is >= 1 and the number provided. The scrolled-to-bottom will still be fired, and the 2 events will not be emitted at the same time.

containerPaddingBottom

Type: number

Default: 0

Optionally extend the calculated height of the collection container, the affect is apparent padding-bottom

headerSlotHeight

Type: number

Default: 0

When injecting content into the header slot, you should also add the px height of the slot. This ensure items are removed from view at the right time.

sectionSize

Type: number

Default: 300

Optionally override the size of the sections a Collection's cells are split into. This is an advanced option and should only be used for performance tuning purposes.

Events

scrolled-to-top

This event is emitted when the container scrollTop is reduced to 0.

scrolled-to-bottom

This event is emitted when the container scrollTop has reach the bottom.

scrolled-to-bottom-range

This event is emitted only when a scrollToBottomRange value is provided, and is fired when the container is scroller with range of the bottom, the range defined by the said prop.

Slots

header

The header slot allows you to simulate a full-page mode for the virtual-scroller content. By setting the VirtualCollection height and width to be that of the browser window, the header will then sit on top of the scrollable items however will move out of view when the VirtualCollection is scrolled down.

<VirtualCollection
    :cellSizeAndPositionGetter="item => { return { width: item.width, height: item.height, x: item.x, y: item.y }}"
    :collection="items.items"
    :height="items.boxHeight"
    :width="items.boxWidth"
    :containerHeightSpacer="50"
    v-on:scrolled-to-top="scrollTop"
    v-on:scrolled-to-bottom="scrollBottom">
  <template v-slot:header>
    <div>
        This content will sit on top of the scrollable items acting as a header.
    </div>
  </template>
  <div slot="cell" slot-scope="props">
      {{props.data}}
  </div>
</VirtualCollection>

cell

<div slot="cell" slot-scope="yourOwnScope">{{yourOwnScope.data.text}}</div>

The data property in items of collection will be passed into the slot scope.

const collection = [
    { data: { text: "#1" } },
    { data: { text: "#2" } },
    { data: { text: "#3" } },
    // ...
]

vue-virtual-collection's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

vue-virtual-collection's Issues

How to handle infinite scroll triggering?

I would like to implement an infinite scroll ui with this library. And would like to know how can I know it's time to trigger another fetch. I seem there are no callback for this use case.

Out-of-bounds array access in getComputedStyle when removing grid items

I'm trying to make a grid that dynamically adds and removes items from a vue virtual collection based on how the user scrolls. However, when I remove items from the collection, I get errors like this:

TypeError: Cannot read property 'width' of undefined
    at VueComponent.getComputedStyle (C:\Users\<me>\Documents\VSCode\atm-prototype\node_modules\vue-virtual-collection\dist\index.js:218:47)

This is because in the instances that items get removed from the collection, getComputedStyle ends up getting passed displayItems whose index is beyond the bounds of the cellSizeAndPosition array:

getComputedStyle(displayItem) {
if (!displayItem) return
const { width, height, x, y } = this._sectionManager.getCellMetadata(displayItem.index)
return {
left: `${x}px`,
top: `${y}px`,
width: `${width}px`,
height: `${height}px`
}
},

Unless there's something I'm doing wrong that I could fix on my end, I guess this could be resolved by just adding a check in getComputedStyle:

if (!displayItem) return
- const { width, height, x, y } = this._sectionManager.getCellMetadata(displayItem.index)
+ const cell = this._sectionManager.getCellMetadata(displayItem.index)
+ if (!cell) return
+ const { width, height, x, y } = cell

Nuxt SSR Support

I love this library. Are there any plans to support Nuxt.js? This is very necessary so that pages using vue-virtual-collection can have good SEO.

Any support for BigInt?

I have a very large virtual map to display (N> Number.MAX_SAFE_INTEGER), will this lib support for index of BigInt?

滑动时白屏,这个能否在优化下??

你好,是这样子的,我用你的这个组件写了个商品列表,每行4列,每个项目里面有一张图片,还有一些文本,每个商品项目尺寸大概是220pxX400px,当我放进2000个左右商品的时候,滑起来的体验不是很好,很容易白屏,我在想是不是调整成transform:translate3d来控制滑动是不是会有所改善?

Do you support unknown item heights

  • I have a bunch of items whose heights is not known
  • It is a list of items loaded from AJAX call where each item may have a different height
  • how does this library handle this?

关于滑动时白屏,优化方案,目前确定是 scrollTop 计算问题

原因是 原生 scrollTop 计算滚动 重新渲染 displayItems 的时机 没有与 vue 渲染时机相对应,导致了渲染延迟,出现了(白屏)

目前的解决方案是结合 better-scroll 的滚动插件----原因是 better-scroll 把原生滚动封装的比较好 对于移动端和PC滚动算是不错的选择

只需要修改 源码中 flushDisplayItems() 方法即可
一下是我结合 better-scroll 的代码

<template>
    <div class="vue-virtual-collection" :style="outerStyle" ref="outer">
        //  使用 better-scroll 封装的一个滚动骨架组件
        <vertical-scroll :listenScroll="true"
                         :probeType="3"
                         :bounce="false"
                         @scroll="verticalScroll">
            <div class="vue-virtual-collection-container" :style="containerStyle">
                <div class="cell-container" v-for="(item, index) in displayItems"
                     :key="item.index"
                     :style="getComputedStyle(item, index)" ref="cellContainer">
                    <slot name="cell" :data="item"></slot>
                </div>
            </div>
        </vertical-scroll>
    </div>
</template>

<script type="text/ecmascript-6">
    import VerticalScroll from "base/VerticalScroll";

    export default {
        methods: {     
            init () {
                // 设置当前视图我们中应该显示那些块
                this.flushDisplayItems(0, 0);
            },
            // 获取到视图应该渲染那些块之外我们还需要设置这些块所应该在的位置
            getComputedStyle (displayItem) {
                if (!displayItem) {
                    return;
                }

                const {width, height, x, y} = this._sectionManager.getCellMetadata(displayItem.index);

                return {
                    transform: `translate3d(${x}px, ${y}px, 0)`,
                    width: `${width}px`,
                    height: `${height}px`
                }
            },
            // 设置当前视图我们中应该显示那些块
            flushDisplayItems (x, y) {
                let scrollLeft = x;
                let scrollTop = Math.abs(y);

                // 然后这里我们需要去设置当前视图中应该渲染那些块
                // 于是我们要在 SectionManager类中定义一个方法去获取需要渲染的那个块的索引
                let indices = this._sectionManager.getCellIndices({
                    height: this.height,
                    width: this.width,
                    x: scrollLeft,
                    y: scrollTop
                });

                // 到这里我们已经获取到了索引了,然后我们就可以去渲染该视图所对应的块了
                const displayItems = [];

                indices.forEach(index => {
                    displayItems.push({
                        index,
                        ...this.collection[index]
                    })
                });

                // 重新渲染数据
                this.displayItems = displayItems;
            },
            // 滚动骨架组件监听滚动的方法-垂直滚动
            verticalScroll (pos) {
                this.flushDisplayItems(pos.x, pos.y)
            }
        },
        components: {
            VerticalScroll
        }
    }
</script>

<style lang="stylus">

    .vue-virtual-collection {
        overflow: hidden;
        -webkit-overflow-scrolling: touch;
        height: 100%;
        width: 100%
    }

    .vue-virtual-collection-container {
        position: relative;
        overflow: hidden;
    }

    .cell-container {
        position: absolute;
        top: 0;
        overflow: hidden;
    }

</style>

滚动骨架组件制作详情请参考这里

How to bind this to the window and not a containing box?

This component seems to be restricted to placing the content within a container within the main window.

I am trying to achieve a virtual scroll affect similar to Pinterest where the window scrolls and not the containing box.

Using this component, when setting the container to be large enough to house all the components this directly affects the items rendered.. eg setting the count to 1000, the column count to 8 but the overall container height 30000 results in the browser just rendering all the components within the 30k px window height, which is all 1000 items.

Is there a way to achieve this with vue-virtual-collection @starkwang ?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.