diff --git a/src/solid/ListItem.tsx b/src/solid/ListItem.tsx index 63b760d4..716579e4 100644 --- a/src/solid/ListItem.tsx +++ b/src/solid/ListItem.tsx @@ -4,8 +4,8 @@ import { ItemResizeObserver } from "../core/resizer"; import { isRTLDocument } from "../core/environment"; import { - Component, - JSX, + type Component, + type JSX, createEffect, createMemo, mergeProps, @@ -29,19 +29,16 @@ interface ListItemProps { */ export const ListItem: Component = (props) => { let elementRef: HTMLDivElement | undefined; - props = mergeProps<[Partial, ListItemProps]>( - { _as: "div" }, - props - ); + const mergedProps = mergeProps({ _as: "div" }, props); - // The index may be changed if elements are inserted to or removed from the start of props.children + // The index may be changed if elements are inserted to or removed from the start of mergedProps.children createEffect(() => { if (!elementRef) return; - onCleanup(props._resizer(elementRef, props._index)); + onCleanup(mergedProps._resizer(elementRef, mergedProps._index)); }); const style = createMemo(() => { - const isHorizontal = props._isHorizontal; + const isHorizontal = mergedProps._isHorizontal; const style: JSX.CSSProperties = { margin: 0, padding: 0, @@ -49,8 +46,8 @@ export const ListItem: Component = (props) => { [isHorizontal ? "height" : "width"]: "100%", [isHorizontal ? "top" : "left"]: "0px", [isHorizontal ? (isRTLDocument() ? "right" : "left") : "top"]: - props._offset + "px", - visibility: props._hide ? "hidden" : "visible", + mergedProps._offset + "px", + visibility: mergedProps._hide ? "hidden" : "visible", }; if (isHorizontal) { style.display = "flex"; @@ -59,8 +56,8 @@ export const ListItem: Component = (props) => { }); return ( - - {props._children} + + {mergedProps._children} ); }; diff --git a/src/solid/RangedFor.tsx b/src/solid/RangedFor.tsx index 2cf168a2..a4783f47 100644 --- a/src/solid/RangedFor.tsx +++ b/src/solid/RangedFor.tsx @@ -6,11 +6,11 @@ import { createRoot, createSignal, onCleanup, - JSX, - Signal, - Accessor, + type JSX, + type Signal, + type Accessor, } from "solid-js"; -import { ItemsRange } from "../core/types"; +import type { ItemsRange } from "../core/types"; interface RenderedNode { _data: Signal; @@ -58,12 +58,12 @@ export const RangedFor = (props: { _dispose: dispose, }); return result; - }) + }), ); if (lookup) { if (newData !== lookup._data) { lookup._data[1]( - newData as Exclude // TODO improve type + newData as Exclude, // TODO improve type ); } current.set(i, lookup); diff --git a/src/solid/VList.tsx b/src/solid/VList.tsx index 6f53e6e6..d56a3d90 100644 --- a/src/solid/VList.tsx +++ b/src/solid/VList.tsx @@ -1,12 +1,12 @@ /** * @jsxImportSource solid-js */ -import { JSX } from "solid-js"; -import { ViewportComponentAttributes } from "./types"; +import { type JSX, splitProps } from "solid-js"; +import type { ViewportComponentAttributes } from "./types"; import { Virtualizer, - VirtualizerHandle, - VirtualizerProps, + type VirtualizerHandle, + type VirtualizerProps, } from "./Virtualizer"; /** @@ -37,45 +37,44 @@ export interface VListProps * Virtualized list component. See {@link VListProps} and {@link VListHandle}. */ export const VList = (props: VListProps): JSX.Element => { - const { - ref, - data, - children, - overscan, - itemSize, - shift, - horizontal, - onScroll, - onScrollEnd, - onRangeChange, - style, - ...attrs - } = props; + const [local, ...attrs] = splitProps(props, [ + "ref", + "data", + "children", + "overscan", + "itemSize", + "shift", + "horizontal", + "onScroll", + "onScrollEnd", + "onRangeChange", + "style", + ]); return (
- {props.children} + {local.children}
); diff --git a/src/solid/Virtualizer.tsx b/src/solid/Virtualizer.tsx index ac07ddd2..1390d6db 100644 --- a/src/solid/Virtualizer.tsx +++ b/src/solid/Virtualizer.tsx @@ -7,11 +7,12 @@ import { createEffect, createSignal, createMemo, - JSX, + type JSX, on, createComputed, type ValidComponent, mergeProps, + createRoot, } from "solid-js"; import { Dynamic } from "solid-js/web"; import { @@ -27,7 +28,7 @@ import { } from "../core/store"; import { createResizer } from "../core/resizer"; import { createScroller } from "../core/scroller"; -import { ItemsRange, ScrollToIndexOpts } from "../core/types"; +import type { ItemsRange, ScrollToIndexOpts } from "../core/types"; import { ListItem } from "./ListItem"; import { RangedFor } from "./RangedFor"; import { isSameRange } from "./utils"; @@ -145,7 +146,7 @@ export interface VirtualizerProps { /** * The end index of viewable items. */ - endIndex: number + endIndex: number, ) => void; } @@ -154,62 +155,74 @@ export interface VirtualizerProps { */ export const Virtualizer = (props: VirtualizerProps): JSX.Element => { let containerRef: HTMLDivElement | undefined; - const { itemSize, horizontal = false } = props; - props = mergeProps<[Partial>, VirtualizerProps]>( - { as: "div" }, - props - ); + const mergedProps = mergeProps({ as: "div", horizontal: false }, props); - const store = createVirtualStore( - props.data.length, - itemSize ?? 40, - undefined, - undefined, - !itemSize + const store = createMemo(() => { + return createVirtualStore( + mergedProps.data.length, + mergedProps.itemSize ?? 40, + undefined, + undefined, + !mergedProps.itemSize, + ); + }); + const resizer = createMemo(() => + createResizer(store(), mergedProps.horizontal), + ); + const scroller = createMemo(() => + createScroller(store(), mergedProps.horizontal), ); - const resizer = createResizer(store, horizontal); - const scroller = createScroller(store, horizontal); - - const [rerender, setRerender] = createSignal(store._getStateVersion()); - const unsubscribeStore = store._subscribe(UPDATE_VIRTUAL_STATE, () => { - setRerender(store._getStateVersion()); + const [rerender, setRerender] = createSignal(); + createEffect(() => { + setRerender(store()._getStateVersion()); }); - const unsubscribeOnScroll = store._subscribe(UPDATE_SCROLL_EVENT, () => { - props.onScroll?.(store._getScrollOffset()); - }); - const unsubscribeOnScrollEnd = store._subscribe( - UPDATE_SCROLL_END_EVENT, - () => { - props.onScrollEnd?.(); - } - ); + const { unsubscribeStore, unsubscribeOnScroll, unsubscribeOnScrollEnd } = + createRoot(() => { + return { + unsubscribeStore: store()._subscribe(UPDATE_VIRTUAL_STATE, () => { + createEffect(() => { + setRerender(store()._getStateVersion()); + }); + }), + unsubscribeOnScroll: store()._subscribe(UPDATE_SCROLL_EVENT, () => { + createEffect(() => { + mergedProps.onScroll?.(store()._getScrollOffset()); + }); + }), + unsubscribeOnScrollEnd: store()._subscribe( + UPDATE_SCROLL_END_EVENT, + () => { + createEffect(() => { + mergedProps.onScrollEnd?.(); + }); + }, + ), + }; + }); const range = createMemo((prev) => { rerender(); - const next = store._getRange(); + const next = store()._getRange(); if (prev && isSameRange(prev, next)) { return prev; } return next; }); - const scrollDirection = createMemo( - () => rerender() && store._getScrollDirection() - ); - const totalSize = createMemo(() => rerender() && store._getTotalSize()); + const totalSize = createMemo(() => rerender() && store()._getTotalSize()); - const jumpCount = createMemo(() => rerender() && store._getJumpCount()); + const jumpCount = createMemo(() => rerender() && store()._getJumpCount()); const overscanedRange = createMemo((prev) => { - const overscan = props.overscan ?? 4; + const overscan = mergedProps.overscan ?? 4; const [startIndex, endIndex] = range(); const next = getOverscanedRange( startIndex, endIndex, overscan, - scrollDirection(), - props.data.length + store()._getScrollDirection(), + mergedProps.data.length, ); if (prev && isSameRange(prev, next)) { return prev; @@ -218,77 +231,80 @@ export const Virtualizer = (props: VirtualizerProps): JSX.Element => { }); onMount(() => { - if (props.ref) { - props.ref({ + if (mergedProps.ref) { + mergedProps.ref({ get scrollOffset() { - return store._getScrollOffset(); + return store()._getScrollOffset(); }, get scrollSize() { - return getScrollSize(store); + return getScrollSize(store()); }, get viewportSize() { - return store._getViewportSize(); + return store()._getViewportSize(); }, - getItemOffset: store._getItemOffset, - scrollToIndex: scroller._scrollToIndex, - scrollTo: scroller._scrollTo, - scrollBy: scroller._scrollBy, + getItemOffset: store()._getItemOffset, + scrollToIndex: scroller()._scrollToIndex, + scrollTo: scroller()._scrollTo, + scrollBy: scroller()._scrollBy, }); } - const scrollable = props.scrollRef || containerRef!.parentElement!; - resizer._observeRoot(scrollable); - scroller._observe(scrollable); + const scrollable = mergedProps.scrollRef || containerRef!.parentElement!; + resizer()._observeRoot(scrollable); + scroller()._observe(scrollable); onCleanup(() => { - if (props.ref) { - props.ref(); + if (mergedProps.ref) { + mergedProps.ref(); } unsubscribeStore(); unsubscribeOnScroll(); unsubscribeOnScrollEnd(); - resizer._dispose(); - scroller._dispose(); + resizer()._dispose(); + scroller()._dispose(); }); }); createComputed( on( - () => props.data.length, + () => mergedProps.data.length, (count) => { - if (count !== store._getItemsLength()) { - store._update(ACTION_ITEMS_LENGTH_CHANGE, [count, props.shift]); + if (count !== store()._getItemsLength()) { + store()._update(ACTION_ITEMS_LENGTH_CHANGE, [ + count, + mergedProps.shift, + ]); } - } - ) + }, + ), ); createComputed( on( - () => props.startMargin || 0, + () => mergedProps.startMargin || 0, (value) => { - if (value !== store._getStartSpacerSize()) { - store._update(ACTION_START_OFFSET_CHANGE, value); + if (value !== store()._getStartSpacerSize()) { + store()._update(ACTION_START_OFFSET_CHANGE, value); } - } - ) + }, + ), ); createEffect( on(jumpCount, () => { - scroller._fixScrollJump(); - }) + scroller()._fixScrollJump(); + }), ); createEffect(() => { const next = range(); - props.onRangeChange && props.onRangeChange(next[0], next[1]); + mergedProps.onRangeChange && mergedProps.onRangeChange(next[0], next[1]); }); return ( (props: VirtualizerProps): JSX.Element => { flex: "none", // flex style can break layout position: "relative", visibility: "hidden", // TODO replace with other optimization methods - width: horizontal ? totalSize() + "px" : "100%", - height: horizontal ? "100%" : totalSize() + "px", - "pointer-events": scrollDirection() !== SCROLL_IDLE ? "none" : "auto", + width: mergedProps.horizontal ? totalSize() + "px" : "100%", + height: mergedProps.horizontal ? "100%" : totalSize() + "px", + "pointer-events": + store()._getScrollOffset() !== SCROLL_IDLE ? "none" : "auto", }} > { const offset = createMemo(() => { rerender(); - return store._getItemOffset(index); + return store()._getItemOffset(index); }); const hide = createMemo(() => { rerender(); - return store._isUnmeasuredItem(index); + return store()._isUnmeasuredItem(index); }); return ( ); }} diff --git a/src/solid/WindowVirtualizer.tsx b/src/solid/WindowVirtualizer.tsx index 1e40e109..65e23a31 100644 --- a/src/solid/WindowVirtualizer.tsx +++ b/src/solid/WindowVirtualizer.tsx @@ -7,9 +7,11 @@ import { createEffect, createSignal, createMemo, - JSX, + type JSX, on, createComputed, + mergeProps, + createRoot, } from "solid-js"; import { SCROLL_IDLE, @@ -24,7 +26,7 @@ import { createWindowScroller } from "../core/scroller"; import { ListItem } from "./ListItem"; import { RangedFor } from "./RangedFor"; import { isSameRange } from "./utils"; -import { ItemsRange } from "../core/types"; +import type { ItemsRange } from "../core/types"; // /** // * Methods of {@link WindowVirtualizer}. @@ -82,7 +84,7 @@ export interface WindowVirtualizerProps { /** * The end index of viewable items. */ - endIndex: number + endIndex: number, ) => void; } @@ -90,59 +92,61 @@ export interface WindowVirtualizerProps { * {@link Virtualizer} controlled by the window scrolling. See {@link WindowVirtualizerProps} and {@link WindowVirtualizer}. */ export const WindowVirtualizer = ( - props: WindowVirtualizerProps + props: WindowVirtualizerProps, ): JSX.Element => { let containerRef: HTMLDivElement | undefined; - const { - // ref: _ref, - data: _data, - children: _children, - overscan: _overscan, - itemSize, - shift: _shift, - horizontal = false, - onScrollEnd: _onScrollEnd, - onRangeChange: _onRangeChange, - } = props; - - const store = createVirtualStore( - props.data.length, - itemSize ?? 40, - undefined, - undefined, - !itemSize + const mergedProps = mergeProps({ horizontal: false }, props); + const store = createMemo(() => + createVirtualStore( + mergedProps.data.length, + mergedProps.itemSize ?? 40, + undefined, + undefined, + !mergedProps.itemSize, + ), + ); + const resizer = createMemo(() => + createWindowResizer(store(), mergedProps.horizontal), + ); + const scroller = createMemo(() => + createWindowScroller(store(), mergedProps.horizontal), ); - const resizer = createWindowResizer(store, horizontal); - const scroller = createWindowScroller(store, horizontal); - - const [rerender, setRerender] = createSignal(store._getStateVersion()); - const unsubscribeStore = store._subscribe(UPDATE_VIRTUAL_STATE, () => { - setRerender(store._getStateVersion()); + const [rerender, setRerender] = createSignal(); + createEffect(() => { + setRerender(store()._getStateVersion()); }); - const unsubscribeOnScrollEnd = store._subscribe( - UPDATE_SCROLL_END_EVENT, - () => { - props.onScrollEnd?.(); - } - ); + const { unsubscribeStore, unsubscribeOnScrollEnd } = createRoot(() => { + return { + unsubscribeStore: store()._subscribe(UPDATE_VIRTUAL_STATE, () => { + createEffect(() => { + setRerender(store()._getStateVersion()); + }); + }), + unsubscribeOnScrollEnd: store()._subscribe( + UPDATE_SCROLL_END_EVENT, + () => { + createEffect(() => { + mergedProps.onScrollEnd?.(); + }); + }, + ), + }; + }); const range = createMemo((prev) => { rerender(); - const next = store._getRange(); + const next = store()._getRange(); if (prev && isSameRange(prev, next)) { return prev; } return next; }); - const scrollDirection = createMemo( - () => rerender() && store._getScrollDirection() - ); - const totalSize = createMemo(() => rerender() && store._getTotalSize()); + const totalSize = createMemo(() => rerender() && store()._getTotalSize()); - const jumpCount = createMemo(() => rerender() && store._getJumpCount()); + const jumpCount = createMemo(() => rerender() && store()._getJumpCount()); const overscanedRange = createMemo((prev) => { const overscan = props.overscan ?? 4; @@ -151,8 +155,8 @@ export const WindowVirtualizer = ( startIndex, endIndex, overscan, - scrollDirection(), - props.data.length + store()._getScrollDirection(), + props.data.length, ); if (prev && isSameRange(prev, next)) { return prev; @@ -161,14 +165,14 @@ export const WindowVirtualizer = ( }); onMount(() => { - resizer._observeRoot(containerRef!); - scroller._observe(containerRef!); + resizer()._observeRoot(containerRef!); + scroller()._observe(containerRef!); onCleanup(() => { unsubscribeStore(); unsubscribeOnScrollEnd(); - resizer._dispose(); - scroller._dispose(); + resizer()._dispose(); + scroller()._dispose(); }); }); @@ -176,22 +180,22 @@ export const WindowVirtualizer = ( on( () => props.data.length, (len) => { - if (len !== store._getItemsLength()) { - store._update(ACTION_ITEMS_LENGTH_CHANGE, [len, props.shift]); + if (len !== store()._getItemsLength()) { + store()._update(ACTION_ITEMS_LENGTH_CHANGE, [len, props.shift]); } - } - ) + }, + ), ); createEffect( on(jumpCount, () => { - scroller._fixScrollJump(); - }) + scroller()._fixScrollJump(); + }), ); createEffect(() => { const next = range(); - props.onRangeChange && props.onRangeChange(next[0], next[1]); + mergedProps.onRangeChange && mergedProps.onRangeChange(next[0], next[1]); }); return ( @@ -203,9 +207,10 @@ export const WindowVirtualizer = ( flex: "none", // flex style can break layout position: "relative", visibility: "hidden", // TODO replace with other optimization methods - width: horizontal ? totalSize() + "px" : "100%", - height: horizontal ? "100%" : totalSize() + "px", - "pointer-events": scrollDirection() !== SCROLL_IDLE ? "none" : "auto", + width: mergedProps.horizontal ? totalSize() + "px" : "100%", + height: mergedProps.horizontal ? "100%" : totalSize() + "px", + "pointer-events": + store()._getScrollDirection() !== SCROLL_IDLE ? "none" : "auto", }} > ( _render={(data, index) => { const offset = createMemo(() => { rerender(); - return store._getItemOffset(index); + return store()._getItemOffset(index); }); const hide = createMemo(() => { rerender(); - return store._isUnmeasuredItem(index); + return store()._isUnmeasuredItem(index); }); return ( ); }}