├── .gitignore ├── .npmignore ├── .prettierrc ├── LICENSE ├── README.md ├── builders ├── common.dist.js ├── common.js ├── esm.dist.js └── esm.js ├── examples └── next │ ├── .gitignore │ ├── next-env.d.ts │ ├── package.json │ ├── pages │ ├── _app.tsx │ ├── custom-props.tsx │ ├── index.tsx │ ├── init.css │ └── simple.tsx │ ├── tsconfig.json │ └── yarn.lock ├── package.json ├── src └── index.tsx ├── tsconfig.es5.json ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | DS_Store 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | examples 3 | .prettierrc 4 | tsconfig.es5.json 5 | DS_Store 6 | builders 7 | yarn.lock -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "tabWidth": 4, 4 | "semi": false, 5 | "trailingComma": "none", 6 | "singleQuote": true 7 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 SaltyAom 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @saltyaom/react-table 2 | Declarative React Table under 1kb. 3 | 4 | ![Polka Drake](https://user-images.githubusercontent.com/35027979/128559117-6cc3adcb-daf9-4bdd-8778-0abda552d5a1.jpg) 5 | ##### Humanity Restored 6 | 7 | ## Feature 8 | - No dependencies. 9 | - Light, 700 bytes on production. 10 | - Easy to understand, declarative. 11 | - Automatic key management. 12 | - Full control over table. 13 | - Full TypeScript support. 14 | 15 | ## Size 16 | Should be around 700 bytes, checkout [Bundlephobia](https://bundlephobia.com/package/@saltyaom/react-table) for accurate result. 17 | 18 | ## Getting start 19 | ```bash 20 | yarn add @saltyaom/react-table 21 | 22 | // Or npm 23 | npm install @saltyaom/react-table --save 24 | ``` 25 | 26 | ## Example 27 | ```jsx 28 | import Table from '@saltyaom/react-table' 29 | 30 | const Example = () => { 31 | return ( 32 | 40 | ) 41 | } 42 | ``` 43 | 44 | ## Why 45 | Compose React table in a simple, elegant way. 46 | 47 | Creating table in React is complicate. 48 | 49 | Let create a simple table from the following data. 50 | 51 | | name | type | value | 52 | | ------ | ---- | ----- | 53 | | Okayu | cat | 1 | 54 | | Korone | dog | -1 | 55 | 56 | Where the requirement is: 57 | - First field on table head is bold. 58 | - Value field must be color by the following: 59 | - if value >= 0, return green 60 | - otherwise, return red 61 | 62 | Implement on normal React would be like: 63 | ```jsx 64 | const VTuberTable = () => { 65 | const header = ['name', 'description', 'value'] 66 | const data = [ 67 | ['Okayu' , 'cat', 10], 68 | ['Korone', 'dog', -1] 69 | ] 70 | 71 | return ( 72 |
73 | 74 | 75 | {header.map((title, index) => { 76 | if(index === 0) return 77 | 78 | return ( 79 | 80 | ) 81 | })} 82 | 83 | 84 | 85 | {data.map(row => 86 | 87 | {row.map((data, index) => { 88 | if(data === 2) 89 | if(data >= 0) 90 | return ( 91 | 97 | ) 98 | else 99 | return ( 100 | 106 | ) 107 | 108 | return ( 109 | 115 | ) 116 | })} 117 | 118 | )} 119 | 120 |
{title}{title}
95 | {data} 96 | 104 | {data} 105 | 113 | {data} 114 |
121 | ) 122 | } 123 | ``` 124 | 125 | The problem is: 126 | - The code is very long. 127 | - Semantic table require a lot of boilerplate, thead, tr, table. 128 | - Hard to understand, imperative. 129 | - `key` managment is complex. 130 | 131 | ## Entering @saltyaom/react-table 132 | `@saltyaom/react-table` is a simple, declarative way to compose table in React. 133 | 134 | All you need to do is specified your data and key, you can bring your className anywhere, even a custom condition for class. 135 | 136 | In the other word, you have full control over the table even in a declarative way. 137 | 138 | Let's re-implement previous table in `@saltyaom/react-table`. 139 | 140 | ```jsx 141 | import Table from '@saltyaom/react-table' 142 | 143 | const VTuberTable = () => { 144 | const header = ['name', 'description', 'value'] 145 | const data = [ 146 | ['Okayu' , 'cat', 10], 147 | ['Korone', 'dog', -1] 148 | ] 149 | 150 | return ( 151 | element 160 | allThClassName="title" 161 | // Apply 'bold' to index 0 ` in order. 189 | * 190 | * @example 191 | * ['name', 'description'] 192 | */ 193 | header: T 194 | 195 | /** 196 | * Data to be appear for each row `
162 | thClassName={['bold']} 163 | 164 | tdClassName={[ 165 | '', 166 | '', 167 | // On index 2, apply custom condition 168 | (value: number) => value >= 0 ? 'green' : 'red' 169 | ]} 170 | /> 171 | ) 172 | } 173 | ``` 174 | 175 | That's it, we have a simple happy ending for composing table in React. 176 | 177 | ## Documentation 178 | Table is the only export and is default export from `@saltyaom/react-table`. 179 | 180 | The acceptable props is: 181 | ```typescript 182 | export interface ITable< 183 | T = 184 | | (string | number | JSX.Element)[] 185 | | readonly (string | number | JSX.Element)[] 186 | > { 187 | /** 188 | * Table header to be appear in `
` in order. 197 | * 198 | * @example 199 | * [ 200 | * ['Korone', 'Dog'], 201 | * ['Okayu', 'Cat'] 202 | * ] 203 | */ 204 | data: T[] | readonly T[] 205 | 206 | /** 207 | * Key of data, can be either `string` which match in `header` or number as index. 208 | * 209 | * @example 210 | * 0 211 | * 212 | * @example 213 | * 'name' 214 | */ 215 | dataKey?: string | number 216 | 217 | /** 218 | * className of wrapper of `` 219 | * (element: `
`) 220 | */ 221 | wrapperClassName?: string 222 | /** 223 | * className of `
` 224 | */ 225 | className?: string 226 | /** 227 | * Width of each cell in order from left to right. 228 | * 229 | * @example 230 | * [80, 160] 231 | */ 232 | cellsWidth?: number[] 233 | 234 | /** 235 | * className of `` 236 | */ 237 | theadClassName?: string 238 | /** 239 | * className of `` 249 | */ 250 | tbodyClassName?: string 251 | /** 252 | * className of `` 287 | */ 288 | allTdClassName?: string 289 | 290 | /** 291 | * Prepend element before table 292 | * 293 | * @example 294 | * 302 | */ 303 | beforeTable?: JSX.Element 304 | /** 305 | * Append element after table 306 | * 307 | * @example 308 | *
309 | * 310 | * 311 | *
312 | */ 313 | afterTable?: JSX.Element 314 | 315 | /** 316 | * Add custom props to `
` 240 | */ 241 | thClassName?: string[] 242 | /** 243 | * className to apply to all `` 244 | */ 245 | allThClassName?: string 246 | 247 | /** 248 | * className of `
` 253 | * 254 | * Can be either `string` or `function()` which accepts `([valueof data:, index: number])` 255 | * 256 | * @example 257 | * w-8 258 | * 259 | * @example 260 | // If nothing is returned, fallback to '' 261 | * (rowData, index) => { 262 | * if(rowData.value === 0) return 'bg-blue-50' 263 | * if(rowData.index === 0) return 'bg-red-50' 264 | * } 265 | */ 266 | trClassName?: string | ((data: readonly T[], index: number) => string) 267 | /** 268 | * className of `` 269 | * 270 | * Can be either `string[]` or `function()` which accepts `([valueof data:, index: number])` 271 | * 272 | * @example 273 | * ['w-8', 'w-16'] 274 | * 275 | * @example 276 | // If nothing is returned, fallback to '' 277 | * (value, index) => { 278 | * if(value === 0) return 'text-red-500' 279 | * if(index === 0) return 'text-blue-500' 280 | * } 281 | */ 282 | tdClassName?: 283 | | (string | ((data: any, index: number) => string))[] 284 | | ((data: string, index: number) => string) 285 | /** 286 | * className to apply to all `
` element 317 | * 318 | * @example 319 | * { 320 | * style={ 321 | * borderCollapse: 'collapse' 322 | * } 323 | * } 324 | */ 325 | tableProps?: Omit< 326 | DetailedHTMLProps< 327 | TableHTMLAttributes, 328 | HTMLTableElement 329 | >, 330 | 'className' 331 | > 332 | 333 | /** 334 | * Add custom props to `` element 335 | * 336 | * @example 337 | * { 338 | * onClick: () => console.log("Clicked") 339 | * } 340 | */ 341 | theadProps?: Omit< 342 | DetailedHTMLProps< 343 | HTMLAttributes, 344 | HTMLTableSectionElement 345 | >, 346 | 'className' 347 | > 348 | /** 349 | * Add custom props to `` element 368 | * 369 | * @example 370 | * { 371 | * onClick: (data, index) => console.log("Clicked") 372 | * } 373 | */ 374 | tbodyProps?: Omit< 375 | DetailedHTMLProps< 376 | HTMLAttributes, 377 | HTMLTableSectionElement 378 | >, 379 | 'className' 380 | > 381 | /** 382 | * Add custom props to `` element 383 | * 384 | * @example 385 | * (data, index) => { 386 | * if(isOdd(index)) return ({ className: '--odd' }) 387 | * } 388 | */ 389 | trProps?: ( 390 | row: T, 391 | index: number 392 | ) => Omit< 393 | DetailedHTMLProps< 394 | HTMLAttributes, 395 | HTMLTableRowElement 396 | >, 397 | 'className' 398 | > | void 399 | /** 400 | * Add custom props to `
` element 350 | * 351 | * @example 352 | * (data, index) => { 353 | * if(isOdd(index)) return ({ className: '--odd' }) 354 | * } 355 | */ 356 | thProps?: ( 357 | data: T[keyof T], 358 | index: number 359 | ) => Omit< 360 | DetailedHTMLProps< 361 | ThHTMLAttributes, 362 | HTMLTableHeaderCellElement 363 | >, 364 | 'className' 365 | > | void 366 | /** 367 | * Add custom props to `
` element 401 | * 402 | * @example 403 | * (data, { column, row }) => ({ 404 | * onClick: () => console.log(column, row) 405 | * }) 406 | */ 407 | tdProps?: ( 408 | data: T[keyof T], 409 | indexes: { 410 | column: number 411 | row: number 412 | } 413 | ) => Omit< 414 | DetailedHTMLProps< 415 | TdHTMLAttributes, 416 | HTMLTableDataCellElement 417 | >, 418 | 'className' 419 | > | void 420 | } 421 | ``` 422 | 423 | For more information, you can looks directly in the source code as it's very easy to read. 424 | -------------------------------------------------------------------------------- /builders/common.dist.js: -------------------------------------------------------------------------------- 1 | require('esbuild') 2 | .build({ 3 | entryPoints: ['./src/index.tsx'], 4 | outdir: './build/dist/cjs', 5 | format: 'cjs', 6 | bundle: true, 7 | minify: true, 8 | sourcemap: 'external', 9 | external: ['react'], 10 | keepNames: false, 11 | target: ['es2019'] 12 | }) 13 | .catch(() => process.exit(1)) 14 | -------------------------------------------------------------------------------- /builders/common.js: -------------------------------------------------------------------------------- 1 | require('esbuild') 2 | .build({ 3 | entryPoints: ['./src/index.tsx'], 4 | outdir: './build', 5 | format: 'cjs', 6 | bundle: true, 7 | minify: false, 8 | sourcemap: 'external', 9 | external: ['react'], 10 | keepNames: false, 11 | target: ['es2019'] 12 | }) 13 | .catch(() => process.exit(1)) 14 | -------------------------------------------------------------------------------- /builders/esm.dist.js: -------------------------------------------------------------------------------- 1 | require('esbuild') 2 | .build({ 3 | entryPoints: ['./src/index.tsx'], 4 | outdir: './build/dist/esm', 5 | format: 'esm', 6 | bundle: true, 7 | minify: true, 8 | sourcemap: 'external', 9 | external: ['react'], 10 | keepNames: false, 11 | target: ['es2019'] 12 | }) 13 | .catch(() => process.exit(1)) 14 | -------------------------------------------------------------------------------- /builders/esm.js: -------------------------------------------------------------------------------- 1 | require('esbuild') 2 | .build({ 3 | entryPoints: ['./src/index.tsx'], 4 | outdir: './build/esm', 5 | format: 'esm', 6 | bundle: true, 7 | minify: false, 8 | sourcemap: 'external', 9 | external: ['react'], 10 | keepNames: false, 11 | target: ['es2019'] 12 | }) 13 | .catch(() => process.exit(1)) 14 | -------------------------------------------------------------------------------- /examples/next/.gitignore: -------------------------------------------------------------------------------- 1 | .next 2 | node_modules 3 | DS_Store -------------------------------------------------------------------------------- /examples/next/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | -------------------------------------------------------------------------------- /examples/next/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "next", 8 | "rb": "cd ../.. && yarn build && cd examples/next && rm -rf node_modules && rm -rf .next && yarn && yarn dev" 9 | }, 10 | "dependencies": { 11 | "@saltyaom/react-table": "../../", 12 | "next": "^11.0.1", 13 | "react": "^17.0.2", 14 | "react-dom": "^17.0.2" 15 | }, 16 | "devDependencies": { 17 | "@types/node": "^16.4.13", 18 | "@types/react": "^17.0.16", 19 | "typescript": "^4.3.5" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/next/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import type { AppProps } from 'next/app' 2 | 3 | import './init.css' 4 | 5 | const App = ({ Component, pageProps }: AppProps) => 6 | 7 | export default App 8 | -------------------------------------------------------------------------------- /examples/next/pages/custom-props.tsx: -------------------------------------------------------------------------------- 1 | import Table from '@saltyaom/react-table' 2 | 3 | const CustomProps = () => { 4 | return ( 5 | { 15 | return { 16 | onClick: () => console.log(data, i) 17 | } 18 | }} 19 | /> 20 | ) 21 | } 22 | 23 | export default CustomProps 24 | -------------------------------------------------------------------------------- /examples/next/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Table from '@saltyaom/react-table' 2 | 3 | const VTuberTable = () => { 4 | const header = ['name', 'type', 'value'] 5 | const size = [100, 96, 60] 6 | const data = [ 7 | ['Okayu', Fox, 10], 8 | ['Korone', Fox, -1], 9 | ['Fubuki', Fox, 50] 10 | ] as const 11 | 12 | return ( 13 | <> 14 |