├── .github └── workflows │ └── master.yaml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .prettierignore ├── .prettierrc.json ├── LICENSE.md ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── src ├── App.jsx ├── Badge.jsx ├── Table.jsx ├── cells │ ├── Cell.jsx │ ├── NumberCell.jsx │ ├── SelectCell.jsx │ └── TextCell.jsx ├── colors.js ├── header │ ├── AddColumnHeader.jsx │ ├── DataTypeIcon.jsx │ ├── Header.jsx │ ├── HeaderMenu.jsx │ └── TypesMenu.jsx ├── img │ ├── ArrowDown.jsx │ ├── ArrowLeft.jsx │ ├── ArrowRight.jsx │ ├── ArrowUp.jsx │ ├── Hash.jsx │ ├── Multi.jsx │ ├── Plus.jsx │ ├── Text.jsx │ └── Trash.jsx ├── index.jsx ├── scrollbarWidth.js ├── style.css └── utils.js └── vite.config.js /.github/workflows/master.yaml: -------------------------------------------------------------------------------- 1 | name: Editable React Table - Master CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [16.x] 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v2 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | - run: npm ci 24 | - run: npm run build 25 | - run: npm run lint 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | build 4 | dist 5 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run lint 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | .prettierrc.json 4 | .husky 5 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "singleQuote": true, 4 | "semi": true 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Archit Pandey 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Editable React Table 2 | 3 | 4 | > [!NOTE] 5 | > [Rowstack](https://rowstack.io) is a professional React database component I built that's brilliantly designed, feature rich and performant. Check it out! 6 | 7 | [](https://codesandbox.io/s/editable-react-table-gchwp?fontsize=14&hidenavigation=1&theme=dark) 8 | 9 |  10 | 11 | ## Features 12 | 13 | - Resizing columns 14 | - Cell data types - number, text, select 15 | - Renaming column headers 16 | - Sort on individual columns 17 | - Adding columns to left or right of a column 18 | - Adding column to end 19 | - Editing data in cells 20 | - Adding a new row of data 21 | - Deleting a column 22 | - Changing column data types 23 | - Adding options for cells of type select 24 | 25 | ## Getting Started 26 | 27 | 1. Clone this repository. 28 | 29 | ```bash 30 | git clone https://github.com/archit-p/editable-react-table 31 | ``` 32 | 33 | 2. Install dependencies and build. 34 | 35 | ```bash 36 | npm install 37 | npm start 38 | ``` 39 | 40 | ## Contributions 41 | 42 | Contributions are welcome! Feel free to pick an item from the roadmap below or open a fresh issue! 43 | 44 | ## Roadmap 45 | 46 | - [x] Support for virtualized rows 47 | - [ ] Date data-type 48 | - [ ] Multi-Select data-type 49 | - [ ] Checkbox data-type 50 | - [ ] Animating dropdowns 51 | - [ ] Performance - supporting 100000 rows 52 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react", 3 | "version": "0.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "@popperjs/core": "^2.0.0", 7 | "clsx": "1.1.1", 8 | "faker": "^5.5.3", 9 | "immutability-helper": "^3.1.1", 10 | "react": "17.0.2", 11 | "react-contenteditable": "^3.3.5", 12 | "react-dom": "17.0.2", 13 | "react-popper": "^2.2.5", 14 | "react-table": "7.7.0", 15 | "react-window": "^1.8.6" 16 | }, 17 | "scripts": { 18 | "dev": "vite", 19 | "build": "vite build", 20 | "serve": "vite preview", 21 | "prepare": "husky install", 22 | "lint": "prettier --check . && eslint src/**/*.jsx", 23 | "format": "prettier --write ." 24 | }, 25 | "devDependencies": { 26 | "@vitejs/plugin-react": "^3.0.0", 27 | "eslint": "^8.30.0", 28 | "eslint-config-react-app": "^7.0.1", 29 | "husky": "^6.0.0", 30 | "prettier": "^2.3.1", 31 | "vite": "4.x", 32 | "vite-plugin-eslint": "^1.8.1" 33 | }, 34 | "browserslist": { 35 | "production": [ 36 | ">0.2%", 37 | "not dead", 38 | "not op_mini all" 39 | ], 40 | "development": [ 41 | "last 1 chrome version", 42 | "last 1 firefox version", 43 | "last 1 safari version" 44 | ] 45 | }, 46 | "eslintConfig": { 47 | "extends": [ 48 | "react-app", 49 | "react-app/jest" 50 | ] 51 | }, 52 | "husky": { 53 | "hooks": { 54 | "pre-commit": "echo \"[Husky] pre-commit\"" 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useReducer } from 'react'; 2 | import './style.css'; 3 | import Table from './Table'; 4 | import { 5 | randomColor, 6 | shortId, 7 | makeData, 8 | ActionTypes, 9 | DataTypes, 10 | } from './utils'; 11 | import update from 'immutability-helper'; 12 | 13 | function reducer(state, action) { 14 | switch (action.type) { 15 | case ActionTypes.ADD_OPTION_TO_COLUMN: 16 | const optionIndex = state.columns.findIndex( 17 | column => column.id === action.columnId 18 | ); 19 | return update(state, { 20 | skipReset: { $set: true }, 21 | columns: { 22 | [optionIndex]: { 23 | options: { 24 | $push: [ 25 | { 26 | label: action.option, 27 | backgroundColor: action.backgroundColor, 28 | }, 29 | ], 30 | }, 31 | }, 32 | }, 33 | }); 34 | case ActionTypes.ADD_ROW: 35 | return update(state, { 36 | skipReset: { $set: true }, 37 | data: { $push: [{}] }, 38 | }); 39 | case ActionTypes.UPDATE_COLUMN_TYPE: 40 | const typeIndex = state.columns.findIndex( 41 | column => column.id === action.columnId 42 | ); 43 | switch (action.dataType) { 44 | case DataTypes.NUMBER: 45 | if (state.columns[typeIndex].dataType === DataTypes.NUMBER) { 46 | return state; 47 | } else { 48 | return update(state, { 49 | skipReset: { $set: true }, 50 | columns: { [typeIndex]: { dataType: { $set: action.dataType } } }, 51 | data: { 52 | $apply: data => 53 | data.map(row => ({ 54 | ...row, 55 | [action.columnId]: isNaN(row[action.columnId]) 56 | ? '' 57 | : Number.parseInt(row[action.columnId]), 58 | })), 59 | }, 60 | }); 61 | } 62 | case DataTypes.SELECT: 63 | if (state.columns[typeIndex].dataType === DataTypes.SELECT) { 64 | return state; 65 | } else { 66 | let options = []; 67 | state.data.forEach(row => { 68 | if (row[action.columnId]) { 69 | options.push({ 70 | label: row[action.columnId], 71 | backgroundColor: randomColor(), 72 | }); 73 | } 74 | }); 75 | return update(state, { 76 | skipReset: { $set: true }, 77 | columns: { 78 | [typeIndex]: { 79 | dataType: { $set: action.dataType }, 80 | options: { $push: options }, 81 | }, 82 | }, 83 | }); 84 | } 85 | case DataTypes.TEXT: 86 | if (state.columns[typeIndex].dataType === DataTypes.TEXT) { 87 | return state; 88 | } else if (state.columns[typeIndex].dataType === DataTypes.SELECT) { 89 | return update(state, { 90 | skipReset: { $set: true }, 91 | columns: { [typeIndex]: { dataType: { $set: action.dataType } } }, 92 | }); 93 | } else { 94 | return update(state, { 95 | skipReset: { $set: true }, 96 | columns: { [typeIndex]: { dataType: { $set: action.dataType } } }, 97 | data: { 98 | $apply: data => 99 | data.map(row => ({ 100 | ...row, 101 | [action.columnId]: row[action.columnId] + '', 102 | })), 103 | }, 104 | }); 105 | } 106 | default: 107 | return state; 108 | } 109 | case ActionTypes.UPDATE_COLUMN_HEADER: 110 | const index = state.columns.findIndex( 111 | column => column.id === action.columnId 112 | ); 113 | return update(state, { 114 | skipReset: { $set: true }, 115 | columns: { [index]: { label: { $set: action.label } } }, 116 | }); 117 | case ActionTypes.UPDATE_CELL: 118 | return update(state, { 119 | skipReset: { $set: true }, 120 | data: { 121 | [action.rowIndex]: { [action.columnId]: { $set: action.value } }, 122 | }, 123 | }); 124 | case ActionTypes.ADD_COLUMN_TO_LEFT: 125 | const leftIndex = state.columns.findIndex( 126 | column => column.id === action.columnId 127 | ); 128 | let leftId = shortId(); 129 | return update(state, { 130 | skipReset: { $set: true }, 131 | columns: { 132 | $splice: [ 133 | [ 134 | leftIndex, 135 | 0, 136 | { 137 | id: leftId, 138 | label: 'Column', 139 | accessor: leftId, 140 | dataType: DataTypes.TEXT, 141 | created: action.focus && true, 142 | options: [], 143 | }, 144 | ], 145 | ], 146 | }, 147 | }); 148 | case ActionTypes.ADD_COLUMN_TO_RIGHT: 149 | const rightIndex = state.columns.findIndex( 150 | column => column.id === action.columnId 151 | ); 152 | const rightId = shortId(); 153 | return update(state, { 154 | skipReset: { $set: true }, 155 | columns: { 156 | $splice: [ 157 | [ 158 | rightIndex + 1, 159 | 0, 160 | { 161 | id: rightId, 162 | label: 'Column', 163 | accessor: rightId, 164 | dataType: DataTypes.TEXT, 165 | created: action.focus && true, 166 | options: [], 167 | }, 168 | ], 169 | ], 170 | }, 171 | }); 172 | case ActionTypes.DELETE_COLUMN: 173 | const deleteIndex = state.columns.findIndex( 174 | column => column.id === action.columnId 175 | ); 176 | return update(state, { 177 | skipReset: { $set: true }, 178 | columns: { $splice: [[deleteIndex, 1]] }, 179 | }); 180 | case ActionTypes.ENABLE_RESET: 181 | return update(state, { skipReset: { $set: true } }); 182 | default: 183 | return state; 184 | } 185 | } 186 | 187 | function App() { 188 | const [state, dispatch] = useReducer(reducer, makeData(1000)); 189 | 190 | useEffect(() => { 191 | dispatch({ type: ActionTypes.ENABLE_RESET }); 192 | }, [state.data, state.columns]); 193 | 194 | return ( 195 |