Skip to content

Commit

Permalink
add keepMounted and itemProps to Vue API to enable sticky groups
Browse files Browse the repository at this point in the history
  • Loading branch information
AmitJoki committed Dec 9, 2024
1 parent 1908d6d commit c01a433
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 5 deletions.
7 changes: 6 additions & 1 deletion src/vue/ListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
StateVersion,
VirtualStore,
} from "../core";
import { ItemProps } from './utils';

/**
* @internal
Expand All @@ -33,6 +34,7 @@ export const ListItem = /*#__PURE__*/ defineComponent({
_isHorizontal: { type: Boolean },
_isSSR: { type: Boolean },
_as: { type: String as PropType<keyof NativeElements>, required: true },
_itemProps: Object as PropType<ReturnType<ItemProps>>,
},
setup(props) {
const elementRef = ref<HTMLDivElement>();
Expand Down Expand Up @@ -65,20 +67,23 @@ export const ListItem = /*#__PURE__*/ defineComponent({
} = props;
const isHide = hide.value;

const { style: styleProp, ...rest } = props._itemProps ?? {};

const style: StyleValue = {
position: isHide && isSSR ? undefined : "absolute",
[isHorizontal ? "height" : "width"]: "100%",
[isHorizontal ? "top" : "left"]: "0px",
[isHorizontal ? (isRTLDocument() ? "right" : "left") : "top"]:
offset.value + "px",
visibility: !isHide || isSSR ? "visible" : "hidden",
...styleProp
};
if (isHorizontal) {
style.display = "flex";
}

return (
<Element ref={elementRef} style={style}>
<Element ref={elementRef} style={style} {...rest}>
{children}
</Element>
);
Expand Down
12 changes: 12 additions & 0 deletions src/vue/VList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import {
ComponentObjectPropsOptions,
ref,
VNode,
PropType,
} from "vue";
import { Virtualizer, VirtualizerHandle } from "./Virtualizer";
import { ItemProps } from './utils';

interface VListHandle extends VirtualizerHandle {}

Expand Down Expand Up @@ -41,6 +43,14 @@ const props = {
* A prop for SSR. If set, the specified amount of items will be mounted in the initial rendering regardless of the container size until hydrated.
*/
ssrCount: Number,
/**
* A function that provides properties/attributes for item element
*/
itemProps: Function as PropType<ItemProps>,
/**
* List of indexes that should be always mounted, even when off screen.
*/
keepMounted: Array as PropType<number[]>,
} satisfies ComponentObjectPropsOptions;

export const VList = /*#__PURE__*/ defineComponent({
Expand Down Expand Up @@ -93,9 +103,11 @@ export const VList = /*#__PURE__*/ defineComponent({
data={props.data}
overscan={props.overscan}
itemSize={props.itemSize}
itemProps={props.itemProps}
shift={props.shift}
ssrCount={props.ssrCount}
horizontal={horizontal}
keepMounted={props.keepMounted}
onScroll={onScroll}
onScrollEnd={onScrollEnd}
>
Expand Down
36 changes: 33 additions & 3 deletions src/vue/Virtualizer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,10 @@ import {
ItemsRange,
ScrollToIndexOpts,
microtask,
sort,
} from "../core";
import { ListItem } from "./ListItem";
import { getKey, isSameRange } from "./utils";
import { getKey, isSameRange, ItemProps } from "./utils";

export interface VirtualizerHandle {
/**
Expand Down Expand Up @@ -127,6 +128,14 @@ const props = {
* @defaultValue "div"
*/
item: { type: String as PropType<keyof NativeElements>, default: "div" },
/**
* A function that provides properties/attributes for item element
*/
itemProps: Function as PropType<ItemProps>,
/**
* List of indexes that should be always mounted, even when off screen.
*/
keepMounted: Array as PropType<number[]>,
} satisfies ComponentObjectPropsOptions;

export const Virtualizer = /*#__PURE__*/ defineComponent({
Expand Down Expand Up @@ -248,9 +257,10 @@ export const Virtualizer = /*#__PURE__*/ defineComponent({
const total = totalSize.value;

const items: VNode[] = [];
for (let i = startIndex, j = endIndex; i <= j; i++) {

function getListItem(i: number) {
const e = slots.default({ item: props.data![i]!, index: i })[0]!;
items.push(
return (
<ListItem
key={getKey(e, i)}
_rerender={rerender}
Expand All @@ -261,9 +271,29 @@ export const Virtualizer = /*#__PURE__*/ defineComponent({
_isHorizontal={isHorizontal}
_isSSR={isSSR}
_as={ItemElement}
_itemProps={props.itemProps?.({ item: props.data![i]!, index: i })}
/>
);
}
for (let i = startIndex, j = endIndex; i <= j; i++) {
items.push(getListItem(i));
}

if (props.keepMounted) {
const startItems: VNode[] = [];
const endItems: VNode[] = [];
sort(props.keepMounted).forEach((index) => {
if (index < startIndex) {
startItems.push(getListItem(index));
}
if (index > endIndex) {
endItems.push(getListItem(index));
}
});

items.unshift(...startItems);
items.push(...endItems);
}

return (
<Element
Expand Down
7 changes: 6 additions & 1 deletion src/vue/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { VNode } from "vue";
import { CSSProperties, VNode } from "vue";
import { ItemsRange } from "../core";

/**
Expand All @@ -15,3 +15,8 @@ export const getKey = (e: VNode, i: number): Exclude<VNode["key"], null> => {
export const isSameRange = (prev: ItemsRange, next: ItemsRange): boolean => {
return prev[0] === next[0] && prev[1] === next[1];
};

export type ItemProps = (payload: {
item: any;
index: number;
}) => { [key: string]: any; style?: CSSProperties; class?: string } | undefined;
65 changes: 65 additions & 0 deletions stories/vue/StickyGroup.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<script setup lang="ts">
import { CSSProperties, ref } from "vue";
import { Virtualizer, VList } from "../../src/vue";
const sizes = [20, 40, 180, 77];
const activeIndex = ref(0);
const data = Array.from({ length: 1000 }).map((_, i) => sizes[i % 4]!);
const itemProps = ({ index }: { index: number }) => {
if (index % 100 === 0)
return {
style: {
...(activeIndex.value === index
? {
position: "sticky",
top: 0,
}
: {}),
zIndex: 1,
} as CSSProperties,
};
return {};
};
const listRef = ref<InstanceType<typeof Virtualizer>>();
function onScroll() {
if (!listRef.value) return;
const start = listRef.value.findStartIndex();
const activeStickyIndex = [0, 100, 200, 300, 400, 500, 600, 700, 800, 900]
.reverse()
.find((index) => start >= index)!;
activeIndex.value = activeStickyIndex;
}
</script>

<template>
<VList
ref="listRef"
:data="data"
:style="{ height: '100vh' }"
#default="{ item, index }"
:item-props="itemProps"
:keep-mounted="[activeIndex]"
@scroll="onScroll"
>
<div
:key="index"
:style="{
height: item + 'px',
background: 'white',
borderBottom: 'solid 1px #ccc',
...(index % 100 === 0
? {
background: 'yellow',
}
: {}),
}"
>
{{ index }}
</div>
</VList>
</template>

<style scoped>
/* NOP */
</style>
8 changes: 8 additions & 0 deletions stories/vue/VList.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { VList } from "../../src/vue";
import DefaultComponent from "./Default.vue";
import HorizontalComponent from "./Horizontal.vue";
import ControllsComponent from "./Controlls.vue";
import StickyGroupComponent from './StickyGroup.vue';

export default {
component: VList,
Expand All @@ -28,3 +29,10 @@ export const Controlls: StoryObj = {
template: "<Component />",
}),
};

export const StickyGroup: StoryObj = {
render: () => ({
components: { Component: StickyGroupComponent },
template: "<Component />",
}),
}

0 comments on commit c01a433

Please sign in to comment.