-
Notifications
You must be signed in to change notification settings - Fork 124
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
11 changed files
with
743 additions
and
356 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
MIT License | ||
|
||
Copyright (c) 2021 Solid Primitives Working Group | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the "Software"), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is | ||
furnished to do so, subject to the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be included in all | ||
copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
SOFTWARE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
<p> | ||
<img width="100%" src="https://assets.solidjs.com/banner?type=Primitives&background=tiles&project=virtual" alt="Solid Primitives virtual"> | ||
</p> | ||
|
||
# @solid-primitives/virtual | ||
|
||
[![turborepo](https://img.shields.io/badge/built%20with-turborepo-cc00ff.svg?style=for-the-badge&logo=turborepo)](https://turborepo.org/) | ||
[![size](https://img.shields.io/bundlephobia/minzip/@solid-primitives/virtual?style=for-the-badge&label=size)](https://bundlephobia.com/package/@solid-primitives/virtual) | ||
[![version](https://img.shields.io/npm/v/@solid-primitives/virtual?style=for-the-badge)](https://www.npmjs.com/package/@solid-primitives/virtual) | ||
[![stage](https://img.shields.io/endpoint?style=for-the-badge&url=https%3A%2F%2Fraw.githubusercontent.com%2Fsolidjs-community%2Fsolid-primitives%2Fmain%2Fassets%2Fbadges%2Fstage-0.json)](https://github.com/solidjs-community/solid-primitives#contribution-process) | ||
|
||
A basic [virtualized list](https://www.patterns.dev/vanilla/virtual-lists/) component for improving performance when rendering very large lists | ||
|
||
## Installation | ||
|
||
```bash | ||
npm install @solid-primitives/virtual | ||
# or | ||
yarn add @solid-primitives/virtual | ||
# or | ||
pnpm add @solid-primitives/virtual | ||
``` | ||
|
||
## How to use it | ||
|
||
```tsx | ||
<VirtualList | ||
// the list of items (of course, to for this component to be useful, the list would need to be much bigger than shown here) | ||
each={[0, 1, 2, 3, 4, 5, 6, 7]} | ||
// the number of elements to render both before and after the visible section of the list, so passing 5 will render 5 items before the list, and 5 items after. Defaults to 1, cannot be set to zero. This is necessary to hide the blank space around list items when scrolling | ||
overscanCount={5} | ||
// the height of the root element of the virtualizedList itself | ||
rootHeight={20} | ||
// the height of individual rows in the virtualizedList | ||
rowHeight={10} | ||
// the class applied to the root element of the virtualizedList | ||
class={"my-class-name"} | ||
> | ||
{ | ||
// the flowComponent that will be used to transform the items into rows in the list | ||
item => <div>{item}</div> | ||
} | ||
</VirtualList> | ||
``` | ||
|
||
The tests describe the component's exact behavior and how overscanCount handles the start/end of the list in more detail. | ||
Note that the component only handles vertical lists where the number of items is known and the height of an individual item is fixed. | ||
|
||
## Demo | ||
|
||
You can see the VirtualizedList in action in the following sandbox: https://primitives.solidjs.community/playground/virtual | ||
|
||
## Changelog | ||
|
||
See [CHANGELOG.md](./CHANGELOG.md) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
import { createSignal, onMount, onCleanup } from "solid-js"; | ||
import type { Component } from "solid-js"; | ||
import { isServer } from "solid-js/web"; | ||
import { VirtualList } from "../src/index.jsx"; | ||
|
||
const intl = new Intl.NumberFormat(); | ||
|
||
const items = new Array(100_000).fill(0).map((_, i) => i); | ||
|
||
const clampRange = (min: number, max: number, v: number) => (v < min ? min : v > max ? max : v); | ||
|
||
const App: Component = () => { | ||
const [listLength, setListLength] = createSignal(100_000); | ||
const [overscanCount, setOverscanCount] = createSignal(5); | ||
const [rootHeight, setRootHeight] = createSignal(240); | ||
const [rowHeight, setRowHeight] = createSignal(24); | ||
|
||
return ( | ||
<div class="box-border flex min-h-screen w-full flex-col space-y-4 bg-gray-800 p-24 text-white"> | ||
<div class="grid w-full grid-cols-4 bg-white p-4 text-gray-800 shadow-md"> | ||
<div> | ||
<DemoControl | ||
label="Number of rows" | ||
max={100_000} | ||
min={1} | ||
name="rowCount" | ||
setValue={setListLength} | ||
value={listLength()} | ||
/> | ||
</div> | ||
<div> | ||
<DemoControl | ||
label="overscanCount" | ||
max={100} | ||
min={1} | ||
name="overscanCount" | ||
setValue={setOverscanCount} | ||
value={overscanCount()} | ||
/> | ||
</div> | ||
<div> | ||
<DemoControl | ||
label="rootHeight" | ||
max={1_000} | ||
min={100} | ||
name="rootHeight" | ||
setValue={setRootHeight} | ||
value={rootHeight()} | ||
/> | ||
</div> | ||
<div> | ||
<DemoControl | ||
label="rowHeight" | ||
max={100} | ||
min={10} | ||
name="rowHeight" | ||
setValue={setRowHeight} | ||
value={rowHeight()} | ||
/> | ||
</div> | ||
</div> | ||
|
||
<div class="w-full space-y-2 bg-white p-4 text-gray-800 shadow-md"> | ||
View the devtools console for log of items being added and removed from the visible list | ||
</div> | ||
|
||
<VirtualList | ||
each={items.slice(0, listLength())} | ||
overscanCount={overscanCount()} | ||
rootHeight={rootHeight()} | ||
rowHeight={rowHeight()} | ||
class="bg-white text-gray-800" | ||
> | ||
{item => <VirtualListItem item={item} height={rowHeight()} />} | ||
</VirtualList> | ||
</div> | ||
); | ||
}; | ||
|
||
type DemoControlProps = { | ||
label: string; | ||
max: number; | ||
min: number; | ||
name: string; | ||
setValue: (v: number) => void; | ||
value: number; | ||
}; | ||
|
||
const DemoControl: Component<DemoControlProps> = props => ( | ||
<label> | ||
<div class="text-s">{props.label}:</div> | ||
<input | ||
type="number" | ||
name={props.name} | ||
value={props.value} | ||
min={props.min} | ||
max={props.max} | ||
class="mx-2 w-32" | ||
onInput={e => { | ||
props.setValue(clampRange(props.min, props.max, Number(e.target.value))); | ||
}} | ||
/> | ||
<div class="text-xs"> | ||
({intl.format(props.min)} min, {intl.format(props.max)} max) | ||
</div> | ||
</label> | ||
); | ||
|
||
type VirtualListItemProps = { | ||
item: number; | ||
height: number; | ||
}; | ||
|
||
const VirtualListItem: Component<VirtualListItemProps> = props => { | ||
onMount(() => { | ||
if (!isServer) console.log("item added:", props.item); | ||
}); | ||
|
||
onCleanup(() => { | ||
if (!isServer) console.log("item removed::", props.item); | ||
}); | ||
|
||
return ( | ||
<div style={{ height: `${props.height}px` }} class="align-center flex"> | ||
{intl.format(props.item)} | ||
</div> | ||
); | ||
}; | ||
|
||
export default App; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
{ | ||
"name": "@solid-primitives/virtual", | ||
"version": "0.0.1", | ||
"description": "A virtualized list component for performantly rendering lists with many elements", | ||
"author": "Spencer Whitehead <[email protected]>", | ||
"contributors": [], | ||
"license": "MIT", | ||
"homepage": "https://primitives.solidjs.community/package/virtual", | ||
"repository": { | ||
"type": "git", | ||
"url": "git+https://github.com/solidjs-community/solid-primitives.git" | ||
}, | ||
"bugs": { | ||
"url": "https://github.com/solidjs-community/solid-primitives/issues" | ||
}, | ||
"primitive": { | ||
"name": "virtual", | ||
"stage": 0, | ||
"list": [ | ||
"VirtualList" | ||
], | ||
"category": "Display & Media" | ||
}, | ||
"keywords": [ | ||
"solid", | ||
"primitives", | ||
"virtualized" | ||
], | ||
"private": false, | ||
"sideEffects": false, | ||
"files": [ | ||
"dist" | ||
], | ||
"type": "module", | ||
"main": "./dist/index.cjs", | ||
"module": "./dist/index.js", | ||
"types": "./dist/index.d.ts", | ||
"browser": {}, | ||
"exports": { | ||
"import": { | ||
"types": "./dist/index.d.ts", | ||
"default": "./dist/index.js" | ||
}, | ||
"require": { | ||
"types": "./dist/index.d.cts", | ||
"default": "./dist/index.cjs" | ||
} | ||
}, | ||
"typesVersions": {}, | ||
"scripts": { | ||
"dev": "tsx ../../scripts/dev.ts", | ||
"build": "tsx ../../scripts/build.ts", | ||
"vitest": "vitest -c ../../configs/vitest.config.ts", | ||
"test": "pnpm run vitest", | ||
"test:ssr": "pnpm run vitest --mode ssr" | ||
}, | ||
"peerDependencies": { | ||
"solid-js": "^1.6.12" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import { For, createSignal } from "solid-js"; | ||
import type { Accessor, JSX } from "solid-js"; | ||
|
||
/** | ||
* A basic virtualized list (see https://www.patterns.dev/vanilla/virtual-lists/) component for improving performance when rendering very large lists | ||
* | ||
* @param children the flowComponent that will be used to transform the items into rows in the list | ||
* @param class the class applied to the root element of the virtualizedList | ||
* @param each the list of items | ||
* @param overscanCount the number of elements to render both before and after the visible section of the list, so passing 5 will render 5 items before the list, and 5 items after. Defaults to 1, cannot be set to zero. This is necessary to hide the blank space around list items when scrolling | ||
* @param rootHeight the height of the root element of the virtualizedList itself | ||
* @param rowHeight the height of individual rows in the virtualizedList | ||
* @return virtualized list component | ||
*/ | ||
export function VirtualList<T extends readonly any[], U extends JSX.Element>(props: { | ||
children: (item: T[number], index: Accessor<number>) => U; | ||
fallback?: JSX.Element; | ||
class?: string; | ||
each: T | undefined | null | false; | ||
overscanCount?: number; | ||
rootHeight: number; | ||
rowHeight: number; | ||
}): JSX.Element { | ||
let rootElement!: HTMLDivElement; | ||
|
||
const [offset, setOffset] = createSignal(0); | ||
const items = () => props.each || ([] as any as T) | ||
|
||
const getFirstIdx = () => | ||
Math.max(0, Math.floor(offset() / props.rowHeight) - (props.overscanCount || 1)); | ||
|
||
const getLastIdx = () => | ||
Math.min( | ||
items().length, | ||
Math.floor(offset() / props.rowHeight) + | ||
Math.ceil(props.rootHeight / props.rowHeight) + | ||
(props.overscanCount || 1), | ||
); | ||
|
||
return ( | ||
<div | ||
ref={rootElement} | ||
style={{ | ||
overflow: "auto", | ||
height: `${props.rootHeight}px`, | ||
}} | ||
class={props.class} | ||
onScroll={() => { | ||
setOffset(rootElement.scrollTop); | ||
}} | ||
> | ||
<div | ||
style={{ | ||
position: "relative", | ||
width: "100%", | ||
height: `${items().length * props.rowHeight}px`, | ||
}} | ||
> | ||
<div | ||
style={{ | ||
position: "absolute", | ||
top: `${getFirstIdx() * props.rowHeight}px`, | ||
}} | ||
> | ||
<For fallback={props.fallback} each={items().slice(getFirstIdx(), getLastIdx()) as any as T}>{props.children}</For> | ||
</div> | ||
</div> | ||
</div> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import { Component } from "solid-js"; | ||
|
||
export const TEST_LIST = new Array(1_000).fill(undefined).map((_, i) => i); | ||
|
||
type VirtualListItemProps = { | ||
item: number; | ||
}; | ||
|
||
export const VirtualListItem: Component<VirtualListItemProps> = props => ( | ||
<div style={{ height: "100px" }}>{props.item}</div> | ||
); |
Oops, something went wrong.