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

Use typescript #7

Merged
merged 2 commits into from
Nov 19, 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
11 changes: 10 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@krjakbrjak/virtualtable",
"version": "1.0.0",
"version": "1.1.0",
"description": "",
"repository": {
"type": "git",
Expand Down Expand Up @@ -34,6 +34,11 @@
"@testing-library/jest-dom": "^6.1.4",
"@testing-library/react": "^14.1.0",
"@testing-library/react-hooks": "^8.0.1",
"@types/copy-webpack-plugin": "^10.1.0",
"@types/jest": "^29.5.8",
"@types/node": "^20.9.0",
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15",
"babel-loader": "^9.1.3",
"better-docs": "^2.7.2",
"css-loader": "^6.8.1",
Expand All @@ -48,6 +53,9 @@
"jsdoc": "^4.0.2",
"sinon": "^17.0.1",
"style-loader": "^3.3.3",
"ts-jest": "^29.1.1",
"ts-loader": "^9.5.0",
"typescript": "^5.2.2",
"webpack": "^5.89.0",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.15.1"
Expand All @@ -59,6 +67,7 @@
"jsdoc:gen": "jsdoc -c jsdoc.conf.json"
},
"jest": {
"preset": "ts-jest",
"verbose": true,
"testMatch": [
"**/__tests__/**/*.[jt]s?(x)"
Expand Down
1 change: 1 addition & 0 deletions src/Global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
declare module "*.css";
44 changes: 34 additions & 10 deletions src/VirtualTable.js → src/VirtualTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/

import React, {
useReducer, useEffect, useState, useRef,
useReducer, useEffect, useState, useRef, ReactNode,
} from 'react';
import PropTypes from 'prop-types';

Expand All @@ -15,6 +15,26 @@ import './base.css';
import css from './VirtualTable.css';

import { LazyPaginatedCollection } from './helpers/LazyPaginatedCollection';
import { Result, Fetcher } from './helpers/types';

interface State<Type> {
scrollTop: number,
itemHeight: number,
itemCount: number,
items: Array<Type>,
offset: number,
}

interface Action<Type> {
type: 'scroll' | 'render' | 'loaded';
data: Partial<State<Type>>
}

interface Args<Type> {
height: number;
renderer: (data: Type) => ReactNode;
fetcher: Fetcher<Type>;
}

/**
* Reducer function for managing state changes.
Expand All @@ -25,12 +45,12 @@ import { LazyPaginatedCollection } from './helpers/LazyPaginatedCollection';
* @param {any} [action.data] - Additional data associated with the action.
* @returns {Object} - The new state after applying the action.
*/
const reducer = (state, action) => {
function reducer<Type>(state: State<Type>, action: Action<Type>): State<Type> {
switch (action.type) {
case 'scroll':
return { ...state, scrollTop: action.data };
return { ...state, ...action.data };
case 'render':
return { ...state, itemHeight: action.data };
return { ...state, ...action.data };
case 'loaded':
return { ...state, ...action.data };
default:
Expand Down Expand Up @@ -61,11 +81,11 @@ const reducer = (state, action) => {
* @param {VirtualTable.Props} props Properties
* @component
*/
function VirtualTable({ height, renderer, fetcher }) {
function VirtualTable<Type>({ height, renderer, fetcher }: Args<Type>) {
const ref = useRef(null);
const [collection, setCollection] = useState(new LazyPaginatedCollection(1, fetcher));
const [collection, setCollection] = useState<LazyPaginatedCollection<Type>>(new LazyPaginatedCollection<Type>(1, fetcher));

const [state, dispatch] = useReducer(reducer, {
const [state, dispatch] = useReducer(reducer<Type>, {
scrollTop: 0,
itemHeight: 0,
itemCount: 0,
Expand Down Expand Up @@ -129,7 +149,7 @@ function VirtualTable({ height, renderer, fetcher }) {
state,
]);

const generate = (offset, d) => {
const generate = (offset: number, d: Array<Type>) => {
const ret = [];
for (let i = 0; i < d.length; i += 1) {
ret.push(<div key={i + offset}>{renderer(d[i])}</div>);
Expand All @@ -144,7 +164,9 @@ function VirtualTable({ height, renderer, fetcher }) {
if (ref.current.children[0].clientHeight !== state.itemHeight) {
dispatch({
type: 'render',
data: ref.current.children[0].clientHeight,
data: {
itemHeight: ref.current.children[0].clientHeight,
},
});
}
}
Expand Down Expand Up @@ -176,7 +198,9 @@ function VirtualTable({ height, renderer, fetcher }) {
onScroll={(e) => {
dispatch({
type: 'scroll',
data: e.target.scrollTop,
data: {
scrollTop: (e.target as HTMLElement).scrollTop,
},
});
}}
>
Expand Down
30 changes: 16 additions & 14 deletions src/__tests__/helpers.js → src/__tests__/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,23 @@ describe('Helpers', () => {
});

it('LazyPaginatedCollection', (done) => {
const collection = new LazyPaginatedCollection(
const collection = new LazyPaginatedCollection<number>(
COLLECTION_PAGE_SIZE,
(index, count) => 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,
});
}
}),
(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,
});
}
});
}
);

const all = [];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Result, Fetcher } from './types'

/**
* @callback LazyPaginatedCollection.retrieve
* @async
Expand All @@ -10,30 +12,30 @@
/**
* Calss representing a lazy paginated collection of data.
*/
export class LazyPaginatedCollection {
export class LazyPaginatedCollection<Type> {
// Stores a map of Promises to page requests. A key
// corresponds to the index of the item within a collection.
// A value corresponds to a Promise of the fetch request of the items
// from an offset tht corresponds to the key of the map. The number of items
// requested equals #pageSize property.
#pageOffsets;
#pageOffsets: { [id: number]: Promise<Result<Type>> };

// Totsl number of items in a collection. -1 if collection was not loaded.
#totalCount;
#totalCount: number;

// Corresponds to the (at most) number of items fetched at each reauest.
#pageSize;
#pageSize: number;

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

/**
* Constructs a new collection.
*
* @param {number} pageSize Page size
* @param {LazyPaginatedCollection.retrieve} retrieve A callback to fetch the data
*/
constructor(pageSize, retrieve) {
constructor(pageSize: number, retrieve: Fetcher<Type>) {
this.#pageOffsets = {};
this.#totalCount = -1;
this.#pageSize = pageSize;
Expand Down Expand Up @@ -69,7 +71,7 @@ export class LazyPaginatedCollection {
* @private
* @returns {number}
*/
#pageIndexFor = (index) => (index - (index % this.#pageSize));
#pageIndexFor = (index: number) => (index - (index % this.#pageSize));

/**
* Returns an items at index.
Expand All @@ -78,7 +80,7 @@ export class LazyPaginatedCollection {
* @param {number} index An index of an item to retrieve.
* @returns Promise.<number | RangeError>
*/
at(index) {
at(index: number) {
// Invalid offset
if (index < 0) {
return Promise.reject(new RangeError());
Expand Down Expand Up @@ -107,7 +109,7 @@ export class LazyPaginatedCollection {
* @param {number} count Max number of items to fetch.
* @returns Promise.<Array.<Object> | RangeError>
*/
async slice(index, count) {
async slice(index: number, count: number) {
// Invalid offset or count => an empty list
if (index < 0 || count <= 0) {
return Promise.resolve({
Expand All @@ -131,7 +133,7 @@ export class LazyPaginatedCollection {
return Promise.all(all.map((promise) => promise.catch((err) => err)))
.then((results) => results.filter((result) => !(result instanceof Error)))
.then((results) => {
const ret = [];
const ret: Array<Type> = [];
for (let i = 0; i < results.length; i += 1) {
this.#totalCount = results[i].totalCount;
ret.splice(ret.length, 0, ...results[i].items);
Expand Down
7 changes: 6 additions & 1 deletion src/helpers/collections.js → src/helpers/collections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
* @property {Array.Object} items Items
*/

interface Page<Type> {
items: Array<Type>;
offset: number;
}

/**
* Constructs a new slice of data.
*
Expand All @@ -21,7 +26,7 @@
* @param {Slice} slice A slice of the collection
* @returns Array.<Object>
*/
export function slideItems(currentOffset, { items, offset }) {
export function slideItems<Type>(currentOffset: number, { items, offset}: Page<Type>) {
const count = items.length;
// Nothing to do
if (offset === currentOffset) {
Expand Down
7 changes: 7 additions & 0 deletions src/helpers/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface Result<Type> {
from: number;
items: Array<Type>;
totalCount: number;
}

export type Fetcher<Type> = (index: number, count: number) => Promise<Result<Type>>;
File renamed without changes.
3 changes: 2 additions & 1 deletion testApp/src/index.js → testApp/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ import React, { useState } from 'react';
import { createRoot } from 'react-dom/client';

import VirtualTable from '../../src/VirtualTable';
import { Result } from '../../src/helpers/types';

const fetchData = (index, count) => {
const fetchData = (index: number, count: number): Promise<Result<number>> => {
const items = [...Array(count).keys()].map((value) => value + index);
return new Promise((resolve, reject) => {
setTimeout(() => {
Expand Down
9 changes: 7 additions & 2 deletions testApp/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const ESLintPlugin = require('eslint-webpack-plugin');
module.exports = (env = {}) => ({
devtool: 'inline-source-map',
mode: 'development',
entry: './testApp/src/index.js',
entry: './testApp/src/index.tsx',
output: {
path: path.resolve('testApp/dist'),
filename: 'index.js',
Expand All @@ -23,6 +23,11 @@ module.exports = (env = {}) => ({
exclude: /node_modules/,
use: ['babel-loader'],
},
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
{
test: /\.css$/i,
exclude: /node_modules/,
Expand All @@ -36,6 +41,6 @@ module.exports = (env = {}) => ({
],
},
resolve: {
extensions: ['.js'],
extensions: ['.tsx', '.ts', '.js'],
},
});
14 changes: 14 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"outDir": "./dist/",
"sourceMap": true,
"noImplicitAny": true,
"module": "es6",
"target": "es2015",
"jsx": "react",
"allowJs": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"downlevelIteration": true
}
}
9 changes: 7 additions & 2 deletions webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ module.exports = (env = {}) => {
const mode = env.production ? 'production' : 'development';
return ({
mode,
entry: './src/index.js',
entry: './src/index.ts',
output: {
path: path.resolve('dist'),
filename: 'main.js',
Expand All @@ -19,6 +19,11 @@ module.exports = (env = {}) => {
exclude: /node_modules/,
use: ['babel-loader'],
},
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
{
test: /\.css$/i,
exclude: /node_modules/,
Expand All @@ -32,7 +37,7 @@ module.exports = (env = {}) => {
],
},
resolve: {
extensions: ['.js'],
extensions: ['.tsx', '.ts', '.js'],
},
});
};
Loading