Skip to content
Open
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
11 changes: 11 additions & 0 deletions apps/demo/src/demos/virtualList/index.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.container {
border: 1px solid black;
width: 502px;
}
.item {
width: 100px;
height: 100px;
background-color: white;
border: 1px solid black;
background-color: aliceblue;
}
45 changes: 45 additions & 0 deletions apps/demo/src/demos/virtualList/virtual-list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { VirtualList } from "@solid-gadgets/components";
import { RendererProps } from "@solid-gadgets/components/src/VirtualList/type";
import { For } from "solid-js";

import "./index.scss";

const List = ({ list }: RendererProps<{ name: string; id: number }>) => {
return (
<For each={list()}>
{item => (
<div class="item">
<div class="avatar"></div>
<div class="name">{item.name}</div>
<div class="id">{item.id}</div>
</div>
)}
</For>
);
};

export default () => {
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
const data = new Array(1000).fill(0).map((_, idx) => ({
name: `leo${idx}`,
id: idx,
}));

return (
<>
<button onclick={() => window.open("/solid-gadgets/src/web-demos/virtualList/index.html")}>
Web component
</button>
<div class="container">
<VirtualList
dataSource={data}
renderer={List}
itemSize={100}
width="500px"
height="600px"
// horizontal
></VirtualList>
</div>
</>
);
};
5 changes: 5 additions & 0 deletions apps/demo/src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,9 @@ export const routes: RouteConfig[] = [
path: "/resizable-layout",
component: lazy(async () => import("./demos/resizableLayout/resizable-layout")),
},
{
name: "Virtual List",
path: "/virtual-list",
component: lazy(async () => import("./demos/virtualList/virtual-list")),
},
];
1 change: 1 addition & 0 deletions apps/demo/src/web-demos/resizableLayout/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ const splitterDoms = document.querySelectorAll("so-splitter");
splitterDoms.forEach(splitter => {
splitter.setAttribute("style-code", styleCode);
});

registerSplitter();
22 changes: 22 additions & 0 deletions apps/demo/src/web-demos/virtualList/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
.container {
border: 1px solid black;
width: 300px;
height: 660px;
}
</style>
<script src="./index.ts" type="module"></script>
</head>
<body>
<main class="container">
<so-virtual-list item-size="100" buffer="2" width="100%" height="100%"> </so-virtual-list>
</main>
</body>
</html>
35 changes: 35 additions & 0 deletions apps/demo/src/web-demos/virtualList/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/* eslint-disable @typescript-eslint/no-magic-numbers */
import { VirtualListElement, registerVirtualList } from "@solid-gadgets/web-components";

registerVirtualList();

const data = new Array(1000).fill(0).map((_, idx) => ({
name: `leo${idx}`,
id: idx,
}));

const virtualListDom =
document.querySelector<VirtualListElement<typeof data[0]>>("so-virtual-list");

virtualListDom?.setAttribute(
"item-style-code",
`.item {
width: 100px;
height: 100px;
background-color: white;
border: 1px solid black;
background-color: aliceblue;
}`
);
virtualListDom?.setData(data);
virtualListDom?.setRenderer(item => {
const itemDom = document.createElement("div");
itemDom.innerHTML = `
<div class="item">
<div class="avatar"></div>
<div class="name">${item.name}</div>
<div class="id">${item.id}</div>
</div>`;

return itemDom;
});
13 changes: 13 additions & 0 deletions packages/components/src/VirtualList/index.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.virtual-list__wrapper {
overflow: auto;
position: relative;
.virtual-list__phantom {
position: relative;
}
.virtual-list__content {
position: absolute;
width: fit-content;
height: fit-content;
top: 0;
}
}
72 changes: 72 additions & 0 deletions packages/components/src/VirtualList/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/* eslint-disable @typescript-eslint/no-magic-numbers */
import { createEffect, createMemo, createSignal } from "solid-js";

import style from "./index.scss";
import { VirtualListProps } from "./type";

export const VirtualList = (props: VirtualListProps) => {
const { itemSize, width = "100%", height = "100%", buffer = 2, horizontal = false } = props;

const dataSource = createMemo(() => props.dataSource);
const Renderer = createMemo(() => props.renderer);

// eslint-disable-next-line @typescript-eslint/init-declarations
let containerRef: HTMLElement | undefined;

const phantomSize = createMemo(() => dataSource().length * itemSize);

const [shiftSize, setShiftSize] = createSignal(0);
const [startIdx, setStartIdx] = createSignal(0);
const [endIdx, setEndIdx] = createSignal(0);

const displayData = createMemo(() => dataSource().slice(startIdx(), endIdx()));

createEffect(() => {
if (!containerRef) return;
const containerSize = Number(getComputedStyle(containerRef).height.replace("px", ""));
const visibleNum = Math.ceil(containerSize / itemSize);

// init the displayed data
setEndIdx(visibleNum + buffer);

containerRef?.addEventListener("scroll", () => {
const scrollSize = horizontal ? containerRef?.scrollLeft : containerRef?.scrollTop;
if (scrollSize == null) return;

const start = Math.floor(scrollSize / itemSize);
const bufferedStart = start - buffer < 0 ? start : start - buffer;
const bufferedEnd =
start + visibleNum + buffer >= dataSource().length
? dataSource().length
: start + visibleNum + buffer;

setStartIdx(bufferedStart);
setEndIdx(bufferedEnd);

setShiftSize(scrollSize - (scrollSize % itemSize) - (start - bufferedStart) * itemSize);
});
});

const wrapperStyle = createMemo(() => ({ width, height }));
const phantomStyle = createMemo(() =>
horizontal ? { width: `${phantomSize()}px` } : { height: `${phantomSize()}px` }
);
const contentStyle = createMemo(() =>
horizontal
? { transform: `translateX(${shiftSize()}px)`, display: "flex" }
: { transform: `translateY(${shiftSize()}px)` }
);

return (
<main class="virtual-list__wrapper" style={wrapperStyle()} ref={containerRef}>
<div class="virtual-list__phantom" style={phantomStyle()}></div>
<div class="virtual-list__content" style={contentStyle()}>
{/* make it compatible with web component */}
{Renderer()({ list: displayData })}
</div>
</main>
);
};

export * from "./type";
export const virtualListStyle = style;
23 changes: 23 additions & 0 deletions packages/components/src/VirtualList/type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Accessor } from "solid-js";
import { JSX } from "solid-js/jsx-runtime";

export interface RendererProps<I = unknown> {
/** pass as a signal function to be reactive */
list: Accessor<I[]>;
}
export interface VirtualListProps {
/** determine how to render each item of the visible items */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
renderer: (props: RendererProps<any>) => JSX.Element;
dataSource: readonly unknown[];
/** scrolling direction, default as vertical */
horizontal?: boolean;
/** px of item height/width */
itemSize: number;
/** width of the virtual list container */
width?: string;
/** height of the virtual list container */
height?: string;
/** the extra buffered item at the top and bottom */
buffer?: number;
}
1 change: 1 addition & 0 deletions packages/components/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from "./ResizableLayout";
export * from "./VirtualList";
86 changes: 86 additions & 0 deletions packages/web-components/src/VirtualList/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import {
RendererProps,
VirtualList,
VirtualListProps,
virtualListStyle,
} from "@solid-gadgets/components";
import { customElement } from "solid-element";
import { For, createMemo } from "solid-js";
import { createStore } from "solid-js/store";

type Stringify<T> = { [key in keyof T]: string };
export type WebVirtualListProps = Stringify<Omit<VirtualListProps, "dataSource" | "renderer">> & {
itemStyleCode?: string;
itemStyleLink?: string;
};

export type Renderer<I = unknown> = (item: I) => HTMLElement;

export interface VirtualListStore {
dataSource: unknown[];
renderer?: Renderer;
}

export type VirtualListElement<I = unknown> = Element & {
setData: (data: I[]) => void;
setRenderer: (itemRenderer: Renderer<I>) => void;
};

const defaultProps: WebVirtualListProps = {
itemSize: "50",
width: "100%",
height: "100%",
buffer: "2",
horizontal: "false",
itemStyleCode: "",
itemStyleLink: "",
};

const createRenderList =
<I = unknown,>(itemRenderer?: Renderer<I>) =>
({ list }: RendererProps<I>) =>
<For each={list()}>{item => itemRenderer?.(item)}</For>;

export const registerVirtualList = () => {
customElement("so-virtual-list", defaultProps, (props: WebVirtualListProps, { element }) => {
const { itemSize, width = "100%", height = "100%", buffer = "2", horizontal = "false" } = props;

const [store, setStore] = createStore<VirtualListStore>({
dataSource: [],
});
element.setData = (data: unknown[]) => {
setStore("dataSource", data);
};
element.setRenderer = (itemRenderer: Renderer) => {
// avoid solid resolving the itemRenderer since it is a function
setStore("renderer", () => itemRenderer);
};

const processedProps = createMemo(() => ({
itemSize: Number(itemSize),
width,
height,
buffer: Number(buffer),
horizontal: horizontal === "true" ? true : false,
}));
const dataSource = createMemo(() => store.dataSource);
const List = createMemo(() => createRenderList(store.renderer));

return (
<>
<style>
{`* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
`}
{virtualListStyle}
{props.itemStyleCode}
</style>
{props.itemStyleLink?.trim() !== "" && <link rel="stylesheet" href={props.itemStyleLink} />}
<VirtualList dataSource={dataSource()} {...processedProps()} renderer={List()} />
</>
);
});
};
1 change: 1 addition & 0 deletions packages/web-components/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from "./ResizableLayout";
export * from "./VirtualList";