diff --git a/package.json b/package.json index 175bb25..1eb25b7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@krjakbrjak/virtualtable", - "version": "1.1.5", + "version": "1.1.6", "description": "", "repository": { "type": "git", diff --git a/src/VirtualTable.tsx b/src/VirtualTable.tsx index 1d19639..00880eb 100644 --- a/src/VirtualTable.tsx +++ b/src/VirtualTable.tsx @@ -14,7 +14,7 @@ import { slideItems } from './helpers/collections'; import './base.css'; import { LazyPaginatedCollection } from './helpers/LazyPaginatedCollection'; -import { Style, Fetcher } from './helpers/types'; +import { Style, DataSource } from './helpers/types'; import { Container, Row, Col } from 'react-bootstrap'; interface State { @@ -35,18 +35,17 @@ interface Action { interface Args { height: number; renderer: (data: Type, classes: string) => ReactNode; - fetcher: Fetcher; + fetcher: DataSource; style?: Style; } /** * Reducer function for managing state changes. * - * @param {Object} state - The current state of the application. - * @param {Object} action - The action object that describes the state change. - * @param {string} action.type - The type of the action. - * @param {any} [action.data] - Additional data associated with the action. - * @returns {Object} - The new state after applying the action. + * @template {Type} + * @param {State} state - The current state of the application. + * @param {Action} action - The action object that describes the state change. + * @returns {State} - The new state after applying the action. */ function reducer(state: State, action: Action): State { switch (action.type) { @@ -77,7 +76,7 @@ function reducer(state: State, action: Action): State { * @typedef {Object} VirtualTable.Props * @property {number} height A height of the grid. * @property {VirtualTable.render} renderer A function to render data. - * @property {LazyPaginatedCollection.retrieve} fetcher An async function to fetch the data. + * @property {DataSource} fetcher A datasource to fetch the data. */ /** @@ -100,7 +99,7 @@ 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 [collection, setCollection] = useState>(() => new LazyPaginatedCollection(1, fetcher)); const [rect, setRect] = useState({ x: 0, y: 0, @@ -108,6 +107,10 @@ export default function VirtualTable({ height, renderer, fetcher, style }: width: 0, }); + useEffect(() => { + setCollection(new LazyPaginatedCollection(collection.pageSize() ? collection.pageSize() : 1, fetcher)); + }, [fetcher]); + const [state, dispatch] = useReducer(reducer, { scrollTop: 0, itemHeight: 0, @@ -129,27 +132,23 @@ export default function VirtualTable({ height, renderer, fetcher, style }: } }; window.addEventListener('resize', handler); - collection.slice(0, 1).then((result) => { - dispatch({ - type: 'loaded', - data: { - ...result, - itemCount: collection.count(), - }, - }); - }); return function cleanup() { window.removeEventListener('resize', handler, true); } }, []); useEffect(() => { - if (state.itemHeight !== 0) { - const offset = Math.floor(state.scrollTop / state.itemHeight); - collection.slice(offset, collection.pageSize()).then((result) => { + if (collection) { + collection.slice(0, collection.pageSize()).then((result) => { dispatch({ type: 'loaded', data: { + scrollTop: 0, + itemHeight: 0, + items: [], + offset: 0, + selected: -1, + hovered: -1, ...result, itemCount: collection.count(), }, @@ -161,11 +160,12 @@ export default function VirtualTable({ height, renderer, fetcher, style }: useEffect(() => { if (state.itemHeight) { const offset = Math.floor(state.scrollTop / state.itemHeight); - setCurrentOffset(offset); const c = calculatePageCount(); if (c !== collection.pageSize()) { - setCollection(new LazyPaginatedCollection(c, fetcher)); + setCurrentOffset(0); + setCollection(new LazyPaginatedCollection(c, fetcher)); } else { + setCurrentOffset(offset); collection.slice(offset, collection.pageSize()).then((result) => { if (currentOffset !== result.offset) { dispatch({ @@ -297,7 +297,7 @@ export default function VirtualTable({ height, renderer, fetcher, style }: VirtualTable.propTypes = { height: PropTypes.number.isRequired, renderer: PropTypes.func.isRequired, - fetcher: PropTypes.func.isRequired, + fetcher: PropTypes.object.isRequired, }; VirtualTable.defaultProps = { diff --git a/src/__tests__/helpers.ts b/src/__tests__/helpers.ts index 89d42c7..b324266 100644 --- a/src/__tests__/helpers.ts +++ b/src/__tests__/helpers.ts @@ -1,10 +1,29 @@ import { LazyPaginatedCollection } from '../helpers/LazyPaginatedCollection'; import { slideItems } from '../helpers/collections'; +import { DataSource, Result } from '../helpers/types'; describe('Helpers', () => { const COLLECTION_COUNT = 1234; const COLLECTION_PAGE_SIZE = 3; + class TestSource implements DataSource { + fetch(index: number, count: number): Promise> { + return new Promise((resolve, reject) => { + if (index > COLLECTION_COUNT - 1 || index < 0 || count < 0) { + reject(new RangeError()); + } else { + const tmp = Math.min(count, COLLECTION_COUNT - index); + const items = [...Array(tmp).keys()].map((value) => value + index); + resolve({ + from: index, + items, + totalCount: COLLECTION_COUNT, + }); + } + }); + } + } + beforeEach(() => { }); @@ -14,21 +33,7 @@ describe('Helpers', () => { it('LazyPaginatedCollection', (done) => { const collection = new LazyPaginatedCollection( COLLECTION_PAGE_SIZE, - (index, count) => { - return new Promise((resolve, reject) => { - if (index > COLLECTION_COUNT - 1 || index < 0 || count < 0) { - reject(new RangeError()); - } else { - const tmp = Math.min(count, COLLECTION_COUNT - index); - const items = [...Array(tmp).keys()].map((value) => value + index); - resolve({ - from: index, - items, - totalCount: COLLECTION_COUNT, - }); - } - }); - } + new TestSource() ); const all = []; diff --git a/src/helpers/LazyPaginatedCollection.ts b/src/helpers/LazyPaginatedCollection.ts index e437a0a..3679792 100644 --- a/src/helpers/LazyPaginatedCollection.ts +++ b/src/helpers/LazyPaginatedCollection.ts @@ -1,4 +1,4 @@ -import { Result, Fetcher } from './types' +import { Result, DataSource } from './types' /** * @callback LazyPaginatedCollection.retrieve @@ -27,15 +27,15 @@ export class LazyPaginatedCollection { #pageSize: number; // A callback to fetch data - #retrieve: Fetcher; + #retrieve: DataSource; /** * Constructs a new collection. * * @param {number} pageSize Page size - * @param {Fetcher} retrieve A callback to fetch the data + * @param {DataSource} retrieve A data fetcher */ - constructor(pageSize: number, retrieve: Fetcher) { + constructor(pageSize: number, retrieve: DataSource) { this.#pageOffsets = {}; this.#totalCount = -1; this.#pageSize = pageSize; @@ -89,7 +89,7 @@ export class LazyPaginatedCollection { const offset = this.#pageIndexFor(index); // If the page is still missing then fetch it if (this.#pageOffsets[offset] === undefined) { - this.#pageOffsets[offset] = this.#retrieve(offset, this.#pageSize); + this.#pageOffsets[offset] = this.#retrieve.fetch(offset, this.#pageSize); return this.#pageOffsets[offset] .then((result) => { this.#totalCount = result.totalCount; @@ -123,7 +123,7 @@ export class LazyPaginatedCollection { const all = []; for (let i = offset; i < index + count; i += this.#pageSize) { if (this.#pageOffsets[i] === undefined) { - this.#pageOffsets[i] = this.#retrieve(i, this.#pageSize); + this.#pageOffsets[i] = this.#retrieve.fetch(i, this.#pageSize); } all.push(this.#pageOffsets[i]); diff --git a/src/helpers/types.ts b/src/helpers/types.ts index 6c089ec..8c043bf 100644 --- a/src/helpers/types.ts +++ b/src/helpers/types.ts @@ -16,17 +16,6 @@ export interface Result { totalCount: number; } -/** - * A callback function to fetch the items. - * - * @typedef {Function} Fetcher - * @template Type - The type of the element to be returned from the function. - * @param {number} index - The strating index to fetch items. - * @param {number} count - The number of items to fetch. - * @returns {Promise>} - A promise holding the result of the fetch. - */ -export type Fetcher = (index: number, count: number) => Promise>; - /** * Represents the style of the item in the table. */ @@ -44,3 +33,18 @@ export interface Style { */ item: string; } + +/** + * Represents an object that fetches the items. + * + * @template {T} - The type of the element to be returned from the function. + */ +export interface DataSource { + /** + * Fetches data. + * @param {number} index - The strating index to fetch items. + * @param {number} count - The number of items to fetch. + * @returns {Promise>} - A promise holding the result of the fetch. + */ + fetch(index: number, count: number): Promise>; +} diff --git a/src/index.ts b/src/index.ts index 0b818c8..0edc2d1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,10 +5,11 @@ */ import VirtualTable from './VirtualTable'; -import { Result, Fetcher } from './helpers/types'; +import { Result, DataSource, Style } from './helpers/types'; export { VirtualTable, Result, - Fetcher, + DataSource, + Style, }; diff --git a/testApp/src/index.tsx b/testApp/src/index.tsx index bf7e976..286c725 100644 --- a/testApp/src/index.tsx +++ b/testApp/src/index.tsx @@ -8,34 +8,37 @@ import React, { useState } from 'react'; import { createRoot } from 'react-dom/client'; import VirtualTable from '../../src/VirtualTable'; -import { Result, Style } from '../../src/helpers/types'; +import { Result, Style, DataSource } from '../../src/helpers/types'; import css from './index.css'; import Container from 'react-bootstrap/Container'; import Row from 'react-bootstrap/Row'; import Col from 'react-bootstrap/Col'; const style = css as Style; -const fetchData = (index: number, count: number): Promise> => { - const items = [...Array(count).keys()].map((value) => value + index); - return new Promise((resolve, reject) => { - setTimeout(() => { - resolve({ - from: index, - items, - totalCount: 1234, - }); - }, 1000); - }); -}; + +class Fetcher implements DataSource { + fetch(index: number, count: number): Promise> { + const items = [...Array(count).keys()].map((value) => value + index); + return new Promise((resolve, reject) => { + setTimeout(() => { + resolve({ + from: index, + items, + totalCount: 1234, + }); + }, 1000); + }); + } +} function App() { return ( + }} /> - + style={style} @@ -48,14 +51,14 @@ function App() { {i !== undefined ? i : 'unknown'} } height={400} - fetcher={fetchData} + fetcher={new Fetcher()} /> - + + }} /> ); }