Skip to content

Commit

Permalink
Merge pull request #15 from krjakbrjak/VNI-datasource
Browse files Browse the repository at this point in the history
Added datasource interface
  • Loading branch information
krjakbrjak authored Dec 5, 2023
2 parents 0348e71 + c8a8ad2 commit 1b6d5fa
Show file tree
Hide file tree
Showing 7 changed files with 90 additions and 77 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@krjakbrjak/virtualtable",
"version": "1.1.5",
"version": "1.1.6",
"description": "",
"repository": {
"type": "git",
Expand Down
48 changes: 24 additions & 24 deletions src/VirtualTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<Type> {
Expand All @@ -35,18 +35,17 @@ interface Action<Type> {
interface Args<Type> {
height: number;
renderer: (data: Type, classes: string) => ReactNode;
fetcher: Fetcher<Type>;
fetcher: DataSource<Type>;
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<Type>(state: State<Type>, action: Action<Type>): State<Type> {
switch (action.type) {
Expand Down Expand Up @@ -77,7 +76,7 @@ function reducer<Type>(state: State<Type>, action: Action<Type>): State<Type> {
* @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.
*/

/**
Expand All @@ -100,14 +99,18 @@ interface Rect {
*/
export default function VirtualTable<Type>({ height, renderer, fetcher, style }: Args<Type>): JSX.Element {
const ref = useRef(null);
const [collection, setCollection] = useState<LazyPaginatedCollection<Type>>(new LazyPaginatedCollection<Type>(1, fetcher));
const [collection, setCollection] = useState<LazyPaginatedCollection<Type>>(() => new LazyPaginatedCollection<Type>(1, fetcher));
const [rect, setRect] = useState<Rect>({
x: 0,
y: 0,
height: 0,
width: 0,
});

useEffect(() => {
setCollection(new LazyPaginatedCollection<Type>(collection.pageSize() ? collection.pageSize() : 1, fetcher));
}, [fetcher]);

const [state, dispatch] = useReducer(reducer<Type>, {
scrollTop: 0,
itemHeight: 0,
Expand All @@ -129,27 +132,23 @@ export default function VirtualTable<Type>({ 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(),
},
Expand All @@ -161,11 +160,12 @@ export default function VirtualTable<Type>({ 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<Type>(c, fetcher));
} else {
setCurrentOffset(offset);
collection.slice(offset, collection.pageSize()).then((result) => {
if (currentOffset !== result.offset) {
dispatch({
Expand Down Expand Up @@ -297,7 +297,7 @@ export default function VirtualTable<Type>({ height, renderer, fetcher, style }:
VirtualTable.propTypes = {
height: PropTypes.number.isRequired,
renderer: PropTypes.func.isRequired,
fetcher: PropTypes.func.isRequired,
fetcher: PropTypes.object.isRequired,
};

VirtualTable.defaultProps = {
Expand Down
35 changes: 20 additions & 15 deletions src/__tests__/helpers.ts
Original file line number Diff line number Diff line change
@@ -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<number> {
fetch(index: number, count: number): Promise<Result<number>> {
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(() => {
});

Expand All @@ -14,21 +33,7 @@ describe('Helpers', () => {
it('LazyPaginatedCollection', (done) => {
const collection = new LazyPaginatedCollection<number>(
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 = [];
Expand Down
12 changes: 6 additions & 6 deletions src/helpers/LazyPaginatedCollection.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Result, Fetcher } from './types'
import { Result, DataSource } from './types'

/**
* @callback LazyPaginatedCollection.retrieve
Expand Down Expand Up @@ -27,15 +27,15 @@ export class LazyPaginatedCollection<Type> {
#pageSize: number;

// A callback to fetch data
#retrieve: Fetcher<Type>;
#retrieve: DataSource<Type>;

/**
* Constructs a new collection.
*
* @param {number} pageSize Page size
* @param {Fetcher<Type>} retrieve A callback to fetch the data
* @param {DataSource<Type>} retrieve A data fetcher
*/
constructor(pageSize: number, retrieve: Fetcher<Type>) {
constructor(pageSize: number, retrieve: DataSource<Type>) {
this.#pageOffsets = {};
this.#totalCount = -1;
this.#pageSize = pageSize;
Expand Down Expand Up @@ -89,7 +89,7 @@ export class LazyPaginatedCollection<Type> {
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;
Expand Down Expand Up @@ -123,7 +123,7 @@ export class LazyPaginatedCollection<Type> {
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]);
Expand Down
26 changes: 15 additions & 11 deletions src/helpers/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,6 @@ export interface Result<Type> {
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<Result<Type>>} - A promise holding the result of the fetch.
*/
export type Fetcher<Type> = (index: number, count: number) => Promise<Result<Type>>;

/**
* Represents the style of the item in the table.
*/
Expand All @@ -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<T> {
/**
* Fetches data.
* @param {number} index - The strating index to fetch items.
* @param {number} count - The number of items to fetch.
* @returns {Promise<Result<Type>>} - A promise holding the result of the fetch.
*/
fetch(index: number, count: number): Promise<Result<T>>;
}
5 changes: 3 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
39 changes: 21 additions & 18 deletions testApp/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<Result<number>> => {
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<number> {
fetch(index: number, count: number): Promise<Result<number>> {
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 (
<Container>
<Row style={{
height: '50px'
}}/>
}} />
<Row>
<Col/>
<Col />
<Col>
<VirtualTable<number>
style={style}
Expand All @@ -48,14 +51,14 @@ function App() {
{i !== undefined ? i : 'unknown'}
</div>}
height={400}
fetcher={fetchData}
fetcher={new Fetcher()}
/>
</Col>
<Col/>
<Col />
</Row>
<Row style={{
height: '50px'
}}/>
}} />
</Container>
);
}
Expand Down

0 comments on commit 1b6d5fa

Please sign in to comment.