Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added datasource interface #15

Merged
merged 2 commits into from
Dec 5, 2023
Merged
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
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