Skip to content

Commit

Permalink
feat(useVirtualList): adding support for reactive lists (#904)
Browse files Browse the repository at this point in the history
Co-authored-by: Anthony Fu <anthonyfu117@hotmail.com>
  • Loading branch information
JessicaSachs and antfu committed Nov 14, 2021
1 parent 8027364 commit 23356b2
Show file tree
Hide file tree
Showing 6 changed files with 104 additions and 44 deletions.
18 changes: 7 additions & 11 deletions packages/.vitepress/theme/styles/demo.css
Expand Up @@ -99,6 +99,7 @@
border-top: 0;
}


textarea {
display: block;
min-width: 20rem;
Expand All @@ -111,32 +112,27 @@
background: var(--c-bg);
color: var(--c-text);
}

textarea[readonly] {
background: var(--c-bg-light);
}
input[type='text'] {

input[type='text'], input[type='search'], input[type='number'] {
display: block;
min-width: 20rem;
font-size: 1.05rem;
padding: 0.5em 1em 0.4em 1em;
border: 1px solid var(--c-divider-light);
border-radius: 4px;
margin: 1rem 0;
outline: none;
background: var(--c-bg);
color: var(--c-text);
min-width: 20rem;
margin: 0.5rem 0;
}

input[type='number'] {
display: block;
min-width: 15rem;
font-size: 1.05rem;
padding: 0.5em 1em 0.4em 1em;
border: 1px solid var(--c-divider-light);
border-radius: 4px;
margin: 0.5rem 0;
outline: none;
background: var(--c-bg);
color: var(--c-text);
}

input:focus {
Expand Down
1 change: 1 addition & 0 deletions packages/core/useVirtualList/component.ts
Expand Up @@ -34,6 +34,7 @@ export const UseVirtualList = defineComponent<UseVirtualListProps>({
props.list,
props.options,
)

containerProps.style.height = props.height || '300px'
return () => h('div',
{ ...containerProps },
Expand Down
43 changes: 32 additions & 11 deletions packages/core/useVirtualList/demo.vue
@@ -1,43 +1,64 @@
<script setup lang="ts">
import { ref, Ref } from 'vue-demi'
import { ref, Ref, computed } from 'vue-demi'
import { useVirtualList } from '.'
const index: Ref = ref(0)
const index: Ref = ref()
const search = ref('')
const allItems = Array.from(Array(99999).keys())
.map(i => ({
height: i % 2 === 0 ? 42 : 84,
size: i % 2 === 0 ? 'small' : 'large',
}))
const filteredItems = computed(() => {
return allItems.filter(i => i.size.startsWith(search.value.toLowerCase()))
})
const { list, containerProps, wrapperProps, scrollTo } = useVirtualList(
Array.from(Array(99999).keys()),
filteredItems,
{
itemHeight: i => (i % 2 === 0 ? 42 + 8 : 84 + 8),
itemHeight: i => (filteredItems.value[i].height + 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" />
<div>
<div class="inline-block mr-4">
Jump to index
<input v-model="index" placeholder="Index" type="number" />
</div>
<button type="button" @click="handleScrollTo">
Go
</button>
</div>
<div>
<div class="inline-block mr-4">
Filter list by size
<input v-model="search" placeholder="e.g. small, medium, large" type="search" />
</div>
</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"
v-for="{ index, data } in list"
:key="index"
class="border border-$c-divider mb-2"
:style="{
height: ele.index % 2 === 0 ? '42px' : '84px',
height: `${data.height}px`,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}"
>
Row {{ ele.data }}
<span opacity="70" m="l-1">({{ ele.index % 2 === 0 ? 'small' : 'large' }})</span>
Row {{ index }} <span opacity="70" m="l-1">({{ data.size }})</span>
</div>
</div>
</div>
Expand Down
38 changes: 35 additions & 3 deletions packages/core/useVirtualList/index.md
Expand Up @@ -4,15 +4,45 @@ category: Component

# useVirtualList

Composable virtual list. It allows you to display a large list of items while only rendering visible ones on the screen.
Create virtual lists with ease. Virtual lists (sometimes called [*virtual scrollers*](https://akryum.github.io/vue-virtual-scroller/)) allow you to render a large number of items performantly. They only render the minimum number of DOM nodes necessary to show the items within the `container` element by using the `wrapper` element to emulate the container element's full height.

## Usage

### Simple list

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

const { list, containerProps, wrapperProps } = useVirtualList(
Array.from(Array(99999).keys()),
{
// Keep `itemHeight` in sync with the item's row.
itemHeight: 22,
},
)
```

### Config

| State | Type | Description |
|------------|----------|-------------------------------------------------------------------------------------------------|
| itemHeight | `number` | ensure that the total height of the `wrapper` element is calculated correctly.* |
| overscan | `number` | number of pre-rendered DOM nodes. Prevents whitespace between items if you scroll very quickly. |

\* The `itemHeight` must be kept in sync with the height of each row rendered. If you are seeing extra whitespace or jitter when scrolling to the bottom of the list, ensure the `itemHeight` is the same height as the row.

### Reactive list

```typescript
import { useVirtualList, useToggle } from '@vueuse/core'
import { computed } from 'vue'

const [isEven, toggle] = useToggle()
const allItems = Array.from(Array(99999).keys())
const filteredList = computed(() => allItems.filter(i => isEven.value ? i % 2 === 0 : i % 2 === 1))

const { list, containerProps, wrapperProps } = useVirtualList(
filteredList,
{
itemHeight: 22,
},
Expand All @@ -21,9 +51,11 @@ const { list, containerProps, wrapperProps } = useVirtualList(

```html
<template>
<p>Showing {{ isEven ? 'even' : 'odd' }} items</p>
<button @click="toggle">Toggle Even/Odd</button>
<div v-bind="containerProps" style="height: 300px">
<div v-bind="wrapperProps">
<div v-for="item in list" :key="item.index">
<div v-for="item in list" :key="item.index" style="height: 22px">
Row: {{ item.data }}
</div>
</div>
Expand All @@ -37,7 +69,7 @@ const { list, containerProps, wrapperProps } = useVirtualList(
<UseVirtualList :list="list" :options="options" height="300px">
<template #="props">
<!-- you can get current item of list here -->
Row {{ props.data }}
<div style="height: 22px">Row {{ props.data }}</div>
</template>
</UseVirtualList>
```
46 changes: 28 additions & 18 deletions packages/core/useVirtualList/index.ts
@@ -1,4 +1,5 @@
import { watch, Ref, ref, computed } from 'vue-demi'
import { watch, Ref, ref, computed, shallowRef } from 'vue-demi'
import type { MaybeRef } from '@vueuse/shared'
import { useElementSize } from '../useElementSize'

export interface UseVirtualListOptions {
Expand All @@ -16,25 +17,29 @@ export interface UseVirtualListOptions {
overscan?: number
}

export function useVirtualList <T = any>(list: T[], options: UseVirtualListOptions) {
export type UseVirtualListItem<T> = {
data: T
index: number
}

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

const currentList: Ref<UseVirtualListItem<T>[]> = ref([])
const source = shallowRef(list)

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++) {
for (let i = start; i < source.value.length; i++) {
const height = (itemHeight as (index: number) => number)(i)
sum += height
if (sum >= containerHeight) {
Expand All @@ -51,7 +56,7 @@ export function useVirtualList <T = any>(list: T[], options: UseVirtualListOptio

let sum = 0
let offset = 0
for (let i = 0; i < list.length; i++) {
for (let i = 0; i < source.value.length; i++) {
const height = (itemHeight as (index: number) => number)(i)
sum += height
if (sum >= scrollTop) {
Expand All @@ -72,32 +77,37 @@ export function useVirtualList <T = any>(list: T[], options: UseVirtualListOptio
const to = offset + viewCapacity + overscan
state.value = {
start: from < 0 ? 0 : from,
end: to > list.length ? list.length : to,
end: to > source.value.length
? source.value.length
: to,
}
currentList.value = list.slice(state.value.start, state.value.end).map((ele, index) => ({
data: ele,
index: index + state.value.start,
}))
currentList.value = source.value
.slice(state.value.start, state.value.end)
.map((ele, index) => ({
data: ele,
index: index + state.value.start,
}))
}
}

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

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

return list.reduce((sum, _, index) => sum + itemHeight(index), 0)
return source.value.length * itemHeight
return source.value.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)
const height = source.value
.slice(0, index)
.reduce((sum, _, i) => sum + itemHeight(i), 0)
return height
}

Expand Down
2 changes: 1 addition & 1 deletion packages/functions.md
Expand Up @@ -48,7 +48,7 @@
- [`tryOnUnmounted`](https://vueuse.org/shared/tryOnUnmounted/) — safe `onUnmounted`
- [`unrefElement`](https://vueuse.org/core/unrefElement/) — unref for dom element
- [`useTemplateRefsList`](https://vueuse.org/core/useTemplateRefsList/) — shorthand for binding refs to template elements and components inside `v-for`
- [`useVirtualList`](https://vueuse.org/core/useVirtualList/)composable virtual list
- [`useVirtualList`](https://vueuse.org/core/useVirtualList/)create virtual lists with ease
- [`useVModel`](https://vueuse.org/core/useVModel/) — shorthand for v-model binding
- [`useVModels`](https://vueuse.org/core/useVModels/) — shorthand for props v-model binding

Expand Down

0 comments on commit 23356b2

Please sign in to comment.