Skip to content

Instantly share code, notes, and snippets.

@zigcccc
Last active November 3, 2022 08:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save zigcccc/a945a47d28f6a573a731e6069cda5ef6 to your computer and use it in GitHub Desktop.
Save zigcccc/a945a47d28f6a573a731e6069cda5ef6 to your computer and use it in GitHub Desktop.
Generic Pageable Table
// Organism (utilizes Table component which is a molecule, but is not strictly tied to a single feature)
/**
* The goal of this component is to:
* - render the table data
* - handle initial API call + any additional page and/or search related API calls
* - render table header & footer with pagination and actions
*/
const PageableTable = ({
columns,
components,
data,
filters,
hasError,
isLoading,
onFetchData,
onRowClick,
pagination,
tableProps: tablePropsBase,
title,
}) => {
const { values: params } = useFormState();
// Joined Table props
const tableProps = { ...tablePropsBase, columns, data, onRowClick };
const emptyResults = hasEmptyResults({ data, isLoading });
const emptySearchResults = hasEmptySearchResults({ data, isLoading, params });
// We are setting isSearching to true if we are in a loading state and the value
// for the search Query exists
const isSearching = isLoading && Boolean(params.searchKey);
// Props shared between all of the PageableTable child components
const commonProps = { isLoading, isSearching };
const handleRefresh = () => {
onFetchData({ page: pagination.currentPage, params });
};
const handleFetchPreviousPage = () => {
if (pagination.hasPreviousPage) {
onFetchData({ page: pagination.currentPage - 1, params });
}
};
const handleFetchNextPage = () => {
if (pagination.hasNextPage) {
onFetchData({ page: pagination.currentPage + 1, params });
}
};
return (
<div className="pageable-table">
<TableActionbar
{...commonProps}
filters={filters}
hasError={hasError}
onGoToNextPage={handleFetchNextPage}
onGoToPreviousPage={handleFetchPreviousPage}
onRefresh={handleRefresh}
pagination={pagination}
title={title}
/>
<PageableTableBody
{...commonProps}
{...tableProps}
components={components}
hasEmptyResults={emptyResults}
hasEmptySearchResults={emptySearchResults}
hasError={hasError}
onRefresh={handleRefresh}
params={params}
/>
<TableFooter
{...commonProps}
hasError={hasError}
onGoToNextPage={handleFetchNextPage}
onGoToPreviousPage={handleFetchPreviousPage}
pagination={pagination}
/>
</div>
);
};
// Sub-component of the PageableTable organism component
const PageableTableBody = ({
components,
hasEmptyResults,
hasEmptySearchResults,
hasError,
isLoading,
isSearching,
onRefresh,
params,
...rest
}) => {
// Components setup for different Table empty states. We use either component provided by the prop or the
// default one. We also memoize this component, since it will (almost) never change dynamically.
const TableLoadingState = React.useMemo(() => components?.LoadingState || DefaultTableLoadingState, [components]);
const TableErrorState = React.useMemo(() => components?.ErrorState || DefaultTableErrorState, [components]);
const TableEmptyResults = React.useMemo(() => components?.EmptyResults || DefaultTableEmptyResults, [components]);
/**
* The top-most check. Whenever we are in a loading or searching state, we want to display
* the table loading state component.
*/
if (isLoading || isSearching) {
return (
<TableBodyEmptyStateBase>
<TableLoadingState />
</TableBodyEmptyStateBase>
);
}
/**
* If we have an error (and we are currently not loading anything), we display the table
* error state component.
*/
if (hasError) {
return (
<TableBodyEmptyStateBase>
<TableErrorState onRefresh={onRefresh} />
</TableBodyEmptyStateBase>
);
}
/**
* If we have empty search results, we want to display empty search results
* state. IMPORTANT: This check must happen before the generic empty results check
* since by design, empty search results also means empty results, but not vice versa.
*/
if (hasEmptySearchResults || hasEmptyResults) {
return (
<TableBodyEmptyStateBase>
<TableEmptyResults onRefresh={onRefresh} searchKey={params?.searchKey} />
</TableBodyEmptyStateBase>
);
}
/**
* If none of the empty state conditions are met, we return
* the actual table component
*/
return <Table {...rest} />;
};
// System / template (strictly tied to "Products" feature)
const ProductsTable = ({ fetchProducts, products, ...rest }) => {
const history = useHistory();
const handleEditProductClick = (row) => {
history.push(getJoinedPath('products', row.original.id));
};
const columns = getProductsTableColumns({ onEditProduct: handleEditProductClick });
return (
<PageableTable
columns={columns}
components={{ ErrorState: ProductsTableErrorState, EmptyResults: ProductsTableEmptyState }}
data={products}
onFetchData={fetchProducts}
onRowClick={handleEditProductClick}
title="Seznam izdelkov"
{...rest}
/>
);
};
// Molecule (since it utilizes Text component, which is an atom)
const Table = ({ cellPaddingSize, columns, data, hideHeader, onRowClick }) => {
// Data passed to the useTable hook needs to be memoized
// to avaid performance leaks
const memoColumns = React.useMemo(() => columns, [columns]);
const memoData = React.useMemo(() => data, [data]);
const numOfColumns = React.useMemo(() => memoColumns.length, [memoColumns]);
const defaultTableCellSize = getDefaultTableCellSize(numOfColumns);
const handleRowClick = (row) => {
onRowClick?.(row);
};
const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } = useTable({
columns: memoColumns,
data: memoData,
});
return (
<div className="table">
<table className="table__table" {...getTableProps()}>
{!hideHeader && (
<thead className="table__head">
{headerGroups.map((headerGroup) => (
// Apply the header row props
<tr className="table__row" {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map((column) => (
<th
className={classNames('table__cell', cellPaddingSize, column.size || defaultTableCellSize, {
[column.align]: Boolean(column.align),
[`${column.alignHeader}--important`]: Boolean(column.alignHeader),
})}
{...column.getHeaderProps()}
>
<Text.Regular numOfLines={1}>{column.render('Header')}</Text.Regular>
</th>
))}
</tr>
))}
</thead>
)}
<tbody className="table__body" {...getTableBodyProps()}>
{rows.map((row) => {
// Prepare the row for display
prepareRow(row);
return (
// Apply the row props
<tr
className={classNames('table__row table__row--body', { 'table__row--clickable': Boolean(onRowClick) })}
onClick={() => handleRowClick(row)}
{...row.getRowProps()}
>
{row.cells.map((cell) => (
<td
className={classNames('table__cell', cellPaddingSize, cell.column.size || defaultTableCellSize, {
[cell.column.align]: Boolean(cell.column.align),
})}
{...cell.getCellProps()}
>
{cell.render('Cell')}
</td>
))}
</tr>
);
})}
</tbody>
</table>
</div>
);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment