Skip to content

Commit d17bdd1

Browse files
committed
feat: init project
0 parents  commit d17bdd1

23 files changed

+1333
-0
lines changed

.editorconfig

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
root = true
2+
3+
[*]
4+
indent_style = space
5+
indent_size = 2
6+
end_of_line = lf
7+
charset = utf-8
8+
trim_trailing_whitespace = true
9+
insert_final_newline = true

.gitignore

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Logs
2+
logs
3+
*.log
4+
npm-debug.log*
5+
yarn-debug.log*
6+
yarn-error.log*
7+
pnpm-debug.log*
8+
lerna-debug.log*
9+
10+
node_modules
11+
dist
12+
dist-ssr
13+
*.local
14+
15+
# Editor directories and files
16+
.vscode/*
17+
!.vscode/extensions.json
18+
.idea
19+
.DS_Store
20+
*.suo
21+
*.ntvs*
22+
*.njsproj
23+
*.sln
24+
*.sw?

index.html

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
<title>Vite + React + TS</title>
8+
</head>
9+
<body>
10+
<div id="root"></div>
11+
<script type="module" src="/src/main.tsx"></script>
12+
</body>
13+
</html>

package.json

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"name": "tanstack-table-editable",
3+
"private": true,
4+
"version": "0.0.0",
5+
"scripts": {
6+
"dev": "vite",
7+
"build": "tsc && vite build",
8+
"preview": "vite preview"
9+
},
10+
"dependencies": {
11+
"@tanstack/react-table": "^8.7.9",
12+
"clsx": "^1.2.1",
13+
"nanoid": "^4.0.1",
14+
"react": "^18.2.0",
15+
"react-dom": "^18.2.0"
16+
},
17+
"devDependencies": {
18+
"@types/react": "^18.0.27",
19+
"@types/react-dom": "^18.0.10",
20+
"@vitejs/plugin-react-swc": "^3.0.0",
21+
"typescript": "^4.9.3",
22+
"vite": "^4.1.0"
23+
}
24+
}

prettier.config.js

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/** @type {import("prettier").Config} */
2+
module.exports = {
3+
singleQuote: true,
4+
printWidth: 120,
5+
semi: false,
6+
trailingComma: "all",
7+
};

public/vite.svg

+1
Loading

src/App.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function App() {
2+
return <></>
3+
}

src/Columns.tsx

+161
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import { createColumnHelper } from '@tanstack/react-table'
2+
import clsx from 'clsx'
3+
import { nanoid } from 'nanoid'
4+
5+
export type Overview = {
6+
latestDate: string
7+
site: string
8+
electricCompareYear: number
9+
electricCurrentYear: number
10+
electricWeight: number
11+
electricGradient: number
12+
waterUseCompareYear: number
13+
waterUseCurrentYear: number
14+
waterUseWeight: number
15+
waterUseGradient: number
16+
revenueCompareYear: number
17+
revenueCurrentYear: number
18+
revenueWeight: number
19+
revenueGradient: number
20+
ASPCompareYear: number
21+
ASPCurrentYear: number
22+
ASPWeight: number
23+
ASPGradient: number
24+
plants?: Overview[]
25+
}
26+
27+
const columnHelper = createColumnHelper<Overview>()
28+
29+
export const getColumns = ({ footer, latestDate }: { footer?: Overview; latestDate: Date }) => {
30+
const currYear = latestDate.getFullYear()
31+
const lastYear = currYear - 1
32+
33+
return [
34+
columnHelper.group({
35+
id: nanoid(),
36+
header: () => <span>Site</span>,
37+
columns: [
38+
columnHelper.accessor('site', {
39+
cell: (info) => info.getValue(),
40+
footer: () => <span>{footer?.site}</span>,
41+
meta: {
42+
header: { isPlaceholder: true },
43+
cell: { className: clsx('whitespace-nowrap text-center') },
44+
footer: { className: clsx('text-center') },
45+
},
46+
}),
47+
],
48+
meta: {
49+
header: { rowSpan: 2 },
50+
},
51+
}),
52+
columnHelper.group({
53+
id: nanoid(),
54+
header: () => <span>Electricity Consumption (kWh)</span>,
55+
columns: [
56+
columnHelper.accessor('electricCompareYear', {
57+
header: () => <span>{lastYear}</span>,
58+
cell: (info) => <NumericFormat value={info.getValue()} />,
59+
footer: () => <NumericFormat value={footer?.electricCompareYear} />,
60+
}),
61+
columnHelper.accessor('electricCurrentYear', {
62+
header: () => <span>{currYear}</span>,
63+
cell: (info) => <NumericFormat value={info.getValue()} />,
64+
footer: () => <NumericFormat value={footer?.electricCurrentYear} />,
65+
}),
66+
columnHelper.accessor('electricWeight', {
67+
header: () => <span>Weight</span>,
68+
cell: (info) => <NumericFormat value={info.getValue()} unit={1e-2} suffix="%" />,
69+
footer: () => <NumericFormat value={footer?.electricWeight} unit={1e-2} suffix="%" />,
70+
}),
71+
columnHelper.accessor('electricGradient', {
72+
header: () => <span>Gap *</span>,
73+
cell: (info) => <NumericFormat value={info.getValue()} unit={1e-2} suffix="%" />,
74+
footer: () => <NumericFormat value={footer?.electricGradient} unit={1e-2} suffix="%" />,
75+
}),
76+
],
77+
}),
78+
columnHelper.group({
79+
id: nanoid(),
80+
header: () => <span>Water Consumption (Ton)</span>,
81+
columns: [
82+
columnHelper.accessor('waterUseCompareYear', {
83+
header: () => <span>{lastYear}</span>,
84+
cell: (info) => <NumericFormat value={info.getValue()} />,
85+
footer: () => <NumericFormat value={footer?.waterUseCompareYear} />,
86+
}),
87+
columnHelper.accessor('waterUseCurrentYear', {
88+
header: () => <span>{currYear}</span>,
89+
cell: (info) => <NumericFormat value={info.getValue()} />,
90+
footer: () => <NumericFormat value={footer?.waterUseCurrentYear} />,
91+
}),
92+
columnHelper.accessor('waterUseWeight', {
93+
header: () => <span>Weight</span>,
94+
cell: (info) => <NumericFormat value={info.getValue()} unit={1e-2} suffix="%" />,
95+
footer: () => <NumericFormat value={footer?.waterUseWeight} unit={1e-2} suffix="%" />,
96+
}),
97+
columnHelper.accessor('waterUseGradient', {
98+
header: () => <span>Gap *</span>,
99+
cell: (info) => <NumericFormat value={info.getValue()} unit={1e-2} suffix="%" />,
100+
footer: () => <NumericFormat value={footer?.waterUseGradient} unit={1e-2} suffix="%" />,
101+
}),
102+
],
103+
}),
104+
columnHelper.group({
105+
id: nanoid(),
106+
header: () => <span>Revenue (Billion NTD)</span>,
107+
columns: [
108+
columnHelper.accessor('revenueCompareYear', {
109+
header: () => <span>{lastYear}</span>,
110+
cell: (info) => <NumericFormat value={info.getValue()} precision={3} />,
111+
footer: () => <NumericFormat value={footer?.revenueCompareYear} precision={3} />,
112+
}),
113+
columnHelper.accessor('revenueCurrentYear', {
114+
header: () => <span>{currYear}</span>,
115+
cell: (info) => <NumericFormat value={info.getValue()} precision={3} />,
116+
footer: () => <NumericFormat value={footer?.revenueCurrentYear} precision={3} />,
117+
}),
118+
columnHelper.accessor('revenueWeight', {
119+
header: () => <span>Weight</span>,
120+
cell: (info) => <NumericFormat value={info.getValue()} unit={1e-2} suffix="%" />,
121+
footer: () => <NumericFormat value={footer?.revenueWeight} unit={1e-2} suffix="%" />,
122+
}),
123+
columnHelper.accessor('revenueGradient', {
124+
header: () => <span>Gap *</span>,
125+
cell: (info) => <NumericFormat value={info.getValue()} unit={1e-2} suffix="%" />,
126+
footer: () => <NumericFormat value={footer?.revenueGradient} unit={1e-2} suffix="%" />,
127+
}),
128+
],
129+
}),
130+
columnHelper.group({
131+
id: nanoid(),
132+
header: () => <span>ASP (Thousand NTD / Product)</span>,
133+
columns: [
134+
columnHelper.accessor('ASPCompareYear', {
135+
header: () => <span>{lastYear}</span>,
136+
cell: (info) => <NumericFormat value={info.getValue()} precision={3} />,
137+
footer: () => <NumericFormat value={footer?.ASPCompareYear} precision={3} />,
138+
}),
139+
columnHelper.accessor('ASPCurrentYear', {
140+
header: () => <span>{currYear}</span>,
141+
cell: (info) => <NumericFormat value={info.getValue()} precision={3} />,
142+
footer: () => <NumericFormat value={footer?.ASPCurrentYear} precision={3} />,
143+
}),
144+
columnHelper.accessor('ASPGradient', {
145+
header: () => <span>Gap *</span>,
146+
cell: (info) => <NumericFormat value={info.getValue()} unit={1e-2} suffix="%" />,
147+
footer: () => <NumericFormat value={footer?.ASPGradient} unit={1e-2} suffix="%" />,
148+
}),
149+
],
150+
}),
151+
]
152+
}
153+
154+
function NumericFormat({ value = 0, unit = 1, suffix = '', precision = 0 }) {
155+
return (
156+
<span>
157+
{(value / unit).toLocaleString('en-US', { maximumFractionDigits: precision, minimumFractionDigits: precision })}
158+
{suffix}
159+
</span>
160+
)
161+
}

src/assets/react.svg

+1
Loading

src/context-table/ActionCell.tsx

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { useContext } from 'react'
2+
3+
import { TableContext } from './TableContext'
4+
5+
import type { TableContextProps } from './TableContext'
6+
7+
type ActionCellProps = {
8+
children?: React.ReactNode | ((context: TableContextProps) => React.ReactNode)
9+
}
10+
11+
export default function ActionCell({ children }: ActionCellProps) {
12+
const context = useContext(TableContext)
13+
14+
return <>{typeof children === 'function' ? children(context) : children}</>
15+
}

src/context-table/EditableCell.tsx

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { useContext, useEffect, useState } from 'react'
2+
3+
import { TableContext } from './TableContext'
4+
5+
import type { CellContext } from '@tanstack/react-table'
6+
7+
export default function EditableCell<T>({ getValue, row, column }: CellContext<T, unknown>) {
8+
const { updateData, getIsEditing } = useContext(TableContext)
9+
10+
const initialValue = getValue()
11+
12+
// We need to keep and update the state of the cell normally
13+
const [value, setValue] = useState(initialValue)
14+
15+
// When the input is blurred, we'll call our table meta's updateData function
16+
const onBlur = () => {
17+
updateData(row.index, column.id, value)
18+
}
19+
20+
// If the initialValue is changed external, sync it up with our state
21+
useEffect(() => {
22+
setValue(initialValue)
23+
}, [initialValue])
24+
25+
return (
26+
<>
27+
{getIsEditing(row.index) ? (
28+
<input value={value as string} onChange={(e) => setValue(e.target.value)} onBlur={onBlur} />
29+
) : (
30+
<div>{value as string}</div>
31+
)}
32+
</>
33+
)
34+
}

src/context-table/EditableTable.tsx

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { getCoreRowModel, flexRender } from '@tanstack/react-table'
2+
3+
import EditableCell from './EditableCell'
4+
import { TableContext } from './TableContext'
5+
import { useEditableTable } from './hooks'
6+
7+
import type { ColumnDef, ColumnDefBase } from '@tanstack/react-table'
8+
9+
export type EditableTableProps<T> = {
10+
data: T[]
11+
columns: ColumnDef<T, unknown>[] & ColumnDefBase<T, unknown>[]
12+
}
13+
14+
const defaultColumn = {
15+
cell: EditableCell,
16+
}
17+
18+
export default function EditableTable<T>({ data, columns }: EditableTableProps<T>) {
19+
const { table, updateData, setIsEditing, getIsEditing, onCancel, onSave, onDelete } = useEditableTable({
20+
data,
21+
columns,
22+
defaultColumn,
23+
getCoreRowModel: getCoreRowModel(),
24+
})
25+
26+
return (
27+
<TableContext.Provider value={{ updateData, setIsEditing, getIsEditing, onCancel, onSave, onDelete }}>
28+
<div className="relative flex flex-col overflow-auto rounded-t-lg shadow-lg">
29+
<table className="w-full border-separate border-spacing-0 text-right">
30+
<thead>
31+
{table.getHeaderGroups().map((headerGroup) => (
32+
<tr key={headerGroup.id}>
33+
{headerGroup.headers.map((header) => {
34+
return (
35+
<th key={header.id} colSpan={header.colSpan}>
36+
{header.isPlaceholder ? null : (
37+
<div>{flexRender(header.column.columnDef.header, header.getContext())}</div>
38+
)}
39+
</th>
40+
)
41+
})}
42+
</tr>
43+
))}
44+
</thead>
45+
<tbody>
46+
{table.getRowModel().rows.map((row) => {
47+
return (
48+
<tr key={row.id}>
49+
{row.getVisibleCells().map((cell) => {
50+
return <td key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</td>
51+
})}
52+
</tr>
53+
)
54+
})}
55+
</tbody>
56+
</table>
57+
</div>
58+
</TableContext.Provider>
59+
)
60+
}

src/context-table/TableContext.ts

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { createContext } from 'react'
2+
3+
export type TableContextProps = {
4+
updateData: (rowIndex: number, columnId: string, value: unknown) => void
5+
setIsEditing: (rowIndex: number, isEditing: boolean) => void
6+
getIsEditing: (rowIndex: number) => boolean
7+
onCancel: (rowIndex: number) => void
8+
onSave: (rowIndex: number) => void
9+
onDelete: (rowIndex: number) => void
10+
}
11+
12+
export const TableContext = createContext<TableContextProps>({
13+
updateData: () => {},
14+
setIsEditing: () => {},
15+
getIsEditing: () => false,
16+
onCancel: () => {},
17+
onSave: () => {},
18+
onDelete: () => {},
19+
})

0 commit comments

Comments
 (0)