Skip to content

Commit

Permalink
feat(useVirtualList): add useVirtualList function (vitest-dev#710)
Browse files Browse the repository at this point in the history
Co-authored-by: Anthony Fu <anthonyfu117@hotmail.com>
  • Loading branch information
caozhong1996 and antfu committed Sep 17, 2021
1 parent 7c4fefc commit bde7f1a
Show file tree
Hide file tree
Showing 8 changed files with 286 additions and 0 deletions.
8 changes: 8 additions & 0 deletions indexes.json
Original file line number Diff line number Diff line change
Expand Up @@ -995,6 +995,14 @@
"category": "Component",
"description": "shorthand for props v-model binding"
},
{
"name": "useVirtualList",
"package": "core",
"component": true,
"docs": "https://vueuse.org/core/useVirtualList/",
"category": "Sensors",
"description": "virtual list migrating from ahooks to Composition API"
},
{
"name": "useWakeLock",
"package": "core",
Expand Down
1 change: 1 addition & 0 deletions packages/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,6 @@ export * from '../core/usePreferredDark/component'
export * from '../core/usePreferredLanguages/component'
export * from '../core/useTimeAgo/component'
export * from '../core/useTimestamp/component'
export * from '../core/useVirtualList/component'
export * from '../core/useWindowFocus/component'
export * from '../core/useWindowSize/component'
1 change: 1 addition & 0 deletions packages/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export * from './useUrlSearchParams'
export * from './useUserMedia'
export * from './useVModel'
export * from './useVModels'
export * from './useVirtualList'
export * from './useWakeLock'
export * from './useWebSocket'
export * from './useWebWorker'
Expand Down
51 changes: 51 additions & 0 deletions packages/core/useVirtualList/component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { defineComponent, h } from 'vue-demi'
import { useVirtualList, UseVirtualListOptions } from '@vueuse/core'

export interface UseVirtualListProps {
/**
* data of scrollable list
*
* @default []
*/
list: Array<any>
/**
* useVirtualList's options
*
* @default {}
*/
options: UseVirtualListOptions
/**
* virtualList's height
*
* @default 300px
*/
height: string
}

export const UseVirtualList = defineComponent<UseVirtualListProps>({
name: 'UseVirtualList',
props: [
'list',
'options',
'height',
] as unknown as undefined,
setup(props, { slots }) {
const { list, containerProps, wrapperProps } = useVirtualList(
props.list,
props.options,
)
containerProps.style.height = props.height || '300px'
return () => h('div',
{ ...containerProps },
[
h('div',
{ ...wrapperProps.value },
list.value.map((item: any) => h('div',
{ style: { overFlow: 'hidden', height: item.height } },
slots.default ? slots.default(item) : 'Please set content!',
)),
),
],
)
},
})
45 changes: 45 additions & 0 deletions packages/core/useVirtualList/demo.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<script setup lang="ts">
import { ref, Ref } from 'vue-demi'
import { useVirtualList } from '.'
const index: Ref = ref(0)
const { list, containerProps, wrapperProps, scrollTo } = useVirtualList(
Array.from(Array(99999).keys()),
{
itemHeight: i => (i % 2 === 0 ? 42 + 8 : 84 + 8),
overscan: 10,
},
)
const handleScrollTo = () => {
scrollTo(index.value)
}
</script>

<template>
<div>
<div class="mb-4 flex gap-2">
<input v-model="index" placeholder="Index" type="number" />
<button type="button" @click="handleScrollTo">
Go
</button>
</div>
<div v-bind="containerProps" class="h-300px overflow-auto p-2 bg-gray-500/5 rounded">
<div v-bind="wrapperProps">
<div
v-for="ele in list"
:key="ele.index"
class="border border-$c-divider mb-2"
:style="{
height: ele.index % 2 === 0 ? '42px' : '84px',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}"
>
Row {{ ele.data }}
<span opacity="70" m="l-1">({{ ele.index % 2 === 0 ? 'small' : 'large' }})</span>
</div>
</div>
</div>
</div>
</template>
43 changes: 43 additions & 0 deletions packages/core/useVirtualList/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
category: Component
---

# useVirtualList

Composable virtual list. It allows you to display a large list of items while only rendering visible ones on the screen.

## Usage

```typescript
import { useVirtualList } from '@vueuse/core'

const { list, containerProps, wrapperProps } = useVirtualList(
Array.from(Array(99999).keys()),
{
itemHeight: 22,
},
)
```

```html
<template>
<div v-bind="containerProps" style="height: 300px">
<div v-bind="wrapperProps">
<div v-for="item in list" :key="item.index">
Row: {{ item.data }}
</div>
</div>
</div>
</template>
```

## Component

```html
<UseVirtualList :list="list" :options="options" height="300px">
<template #="props">
<!-- you can get current item of list here -->
Row {{ props.data }}
</template>
</UseVirtualList>
```
136 changes: 136 additions & 0 deletions packages/core/useVirtualList/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { watch, Ref, ref, computed } from 'vue-demi'
import { useElementSize } from '../useElementSize'

export interface UseVirtualListOptions {
/**
* item height, accept a pixel value or a function that returns the height
*
* @default 0
*/
itemHeight: number | ((index: number) => number)
/**
* the extra buffer items outside of the view area
*
* @default 5
*/
overscan?: number
}

export function useVirtualList <T = any>(list: T[], options: UseVirtualListOptions) {
const containerRef: Ref = ref<HTMLElement | null>()
const size = useElementSize(containerRef)
const currentList: Ref = ref([])

const state: Ref = ref({ start: 0, end: 10 })
const { itemHeight, overscan = 5 } = options

if (!itemHeight)
console.warn('please enter a valid itemHeight')

const getViewCapacity = (containerHeight: number) => {
if (typeof itemHeight === 'number')
return Math.ceil(containerHeight / itemHeight)

const { start = 0 } = state.value
let sum = 0
let capacity = 0
for (let i = start; i < list.length; i++) {
const height = (itemHeight as (index: number) => number)(i)
sum += height
if (sum >= containerHeight) {
capacity = i
break
}
}
return capacity - start
}

const getOffset = (scrollTop: number) => {
if (typeof itemHeight === 'number')
return Math.floor(scrollTop / itemHeight) + 1

let sum = 0
let offset = 0
for (let i = 0; i < list.length; i++) {
const height = (itemHeight as (index: number) => number)(i)
sum += height
if (sum >= scrollTop) {
offset = i
break
}
}
return offset + 1
}

const calculateRange = () => {
const element = containerRef.value
if (element) {
const offset = getOffset(element.scrollTop)
const viewCapacity = getViewCapacity(element.clientHeight)

const from = offset - overscan
const to = offset + viewCapacity + overscan
state.value = {
start: from < 0 ? 0 : from,
end: to > list.length ? list.length : to,
}
currentList.value = list.slice(state.value.start, state.value.end).map((ele, index) => ({
data: ele,
index: index + state.value.start,
}))
}
}

watch([size.width, size.height], () => {
calculateRange()
})

const totalHeight = computed(() => {
if (typeof itemHeight === 'number')
return list.length * itemHeight

return list.reduce((sum, _, index) => sum + itemHeight(index), 0)
})

const getDistanceTop = (index: number) => {
if (typeof itemHeight === 'number') {
const height = index * itemHeight
return height
}
const height = list.slice(0, index).reduce((sum, _, i) => sum + itemHeight(i), 0)
return height
}

const scrollTo = (index: number) => {
if (containerRef.value) {
containerRef.value.scrollTop = getDistanceTop(index)
calculateRange()
}
}

const offsetTop = computed(() => getDistanceTop(state.value.start))
const wrapperProps = computed(() => {
return {
style: {
width: '100%',
height: `${totalHeight.value - offsetTop.value}px`,
marginTop: `${offsetTop.value}px`,
},
}
})

const containerStyle: Partial<CSSStyleDeclaration> = { overflowY: 'auto' }

return {
list: currentList,
scrollTo,
containerProps: {
ref: containerRef,
onScroll: () => {
calculateRange()
},
style: containerStyle,
},
wrapperProps,
}
}
1 change: 1 addition & 0 deletions packages/functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
- [`useSpeechRecognition`](https://vueuse.org/core/useSpeechRecognition/) — reactive [SpeechRecognition](https://developer.mozilla.org/en-US/docs/Web/API/SpeechRecognition)
- [`useSwipe`](https://vueuse.org/core/useSwipe/) — reactive swipe detection based on [`TouchEvents`](https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent)
- [`useUserMedia`](https://vueuse.org/core/useUserMedia/) — reactive [`mediaDevices.getUserMedia`](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia) streaming
- [`useVirtualList`](https://vueuse.org/core/useVirtualList/) — virtual list migrating from ahooks to Composition API
- [`useWindowFocus`](https://vueuse.org/core/useWindowFocus/) — reactively track window focus with `window.onfocus` and `window.onblur` events
- [`useWindowScroll`](https://vueuse.org/core/useWindowScroll/) — reactive window scroll
- [`useWindowSize`](https://vueuse.org/core/useWindowSize/) — reactive window size
Expand Down

0 comments on commit bde7f1a

Please sign in to comment.