Skip to content

Commit

Permalink
feat: improve async components (#53)
Browse files Browse the repository at this point in the history
* feat: added SkeletonText component
Included some refactoring of older components

* fix(skeleton): import react

* feat(async-table): improve handling of async data

* lint: fix warnings

* lint: fix warnings

* lint: fix warning
Again...

* feat(async-table): increase grid chunk size

* feat(async-table): use rem for grid gaps
  • Loading branch information
colbr authored Oct 6, 2022
1 parent aead9cb commit 3c74997
Show file tree
Hide file tree
Showing 32 changed files with 667 additions and 141 deletions.
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -116,13 +116,14 @@
"@stitches/react": "^1.2.8",
"@storybook/preset-scss": "^1.0.3",
"@uiw/react-md-editor": "^3.12.3",
"remark-emoji": "^3.0.2",
"remark-gemoji": "^7.0.1",
"classnames": "^2.3.1",
"ethers": "^5.6.9",
"focus-visible": "^5.2.0",
"react-infinite-scroll-component": "^6.1.0",
"react-loading-skeleton": "^3.1.0",
"react-router-dom": "^6.3.0",
"remark-emoji": "^3.0.2",
"remark-gemoji": "^7.0.1",
"sass": "^1.52.2"
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { FC } from 'react';

// @ts-ignore
import styles from './HotswapContainer.module.scss';

export interface HotswapContainerProps {
Expand Down
34 changes: 17 additions & 17 deletions src/components/AsyncTable/AsyncTable.module.scss
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
@import '../../styles/colors';

.Container {
.Table {
width: 100%;
border-collapse: collapse;

th {
font-size: 0.75rem;
Expand All @@ -16,26 +17,25 @@
vertical-align: middle;

&:first-of-type {
padding-left: 0;
padding-left: 1rem;
}

&:last-of-type {
padding-right: 0;
padding-right: 1rem;
}
}
}

.Right {
margin-left: auto;
text-align: right;
}

.Center {
margin: 0 auto;
text-align: center;
}

.Left {
margin-right: auto;
text-align: left;
}
.Loading {
margin-top: 3rem;
}

.Grid {
display: grid;
width: 100%;
margin-top: 1rem;
grid-template-columns: repeat(auto-fit, minmax(32%, 0.5fr));
grid-row-start: 1;
list-style: none;
grid-gap: 0.875rem 0.75rem;
}
134 changes: 62 additions & 72 deletions src/components/AsyncTable/AsyncTable.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import React, { ReactNode } from 'react';
import React, { useEffect, useRef } from 'react';

import classNames from 'classnames/bind';
import Skeleton from 'react-loading-skeleton';
import { Column } from './Column';
import { LoadingIndicator } from '../LoadingIndicator';
import { useInfiniteScroll } from './useInfiniteScroll';
import { Grid } from './Grid';
import { Table } from './Table';

import styles from './AsyncTable.module.scss';

const cx = classNames.bind(styles);
import { AsyncTableComponent } from './types';

/**
* NOTE:
Expand All @@ -21,95 +23,83 @@ const cx = classNames.bind(styles);
* should be deferred to a row component, and the minimal
* data for the row (i.e. data from the first query) should
* be rendered first.
*
* @TODO: make sure row components enforce column alignments
* @TODO: improve typings
* @TODO: implement grid view
*/

export interface Column {
id: string; // ID is required because header is optional
header?: string;
alignment: 'left' | 'right' | 'center';
className?: string;
}

export interface SearchKey<T> {
key: keyof T;
name: string;
}

export interface AsyncTableProps<T> {
// Data
data?: T[];
itemKey: keyof T;
className?: string;
columns: Column[];

// Display
data?: T[];
gridComponent: AsyncTableComponent<T>;
isGridView?: boolean;
isLoading?: boolean;
numLoadingRows?: number;
rowHeight?: number;
isSingleColumnGrid?: boolean;
itemKey: keyof T;
loadingText?: string;
// @TODO: pick a better name for "options"
rowComponent: (data: T, options?: unknown) => ReactNode;
gridComponent: (data: T, options?: unknown) => ReactNode;

// Search
rowComponent: AsyncTableComponent<T>;
searchKey: SearchKey<T>;
}

const CONFIG = {
chunkSizeGrid: 6,
chunkSizeList: 10
};

export const AsyncTable = <T extends unknown>({
data,
// itemKey,
className,
columns,
data,
gridComponent,
isGridView,
isLoading,
numLoadingRows = 3,
rowHeight = 40,
isSingleColumnGrid = false,
rowComponent
}: // gridComponent,
// searchKey,
AsyncTableProps<T>) => {
// @TODO: handle loading
}: AsyncTableProps<T>) => {
const lastView = useRef<'grid' | 'list'>();

const { InfiniteScrollWrapper, chunkedComponents, resetChunk } = useInfiniteScroll({
items: data ?? [],
chunkSize: isGridView ? CONFIG.chunkSizeGrid : CONFIG.chunkSizeList,
component: isGridView ? gridComponent : rowComponent
});

// Warn if dev didn't memoize rowComponent
useEffect(() => {
if (lastView.current === 'list' && !isGridView) {
console.warn('Detected unmemoized rowComponent!');
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [rowComponent]);

// Warn if dev didn't memoize gridComponent
useEffect(() => {
if (lastView.current === 'grid' && isGridView) {
console.warn('Detected unmemoized gridComponent!');
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [gridComponent]);

// Reset infinite scroll whenever user changes view
useEffect(() => {
lastView.current = 'grid';
resetChunk();
}, [isGridView, resetChunk]);

if (isGridView) {
return <div>Grid View</div>;
// Grid view doesn't render Skeletons when loading (yet)
if (isGridView && isLoading) {
return <LoadingIndicator className={styles.Loading} text={'Loading'} />;
}

return (
<table className={styles.Container}>
<thead>
<tr>
{columns.map((c: Column) => (
<th
className={cx(c.className, {
Left: c.alignment === 'left',
Center: c.alignment === 'center',
Right: c.alignment === 'right'
})}
key={`async-table-th-${c.id}`}
>
{isLoading ? <Skeleton /> : c.header}
</th>
))}
</tr>
</thead>
<tbody>
{isLoading
? Array(numLoadingRows)
.fill(0)
.map((_, numIndex) => (
<tr key={`async-table-tr-${numIndex}`}>
{columns.map((c: Column) => (
<td key={`async-table-tr-${numIndex}-td-${c.id}`}>
<Skeleton width={'100%'} height={rowHeight} />
</td>
))}
</tr>
))
: data?.map(d => rowComponent(d))}
</tbody>
</table>
<InfiniteScrollWrapper className={className}>
{isGridView ? (
<Grid cards={chunkedComponents} isSingleColumnGrid={isSingleColumnGrid} />
) : (
<Table rows={chunkedComponents} columns={columns} isLoading={isLoading} />
)}
</InfiniteScrollWrapper>
);
};
51 changes: 51 additions & 0 deletions src/components/AsyncTable/Column.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React, { createElement, ReactNode } from 'react';

import { startCase, toLower } from 'lodash';

import classNames from 'classnames/bind';
import styles from './alignments.module.scss';

const cx = classNames.bind(styles);

export interface Column {
id: string; // ID is required because header is optional
header?: string;
alignment: 'left' | 'right' | 'center';
className?: string;
}

interface ColumnProps {
alignment: Column['alignment'];
className?: string;
children: ReactNode;
}

interface ColumnElementProps {
element: 'th' | 'td';
}

export const TableHeader = ({ children, ...props }: ColumnProps) => {
return (
<ColumnElement element="th" {...props}>
{children}
</ColumnElement>
);
};

export const TableData = ({ children, ...props }: ColumnProps) => {
return (
<ColumnElement element="td" {...props}>
{children}
</ColumnElement>
);
};

const ColumnElement = ({ alignment, element, className, children }: ColumnProps & ColumnElementProps) => {
return createElement(
element,
{
className: classNames(cx(className, styles[startCase(toLower(alignment))]))
},
children
);
};
19 changes: 19 additions & 0 deletions src/components/AsyncTable/Grid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React, { memo, ReactNode } from 'react';

import { GridPlaceholders } from './Placeholder';

import styles from './AsyncTable.module.scss';

interface GridProps {
cards: ReactNode;
isSingleColumnGrid: boolean;
}

export const Grid = memo(({ cards, isSingleColumnGrid }: GridProps) => {
return (
<div className={styles.Grid}>
{cards}
{!isSingleColumnGrid && <GridPlaceholders amount={3} />}
</div>
);
});
45 changes: 45 additions & 0 deletions src/components/AsyncTable/Placeholder.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { TableData, TableHeader } from './Column';
import Skeleton from 'react-loading-skeleton';
import React from 'react';

interface HeaderPlaceholdersProps {
amount: number;
}

export const HeaderPlaceholders = ({ amount }: HeaderPlaceholdersProps) => {
const columnPlaceholder = (
<TableHeader alignment={'center'}>
<Skeleton width={'100%'} />
</TableHeader>
);

return <>{Array(amount).fill(columnPlaceholder)}</>;
};

interface RowPlaceholdersProps {
amount: number;
height: number;
numColumns: number;
}

export const RowPlaceholders = ({ amount, height, numColumns }: RowPlaceholdersProps) => {
const columnPlaceholder = (
<TableData alignment={'center'}>
<Skeleton width={'100%'} height={height} />
</TableData>
);

const columns = Array(numColumns).fill(columnPlaceholder);
const rows = Array(amount).fill(<tr>{columns}</tr>);
return <>{rows}</>;
};

interface GridPlaceholdersProps {
amount: number;
}

export const GridPlaceholders = ({ amount }: GridPlaceholdersProps) => {
const placeholders = Array(amount).fill(<div />);

return <>{placeholders}</>;
};
31 changes: 31 additions & 0 deletions src/components/AsyncTable/Table.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React, { memo, ReactNode } from 'react';

import { Column, TableHeader } from './Column';
import { HeaderPlaceholders, RowPlaceholders } from './Placeholder';

import styles from './AsyncTable.module.scss';

interface TableProps {
columns: Column[];
rows: ReactNode;
isLoading: boolean;
}

export const Table = memo(({ columns, rows, isLoading }: TableProps) => (
<table className={styles.Table}>
<thead>
<tr>
{isLoading ? (
<HeaderPlaceholders amount={columns.length} />
) : (
columns.map((c: Column) => (
<TableHeader alignment={c.alignment} key={c.id}>
{c.header}
</TableHeader>
))
)}
</tr>
</thead>
<tbody>{isLoading ? <RowPlaceholders amount={5} height={40} numColumns={columns.length} /> : rows}</tbody>
</table>
));
1 change: 1 addition & 0 deletions src/components/AsyncTable/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './AsyncTable';
export { TableData, TableHeader, Column } from './Column';
Loading

0 comments on commit 3c74997

Please sign in to comment.