From a0a05cffd207fa508b77099c6636d278b15a1419 Mon Sep 17 00:00:00 2001 From: Nikita Vakula Date: Sun, 24 Dec 2023 23:52:37 +0100 Subject: [PATCH 1/4] Add rect to the state Signed-off-by: Nikita Vakula --- src/VirtualTable.tsx | 54 +++++++++++++++++++++++--------------------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/src/VirtualTable.tsx b/src/VirtualTable.tsx index 00880eb..7f02374 100644 --- a/src/VirtualTable.tsx +++ b/src/VirtualTable.tsx @@ -17,6 +17,16 @@ import { LazyPaginatedCollection } from './helpers/LazyPaginatedCollection'; import { Style, DataSource } from './helpers/types'; import { Container, Row, Col } from 'react-bootstrap'; +/** + * Represent the rectangular. + */ +interface Rect { + x: number; + y: number; + height: number; + width: number; +} + interface State { scrollTop: number, itemHeight: number, @@ -25,6 +35,7 @@ interface State { offset: number, selected: number; hovered: number; + rect: Rect; } interface Action { @@ -50,13 +61,9 @@ interface Args { function reducer(state: State, action: Action): State { switch (action.type) { case 'scroll': - return { ...state, ...action.data }; case 'render': - return { ...state, ...action.data }; case 'loaded': - return { ...state, ...action.data }; case 'click': - return { ...state, ...action.data }; case 'hover': return { ...state, ...action.data }; default: @@ -79,16 +86,6 @@ function reducer(state: State, action: Action): State { * @property {DataSource} fetcher A datasource to fetch the data. */ -/** - * Represent the rectangular. - */ -interface Rect { - x: number; - y: number; - height: number; - width: number; -} - /** * @description VirtualTable component. * @@ -100,12 +97,6 @@ interface Rect { export default function VirtualTable({ height, renderer, fetcher, style }: Args): JSX.Element { const ref = useRef(null); const [collection, setCollection] = useState>(() => new LazyPaginatedCollection(1, fetcher)); - const [rect, setRect] = useState({ - x: 0, - y: 0, - height: 0, - width: 0, - }); useEffect(() => { setCollection(new LazyPaginatedCollection(collection.pageSize() ? collection.pageSize() : 1, fetcher)); @@ -119,6 +110,12 @@ export default function VirtualTable({ height, renderer, fetcher, style }: offset: 0, selected: -1, hovered: -1, + rect: { + x: 0, + y: 0, + height: 0, + width: 0, + } }); const [currentOffset, setCurrentOffset] = useState(0); @@ -128,7 +125,12 @@ export default function VirtualTable({ height, renderer, fetcher, style }: useEffect(() => { const handler = () => { if (ref && ref.current) { - setRect(ref.current.getBoundingClientRect()); + dispatch({ + type: 'render', + data: { + rect: ref.current.getBoundingClientRect(), + }, + }); } }; window.addEventListener('resize', handler); @@ -206,11 +208,11 @@ export default function VirtualTable({ height, renderer, fetcher, style }: ref.current.scrollTop = state.scrollTop % state.itemHeight; if (ref.current.children && ref.current.children.length) { if (ref.current.children[0].clientHeight !== state.itemHeight) { - setRect(ref.current.getBoundingClientRect()); dispatch({ type: 'render', data: { itemHeight: ref.current.children[0].clientHeight, + rect: ref.current.getBoundingClientRect(), }, }); } @@ -241,10 +243,10 @@ export default function VirtualTable({ height, renderer, fetcher, style }:
{ const index = Math.floor((e.clientY + state.scrollTop - ref.current.getBoundingClientRect().top) / state.itemHeight); From d709a461e2bde4639d711ec441cea1fa61d40033 Mon Sep 17 00:00:00 2001 From: Nikita Vakula Date: Sun, 24 Dec 2023 23:53:09 +0100 Subject: [PATCH 2/4] Add currentOffset to the state Signed-off-by: Nikita Vakula --- src/VirtualTable.tsx | 50 +++++++++++++++++++++----------------- src/helpers/collections.ts | 2 +- 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/src/VirtualTable.tsx b/src/VirtualTable.tsx index 7f02374..25e6f99 100644 --- a/src/VirtualTable.tsx +++ b/src/VirtualTable.tsx @@ -9,7 +9,7 @@ import React, { } from 'react'; import PropTypes from 'prop-types'; -import { slideItems } from './helpers/collections'; +import { slideItems, Page } from './helpers/collections'; import './base.css'; @@ -28,11 +28,11 @@ interface Rect { } interface State { - scrollTop: number, - itemHeight: number, - itemCount: number, - items: Array, - offset: number, + scrollTop: number; + itemHeight: number; + itemCount: number; + page: Page; + offset: number; selected: number; hovered: number; rect: Rect; @@ -106,7 +106,10 @@ export default function VirtualTable({ height, renderer, fetcher, style }: scrollTop: 0, itemHeight: 0, itemCount: 0, - items: [], + page: { + items: [], + offset: 0, + }, offset: 0, selected: -1, hovered: -1, @@ -118,8 +121,6 @@ export default function VirtualTable({ height, renderer, fetcher, style }: } }); - const [currentOffset, setCurrentOffset] = useState(0); - const calculatePageCount = () => 2 * Math.floor(height / state.itemHeight); useEffect(() => { @@ -147,11 +148,9 @@ export default function VirtualTable({ height, renderer, fetcher, style }: data: { scrollTop: 0, itemHeight: 0, - items: [], - offset: 0, + page: result, selected: -1, hovered: -1, - ...result, itemCount: collection.count(), }, }); @@ -164,16 +163,26 @@ export default function VirtualTable({ height, renderer, fetcher, style }: const offset = Math.floor(state.scrollTop / state.itemHeight); const c = calculatePageCount(); if (c !== collection.pageSize()) { - setCurrentOffset(0); + dispatch({ + type: 'loaded', + data: { + offset: 0, + }, + }); setCollection(new LazyPaginatedCollection(c, fetcher)); - } else { - setCurrentOffset(offset); + } else if (state.offset !== offset) { + dispatch({ + type: 'loaded', + data: { + offset, + }, + }); collection.slice(offset, collection.pageSize()).then((result) => { - if (currentOffset !== result.offset) { + if (state.offset !== result.offset) { dispatch({ type: 'loaded', data: { - ...result, + page: result, itemCount: collection.count(), }, }); @@ -220,7 +229,7 @@ export default function VirtualTable({ height, renderer, fetcher, style }: } }); - if (state.items.length === 0) { + if (state.page.items.length === 0) { return
; } @@ -235,10 +244,7 @@ export default function VirtualTable({ height, renderer, fetcher, style }: height, }} > - {generate(currentOffset, slideItems(currentOffset, { - items: state.items, - offset: state.offset, - }))} + {generate(state.offset, slideItems(state.offset, state.page))}
{ +export interface Page { /** * Page items */ From 0308efbd945e11c91defb02cf879e76ed305272c Mon Sep 17 00:00:00 2001 From: Nikita Vakula Date: Sun, 24 Dec 2023 23:53:33 +0100 Subject: [PATCH 3/4] Refactored effects Signed-off-by: Nikita Vakula --- src/VirtualTable.tsx | 242 ++++++++++++++++++++++++------------------- 1 file changed, 137 insertions(+), 105 deletions(-) diff --git a/src/VirtualTable.tsx b/src/VirtualTable.tsx index 25e6f99..fef4cbc 100644 --- a/src/VirtualTable.tsx +++ b/src/VirtualTable.tsx @@ -28,6 +28,7 @@ interface Rect { } interface State { + ready: boolean; scrollTop: number; itemHeight: number; itemCount: number; @@ -50,6 +51,31 @@ interface Args { style?: Style; } +function get_initial_state(): State { + return { + ready: false, + scrollTop: 0, + itemHeight: 0, + itemCount: 0, + page: { + items: [], + offset: 0, + }, + offset: 0, + selected: -1, + hovered: -1, + rect: { + x: 0, + y: 0, + height: 0, + width: 0, + } + } +} +function calculatePageCount(pageHeight: number, itemHeight: number) { + return 2 * Math.floor(pageHeight / itemHeight); +} + /** * Reducer function for managing state changes. * @@ -96,142 +122,140 @@ function reducer(state: State, action: Action): State { */ export default function VirtualTable({ height, renderer, fetcher, style }: Args): JSX.Element { const ref = useRef(null); + const invisible = useRef(null); const [collection, setCollection] = useState>(() => new LazyPaginatedCollection(1, fetcher)); + const [state, dispatch] = useReducer(reducer, get_initial_state()); - useEffect(() => { - setCollection(new LazyPaginatedCollection(collection.pageSize() ? collection.pageSize() : 1, fetcher)); - }, [fetcher]); + const generate = (offset: number, d: Array) => { + const ret = []; - const [state, dispatch] = useReducer(reducer, { - scrollTop: 0, - itemHeight: 0, - itemCount: 0, - page: { - items: [], - offset: 0, - }, - offset: 0, - selected: -1, - hovered: -1, - rect: { - x: 0, - y: 0, - height: 0, - width: 0, + for (let i = 0; i < d.length; i += 1) { + let className = ''; + if (style) { + className = style.item; + } + if (i + offset === state.selected && style) { + className = `${className} ${style.select}`; + } else if (i + offset === state.hovered && style) { + className = `${className} ${style.hover}`; + } + ret.push(
{renderer(d[i], className)}
); } - }); + return ret; + }; - const calculatePageCount = () => 2 * Math.floor(height / state.itemHeight); - useEffect(() => { - const handler = () => { - if (ref && ref.current) { - dispatch({ - type: 'render', - data: { - rect: ref.current.getBoundingClientRect(), - }, - }); - } - }; - window.addEventListener('resize', handler); - return function cleanup() { - window.removeEventListener('resize', handler, true); + // A callback to update the table view in case of resize event. + const handler = () => { + let itemHeight = state.itemHeight; + let rect = state.rect; + if (invisible && invisible.current) { + itemHeight = invisible.current.clientHeight; + } + if (ref && ref.current) { + rect = ref.current.getBoundingClientRect(); } - }, []); - useEffect(() => { - if (collection) { - collection.slice(0, collection.pageSize()).then((result) => { + // Update the size of the widget and the size of the items + dispatch({ + type: 'render', + data: { + rect, + itemHeight, + scrollTop: 0, + selected: -1, + hovered: -1, + page: { + items: [], + offset: 0, + } + }, + }); + + // If the item's height is already known, then update the lazy collection + // and re-fetch the items. + if (itemHeight) { + const new_collection = new LazyPaginatedCollection(calculatePageCount(rect.height, itemHeight), fetcher); + new_collection.slice(0, new_collection.pageSize()).then((result) => { dispatch({ type: 'loaded', data: { - scrollTop: 0, - itemHeight: 0, page: result, - selected: -1, - hovered: -1, - itemCount: collection.count(), + itemCount: new_collection.count(), }, }); + setCollection(new_collection); }); } - }, [collection]); + }; + // Effect that updates the lazy collection in case fetcher gets updated useEffect(() => { - if (state.itemHeight) { - const offset = Math.floor(state.scrollTop / state.itemHeight); - const c = calculatePageCount(); - if (c !== collection.pageSize()) { - dispatch({ - type: 'loaded', - data: { - offset: 0, - }, - }); - setCollection(new LazyPaginatedCollection(c, fetcher)); - } else if (state.offset !== offset) { - dispatch({ - type: 'loaded', - data: { - offset, - }, - }); - collection.slice(offset, collection.pageSize()).then((result) => { - if (state.offset !== result.offset) { - dispatch({ - type: 'loaded', - data: { - page: result, - itemCount: collection.count(), - }, - }); - } - }); - } - } - }, [ - state, - ]); + setCollection(new LazyPaginatedCollection(collection.pageSize() ? collection.pageSize() : 1, fetcher)); + }, [fetcher]); - const generate = (offset: number, d: Array) => { - const ret = []; + // Effect to fetch the first item (to draw a fake item to get the true size if the item) + // and the total number of items. + useEffect(() => { + collection.slice(0, collection.pageSize()).then((result) => { + dispatch({ + type: 'loaded', + data: { + ready: true, + page: result, + itemCount: collection.count(), + }, + }); + }); - for (let i = 0; i < d.length; i += 1) { - let className = ''; - if (style) { - className = style.item; - } - if (i + offset === state.selected && style) { - className = `${className} ${style.select}`; - } else if (i + offset === state.hovered && style) { - className = `${className} ${style.hover}`; - } - ret.push(
{renderer(d[i], className)}
); + window.addEventListener('resize', handler); + return function cleanup() { + window.removeEventListener('resize', handler, true); } - return ret; - }; + }, []); + // Effect to run on all state updates. useEffect(() => { - if (ref.current) { - ref.current.scrollTop = state.scrollTop % state.itemHeight; - if (ref.current.children && ref.current.children.length) { - if (ref.current.children[0].clientHeight !== state.itemHeight) { + if (state.ready) { + if (state.itemHeight) { + const offset = Math.floor(state.scrollTop / state.itemHeight); + const c = calculatePageCount(height, state.itemHeight); + if (c === collection.pageSize() && state.offset !== offset) { + // Update the offset first and then start fetching the necessary items. + // This ensures a non-interruptive user experience, where all the + // required data is already available. dispatch({ - type: 'render', + type: 'loaded', data: { - itemHeight: ref.current.children[0].clientHeight, - rect: ref.current.getBoundingClientRect(), + offset, }, }); + collection.slice(offset, collection.pageSize()).then((result) => { + if (state.offset !== result.offset) { + dispatch({ + type: 'loaded', + data: { + page: result, + itemCount: collection.count(), + }, + }); + } + }); } + } else { + handler(); } } + }, [state]); + + // Effect to run on each render to make sure that the scrolltop of + // the item container is up-to-date. + useEffect(() => { + if (ref.current) { + ref.current.scrollTop = state.scrollTop % state.itemHeight; + } }); - if (state.page.items.length === 0) { - return
; - } return ( @@ -244,7 +268,15 @@ export default function VirtualTable({ height, renderer, fetcher, style }: height, }} > - {generate(state.offset, slideItems(state.offset, state.page))} + {state.ready && state.itemHeight === 0 && +
+ {renderer(state.page.items[0], '')} +
} + {state.itemHeight !== 0 && generate(state.offset, slideItems(state.offset, state.page))}
Date: Sun, 24 Dec 2023 23:54:57 +0100 Subject: [PATCH 4/4] Bumped version to 1.1.7 Signed-off-by: Nikita Vakula --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1eb25b7..556b542 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@krjakbrjak/virtualtable", - "version": "1.1.6", + "version": "1.1.7", "description": "", "repository": { "type": "git",