Skip to content

Commit

Permalink
Merge pull request #7 from krjakbrjak/VNI-js-2-ts
Browse files Browse the repository at this point in the history
Use typescript
  • Loading branch information
krjakbrjak authored Nov 19, 2023
2 parents 5281e81 + b662ebf commit 7a46fce
Show file tree
Hide file tree
Showing 13 changed files with 262 additions and 64 deletions.
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

0 comments on commit 7a46fce

Please sign in to comment.