Skip to content
This repository was archived by the owner on Nov 5, 2023. It is now read-only.

feature: support scrolling to a specific item by index #65

Merged
merged 2 commits into from
Jun 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,20 @@ npm install vue-virtual-scroll-grid

## Available Props

| Name | Description | Type | Validation |
|----------------|---------------------------------------------------------------------------|----------------------------------------------------------------|-------------------------------------------------|
| `length` | The number of items in the list | `number` | Required, an integer greater than or equal to 0 |
| `pageProvider` | The callback that returns a page of items as a promise | `(pageNumber: number, pageSize: number) => Promise<unknown[]>` | Required |
| `pageSize` | The number of items in a page from the item provider (e.g. a backend API) | `number` | Required, an integer greater than or equal to 1 |
| Name | Description | Type | Validation |
|----------------|---------------------------------------------------------------------------|----------------------------------------------------------------|---------------------------------------------------------------------------------|
| `length` | The number of items in the list | `number` | Required, an integer greater than or equal to 0 |
| `pageProvider` | The callback that returns a page of items as a promise | `(pageNumber: number, pageSize: number) => Promise<unknown[]>` | Required |
| `pageSize` | The number of items in a page from the item provider (e.g. a backend API) | `number` | Required, an integer greater than or equal to 1 |
| `scrollTo` | Scroll to a specific item by index | `number` | Optional, an integer from 0 to the `length` prop - 1 |

Example:

```vue
<Grid :length="1000"
:pageProvider="async (pageNumber, pageSize) => Array(pageSize).fill('x')"
:pageSize="40"
:scrollTo="10"
>
<!-- ...slots -->
</Grid>
Expand Down
19 changes: 18 additions & 1 deletion src/Grid.vue
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,15 @@
</template>

<script lang="ts">
import { defineComponent, PropType, ref } from "vue";
import { defineComponent, onUpdated, PropType, ref } from "vue";
import {
fromProp,
fromResizeObserver,
fromWindowScroll,
useObservable,
} from "./utilites";
import { PageProvider, pipeline } from "./pipeline";
import { once } from "ramda";

export default defineComponent({
name: "Grid",
Expand All @@ -69,6 +70,12 @@ export default defineComponent({
required: true,
validator: (value: number) => Number.isInteger(value) && value >= 1,
},
// Scroll to a specific item by index, must be less than the length prop
scrollTo: {
type: Number as PropType<number>,
required: false,
validator: (value: number) => Number.isInteger(value) && value >= 0,
},
},
setup(props) {
// template refs
Expand All @@ -79,6 +86,7 @@ export default defineComponent({
const {
buffer$, // the items in the current scanning window
contentHeight$, // the height of the whole list
windowScrollTo$, // the value sent to window.scrollTo()
} = pipeline({
// streams of prop
length$: fromProp(props, "length"),
Expand All @@ -90,8 +98,17 @@ export default defineComponent({
rootResize$: fromResizeObserver(rootRef, "target"),
// a stream of root elements when scrolling
scroll$: fromWindowScroll(rootRef),
scrollTo$: fromProp(props, "scrollTo"),
});

onUpdated(
once(() => {
windowScrollTo$.subscribe((next) => {
window.scrollTo({ top: next, behavior: "smooth" });
});
})
);

return {
rootRef,
probeRef,
Expand Down
4 changes: 3 additions & 1 deletion src/demo/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
:length="length"
:pageSize="pageSize"
:pageProvider="pageProvider"
:scrollTo="scrollTo"
:class="$style.grid"
>
<template v-slot:probe>
Expand Down Expand Up @@ -42,7 +43,7 @@ import Grid from "../Grid.vue";
import Header from "./Header.vue";
import Control from "./Control.vue";
import ProductItem from "./ProductItem.vue";
import { length, pageSize, pageProvider } from "./store";
import { length, pageSize, pageProvider, scrollTo } from "./store";

export default defineComponent({
name: "App",
Expand All @@ -51,6 +52,7 @@ export default defineComponent({
length,
pageSize,
pageProvider,
scrollTo,
}),
});
</script>
Expand Down
43 changes: 33 additions & 10 deletions src/demo/Control.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<div :class="$style.root">
<div :class="$style.length">
<label for="length" :class="$style.rangeLabel">
<label for="length" :class="$style.label">
Item Count: {{ length }}
</label>
<input
Expand All @@ -16,7 +16,7 @@
</div>

<div :class="$style.pageSize">
<label for="pageSize" :class="$style.rangeLabel">
<label for="pageSize" :class="$style.label">
Items Per Page: {{ pageSize }}
</label>
<input
Expand Down Expand Up @@ -67,17 +67,29 @@
</label>
</div>
</div>

<div :class="$style.scrollTo">
<label for="pageSize" :class="$style.label"> Scroll To: </label>
<input
type="number"
id="scrollTo"
min="0"
max="1000"
v-model.number="scrollTo"
:class="$style.number"
/>
</div>
</div>
</template>

<script lang="ts">
import { defineComponent } from "vue";
import { collection, length, pageSize } from "./store";
import { collection, length, pageSize, scrollTo } from "./store";

export default defineComponent({
name: "Control",
setup: () => {
return { length, pageSize, collection };
return { length, pageSize, collection, scrollTo };
},
});
</script>
Expand All @@ -96,6 +108,7 @@ export default defineComponent({
grid-template:
"length pageProvider" auto
"pageSize pageProvider" auto
"scrollTo pageProvider" auto
/ 2fr 1fr;
place-items: center stretch;
grid-gap: 1.5rem;
Expand All @@ -117,6 +130,10 @@ export default defineComponent({
justify-content: flex-start;
}

.scrollTo {
grid-area: scrollTo;
}

.radioList {
flex: 1 1 auto;
display: flex;
Expand All @@ -140,7 +157,16 @@ export default defineComponent({
width: 100%;
}

.rangeLabel {
.number {
background-color: var(--color-rice);
width: 100%;
border: 1px solid var(--color-black);
padding: 5px;
font-size: 1.4rem;
color: var(--color-black);
}

.label {
margin-bottom: 0.5rem;
font-weight: 700;
}
Expand All @@ -153,11 +179,8 @@ export default defineComponent({
@media (min-width: 760px) {
.root {
grid-template:
"length pageSize pageProvider" auto
/ 1fr 1fr 1fr;
}

.pageProvider {
"length pageSize pageProvider scrollTo" auto
/ 2fr 2fr 2fr 1fr;
}

.category {
Expand Down
1 change: 1 addition & 0 deletions src/demo/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { curry, prop } from "ramda";

export const length = ref<number>(1000);
export const pageSize = ref<number>(40);
export const scrollTo = ref<number | undefined>(undefined);

export type Collection = "" | "all-mens" | "womens-view-all";
export const collection = ref<Collection>("");
Expand Down
30 changes: 28 additions & 2 deletions src/pipeline.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,28 @@
import { combineLatest, merge, Observable, range } from "rxjs";
import { combineLatest, merge, Observable, of, range } from "rxjs";
import {
distinct,
distinctUntilChanged,
filter,
map,
mergeMap,
scan,
shareReplay,
switchMap,
take,
withLatestFrom,
} from "rxjs/operators";
import {
__,
addIndex,
apply,
complement,
concat,
difference,
equals,
identity,
ifElse,
insertAll,
isNil,
map as ramdaMap,
pipe,
remove,
Expand Down Expand Up @@ -221,11 +225,13 @@ interface PipelineInput {
itemRect$: Observable<DOMRectReadOnly>;
rootResize$: Observable<Element>;
scroll$: Observable<Element>;
scrollTo$: Observable<number | undefined>;
}

interface PipelineOutput {
buffer$: Observable<InternalItem[]>;
contentHeight$: Observable<number>;
windowScrollTo$: Observable<number>;
}

export function pipeline({
Expand All @@ -235,6 +241,7 @@ export function pipeline({
itemRect$,
rootResize$,
scroll$,
scrollTo$,
}: PipelineInput): PipelineOutput {
// region: measurements of the visual grid
const heightAboveWindow$: Observable<number> = merge(
Expand All @@ -253,6 +260,25 @@ export function pipeline({
);
// endregion

// region: scroll to a given item by index
const windowScrollTo$ = scrollTo$.pipe(
filter(complement(isNil)),
switchMap((scrollTo) =>
combineLatest([of(scrollTo), resizeMeasurement$, rootResize$]).pipe(
take(1)
)
),
map(
([scrollTo, { columns, itemHeightWithGap }, rootEl]) =>
// The offset within the grid
Math.floor(scrollTo / columns) * itemHeightWithGap +
// The offset of grid root to the document
(rootEl.getBoundingClientRect().top +
document.documentElement.scrollTop)
)
);
// endregion

// region: rendering buffer
const bufferMeta$: Observable<BufferMeta> = combineLatest(
[heightAboveWindow$, resizeMeasurement$],
Expand Down Expand Up @@ -285,5 +311,5 @@ export function pipeline({
).pipe(scan(accumulateBuffer, []));
// endregion

return { buffer$, contentHeight$ };
return { buffer$, contentHeight$, windowScrollTo$ };
}