-
Notifications
You must be signed in to change notification settings - Fork 751
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* build infra of Table component * add scrollbar * - create generic table - make simple example on story book * fix: lint warnings * fix: build error * fix: update feedback from hachiojidev * fix: update feedback * fix: update type of value The purpose of using value is to implement search feature ( by string ) * fix: lint issue fix on storybook component * fix: remove lint disable line * fix: remove eslint disable lines Co-authored-by: Ranonpro <59729252+ranon127@users.noreply.github.com>
- Loading branch information
Showing
7 changed files
with
547 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
export const columns = [ | ||
{ | ||
id: 1, | ||
name: "id", | ||
hidden: true, | ||
}, | ||
{ | ||
id: 2, | ||
name: "pool", | ||
label: "POOL", | ||
render: ({ value }: { value: React.ReactNode }): React.ReactNode => value, | ||
}, | ||
{ | ||
id: 3, | ||
name: "apy", | ||
label: "APY", | ||
}, | ||
{ | ||
id: 4, | ||
name: "EARNED", | ||
}, | ||
{ | ||
id: 5, | ||
name: "STAKED", | ||
}, | ||
{ | ||
id: 6, | ||
name: "DETAILS", | ||
}, | ||
{ | ||
id: 7, | ||
name: "LINKS", | ||
}, | ||
{ | ||
id: 8, | ||
name: "TAGS", | ||
}, | ||
]; | ||
|
||
export const data = []; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import styled from "styled-components"; | ||
|
||
const StyledTh = styled.th` | ||
background: #eff4f5; | ||
padding: 8px; | ||
font-size: 12px; | ||
color: #8f80ba; | ||
&:first-child { | ||
border-top-left-radius: 4px; | ||
border-bottom-right-radius: 4px; | ||
padding-left: 16px; | ||
} | ||
&:last-child { | ||
border-top-right-radius: 4px; | ||
border-bottom-left-radius: 4px; | ||
padding-right: 16px; | ||
} | ||
`; | ||
|
||
export default StyledTh; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,258 @@ | ||
import { useMemo, useReducer, useEffect, ReactNode } from "react"; | ||
import { | ||
ColumnByNamesType, | ||
ColumnType, | ||
TableState, | ||
TableAction, | ||
DataType, | ||
UseTableReturnType, | ||
UseTableOptionsType, | ||
RowType, | ||
HeaderType, | ||
ColumnStateType, | ||
HeaderRenderType, | ||
} from "./types"; | ||
import { byTextAscending, byTextDescending } from "./utils"; | ||
|
||
const sortByColumn = <T extends DataType>( | ||
data: RowType<T>[], | ||
sortColumn: string, | ||
columns: ColumnStateType<T>[] | ||
): RowType<T>[] => { | ||
let isAscending = null; | ||
let sortedRows: RowType<T>[] = [...data]; | ||
|
||
columns.forEach((column) => { | ||
// if the row was found | ||
if (sortColumn === column.name) { | ||
isAscending = column.sorted.asc; | ||
|
||
if (column.sort) { | ||
sortedRows = isAscending ? data.sort(column.sort) : data.sort(column.sort).reverse(); | ||
// default to sort by string | ||
} else { | ||
sortedRows = isAscending | ||
? data.sort(byTextAscending((object) => object.original[sortColumn])) | ||
: data.sort(byTextDescending((object) => object.original[sortColumn])); | ||
} | ||
} | ||
}); | ||
|
||
return sortedRows; | ||
}; | ||
|
||
const getPaginatedData = <T extends DataType>(rows: RowType<T>[], perPage: number, page: number) => { | ||
const start = (page - 1) * perPage; | ||
const end = start + perPage; | ||
return rows.slice(start, end); | ||
}; | ||
|
||
const getColumnsByName = <T extends DataType>(columns: ColumnType<T>[]): ColumnByNamesType<T> => { | ||
const columnsByName: ColumnByNamesType<T> = {}; | ||
columns.forEach((column) => { | ||
const col: ColumnType<T> = { | ||
id: column.id, | ||
name: column.name, | ||
label: column.label, | ||
}; | ||
|
||
if (column.render) { | ||
col.render = column.render; | ||
} | ||
col.hidden = column.hidden; | ||
columnsByName[column.name] = col; | ||
}); | ||
|
||
return columnsByName; | ||
}; | ||
|
||
const sortDataInOrder = <T extends DataType>(data: T[], columns: ColumnType<T>[]): T[] => { | ||
return data.map((row: T) => { | ||
const newRow: DataType = {}; | ||
columns.forEach((column) => { | ||
if (!(column.name in row)) { | ||
throw new Error(`Invalid row data, ${column.name} not found`); | ||
} | ||
newRow[column.name] = row[column.name]; | ||
}); | ||
return newRow as T; | ||
}); | ||
}; | ||
|
||
const makeRender = <T extends DataType, K>( | ||
valueT: K, | ||
render: (({ value, row }: { value: K; row: T }) => ReactNode) | undefined, | ||
row: T | ||
) => { | ||
return render ? () => render({ row, value: valueT }) : () => valueT; | ||
}; | ||
|
||
const makeHeaderRender = (label: string, render?: HeaderRenderType<string>) => { | ||
return render ? () => render({ label }) : () => label; | ||
}; | ||
export const createReducer = <T extends DataType>() => ( | ||
state: TableState<T>, | ||
action: TableAction<T> | ||
): TableState<T> => { | ||
switch (action.type) { | ||
case "SET_ROWS": { | ||
let rows = [...action.data]; | ||
// preserve sorting if a sort is already enabled when data changes | ||
if (state.sortColumn) { | ||
rows = sortByColumn(action.data, state.sortColumn, state.columns); | ||
} | ||
|
||
if (state.paginationEnabled === true) { | ||
rows = getPaginatedData(rows, state.pagination.perPage, state.pagination.page); | ||
} | ||
|
||
if (state.paginationEnabled === true) { | ||
rows = getPaginatedData(rows, state.pagination.perPage, state.pagination.page); | ||
} | ||
|
||
return { | ||
...state, | ||
rows, | ||
originalRows: action.data, | ||
}; | ||
} | ||
|
||
case "GLOBAL_FILTER": { | ||
const filteredRows = action.filter(state.originalRows); | ||
const selectedRowsById: { [key: number]: boolean } = {}; | ||
state.selectedRows.forEach((row) => { | ||
selectedRowsById[row.id] = !!row.selected; | ||
}); | ||
|
||
return { | ||
...state, | ||
rows: filteredRows.map((row) => { | ||
return selectedRowsById[row.id] ? { ...row, selected: selectedRowsById[row.id] } : { ...row }; | ||
}), | ||
filterOn: true, | ||
}; | ||
} | ||
case "SEARCH_STRING": { | ||
const stateCopySearch = { ...state }; | ||
stateCopySearch.rows = stateCopySearch.originalRows.filter((row) => { | ||
return ( | ||
row.cells.filter((cell) => { | ||
if (cell.value.includes(action.searchString)) { | ||
return true; | ||
} | ||
return false; | ||
}).length > 0 | ||
); | ||
}); | ||
return stateCopySearch; | ||
} | ||
default: | ||
throw new Error("Invalid reducer action"); | ||
} | ||
}; | ||
|
||
export const useTable = <T extends DataType>( | ||
columns: ColumnType<T>[], | ||
data: T[], | ||
options?: UseTableOptionsType<T> | ||
): UseTableReturnType<T> => { | ||
const columnsWithSorting: ColumnStateType<T>[] = useMemo( | ||
() => | ||
columns.map((column) => { | ||
return { | ||
...column, | ||
label: column.label ? column.label : column.name, | ||
hidden: column.hidden ? column.hidden : false, | ||
sort: column.sort, | ||
sorted: { | ||
on: false, | ||
}, | ||
}; | ||
}), | ||
[columns] | ||
); | ||
const columnsByName = useMemo(() => getColumnsByName(columnsWithSorting), [columnsWithSorting]); | ||
|
||
const tableData: RowType<T>[] = useMemo(() => { | ||
const sortedData = sortDataInOrder(data, columnsWithSorting); | ||
|
||
const newData = sortedData.map((row, idx) => { | ||
return { | ||
id: idx, | ||
selected: false, | ||
hidden: false, | ||
original: row, | ||
cells: Object.entries(row) | ||
.map(([column, value]) => { | ||
return { | ||
hidden: columnsByName[column].hidden, | ||
field: column, | ||
value, | ||
render: makeRender(value, columnsByName[column].render, row), | ||
}; | ||
}) | ||
.filter((cell) => !cell.hidden), | ||
}; | ||
}); | ||
return newData; | ||
}, [data, columnsWithSorting, columnsByName]); | ||
|
||
const reducer = createReducer<T>(); | ||
|
||
const [state, dispatch] = useReducer(reducer, { | ||
columns: columnsWithSorting, | ||
columnsByName, | ||
originalRows: tableData, | ||
rows: tableData, | ||
selectedRows: [], | ||
toggleAllState: false, | ||
filterOn: !!options?.filter, | ||
sortColumn: null, | ||
paginationEnabled: !!options?.pagination, | ||
pagination: { | ||
page: 1, | ||
perPage: 10, | ||
canNext: true, | ||
canPrev: false, | ||
nextPage: () => { | ||
// nextPage feature | ||
}, | ||
prevPage: () => { | ||
// prevPage feature | ||
}, | ||
}, | ||
}); | ||
|
||
useEffect(() => { | ||
dispatch({ type: "SET_ROWS", data: tableData }); | ||
}, [tableData]); | ||
|
||
const headers: HeaderType<T>[] = useMemo(() => { | ||
return [ | ||
...state.columns.map((column) => { | ||
const label = column.label ? column.label : column.name; | ||
return { | ||
...column, | ||
render: makeHeaderRender(label, column.headerRender), | ||
}; | ||
}), | ||
]; | ||
}, [state.columns]); | ||
|
||
useEffect(() => { | ||
if (options?.filter) { | ||
dispatch({ type: "GLOBAL_FILTER", filter: options.filter }); | ||
} | ||
}, [options?.filter]); | ||
|
||
return { | ||
headers: headers.filter((column) => !column.hidden), | ||
rows: state.rows, | ||
originalRows: state.originalRows, | ||
selectedRows: state.selectedRows, | ||
dispatch, | ||
setSearchString: (searchString: string) => dispatch({ type: "SEARCH_STRING", searchString }), | ||
pagination: state.pagination, | ||
toggleAllState: state.toggleAllState, | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import React, { useMemo } from "react"; | ||
import { useTable, ColumnType } from "./index"; | ||
import { data, columns } from "./example/const"; | ||
import StyledTh from "./example/header"; | ||
import { DataType } from './types' | ||
|
||
const Table = <T extends DataType>({ _columns, _data }: { _columns: ColumnType<T>[]; _data: T[] }) => { | ||
const { headers, rows } = useTable(_columns, _data, { | ||
sortable: true, | ||
}); | ||
|
||
return ( | ||
<table> | ||
<thead> | ||
<tr> | ||
{headers.map((header) => ( | ||
<StyledTh | ||
key={`header-${header.id}`} | ||
data-testid={`column-${header.name}`} | ||
> | ||
{header.label} | ||
|
||
{header.sorted && header.sorted.on ? <span data-testid={`sorted-${header.name}`} /> : null} | ||
</StyledTh> | ||
))} | ||
</tr> | ||
</thead> | ||
<tbody> | ||
{rows.map((row) => ( | ||
<tr data-testid={`row-${row.id}`} key={row.id}> | ||
{row.cells.map((cell) => ( | ||
<td>{cell.render()}</td> | ||
))} | ||
</tr> | ||
))} | ||
</tbody> | ||
</table> | ||
); | ||
}; | ||
|
||
const TableComponent: React.FunctionComponent = () => { | ||
const memoColumns = useMemo(() => columns, []); | ||
const memoData = useMemo(() => data, []); | ||
|
||
return ( | ||
<Table _columns={memoColumns} _data={memoData} /> | ||
); | ||
} | ||
|
||
export default { | ||
title: "Components/Table", | ||
component: TableComponent, | ||
argTypes: {}, | ||
}; | ||
|
||
export const Default: React.FC = () => { | ||
return ( | ||
<div style={{ width: "500px" }}> | ||
<TableComponent /> | ||
</div> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export * from "./hooks"; | ||
export * from "./types"; | ||
export * from "./utils"; |
Oops, something went wrong.