├── .babelrc ├── .codesandbox └── ci.json ├── .config ├── base.js ├── commonjs.js ├── es.js ├── factory.js ├── helpers │ └── licenseBanner.js ├── minified.js └── umd.js ├── .editorconfig ├── .github ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .npmignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── baseEditorComponent.tsx ├── helpers.tsx ├── hotColumn.tsx ├── hotTable.tsx ├── index.tsx ├── json.d.ts ├── portalManager.tsx ├── settingsMapper.ts └── types.tsx ├── test-tsconfig.json ├── test ├── _helpers.tsx ├── autoSizeWarning.spec.tsx ├── componentInternals.spec.tsx ├── hotColumn.spec.tsx ├── hotTable.spec.tsx ├── jestsetup.ts ├── reactContext.spec.tsx ├── reactHooks.spec.tsx ├── reactLazy.spec.tsx ├── reactMemo.spec.tsx ├── reactPureComponent.spec.tsx ├── redux.spec.tsx └── settingsMapper.spec.tsx └── tsconfig.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-react" 4 | ], 5 | "env": { 6 | "test": { 7 | "presets": [ 8 | "@babel/env", 9 | "@babel/preset-typescript" 10 | ], 11 | "plugins": [ 12 | "@babel/transform-runtime", 13 | "@babel/plugin-proposal-class-properties" 14 | ] 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.codesandbox/ci.json: -------------------------------------------------------------------------------- 1 | { 2 | "sandboxes": [ 3 | "github/handsontable/examples/tree/master/react/pull-request" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.config/base.js: -------------------------------------------------------------------------------- 1 | import nodeResolve from 'rollup-plugin-node-resolve'; 2 | import babel from 'rollup-plugin-babel'; 3 | import typescript from 'rollup-plugin-typescript2'; 4 | import json from 'rollup-plugin-json'; 5 | import commonjs from 'rollup-plugin-commonjs'; 6 | 7 | export const plugins = { 8 | typescript: typescript(), 9 | babel: babel({ 10 | babelrc: false, 11 | exclude: ['/node_modules/', '**.json'], 12 | extensions: ['.js', '.ts', '.tsx', '.jsx'], 13 | presets: [ 14 | '@babel/env' 15 | ], 16 | }), 17 | nodeResolve: nodeResolve(), 18 | json: json({ 19 | include: 'package.json', 20 | compact: true 21 | }), 22 | commonjs: commonjs({ 23 | include: [ 24 | 'node_modules/**', 25 | 'src/lib/**' 26 | ] 27 | }) 28 | }; 29 | 30 | export const baseConfig = { 31 | input: 'src/index.tsx', 32 | plugins: [ 33 | plugins.json, 34 | plugins.replace, 35 | plugins.commonjs, 36 | plugins.typescript, 37 | plugins.babel, 38 | plugins.nodeResolve, 39 | ], 40 | external: [ 41 | 'react', 42 | 'react-dom', 43 | 'handsontable' 44 | ], 45 | }; 46 | -------------------------------------------------------------------------------- /.config/commonjs.js: -------------------------------------------------------------------------------- 1 | import typescript from 'rollup-plugin-typescript2'; 2 | import { baseConfig, plugins } from './base'; 3 | 4 | const env = process.env.NODE_ENV; 5 | const filename = 'react-handsontable.js'; 6 | 7 | export const cjsConfig = { 8 | output: { 9 | format: env, 10 | indent: false, 11 | file: `./commonjs/${filename}`, 12 | exports: 'named' 13 | }, 14 | plugins: baseConfig.plugins, 15 | }; 16 | -------------------------------------------------------------------------------- /.config/es.js: -------------------------------------------------------------------------------- 1 | import typescript from 'rollup-plugin-typescript2'; 2 | import { baseConfig } from './base'; 3 | import { plugins } from './base'; 4 | 5 | const env = process.env.NODE_ENV; 6 | const filename = 'react-handsontable.js'; 7 | 8 | export const esConfig = { 9 | output: { 10 | format: env, 11 | indent: false, 12 | file: `./es/${filename}` 13 | }, 14 | plugins: [ 15 | plugins.json, 16 | plugins.replace, 17 | plugins.commonjs, 18 | typescript({ 19 | tsconfigOverride: { 20 | compilerOptions: { 21 | declaration: true 22 | } 23 | }, 24 | useTsconfigDeclarationDir: true, 25 | }), 26 | plugins.babel, 27 | plugins.nodeResolve, 28 | ] 29 | }; 30 | -------------------------------------------------------------------------------- /.config/factory.js: -------------------------------------------------------------------------------- 1 | import { baseConfig } from './base'; 2 | import { cjsConfig } from './commonjs'; 3 | import { esConfig } from './es'; 4 | import { umdConfig } from './umd'; 5 | import { minConfig } from './minified'; 6 | 7 | export function createConfig() { 8 | const env = process.env.NODE_ENV; 9 | const config = baseConfig; 10 | const newConfigs = { 11 | cjs: cjsConfig, 12 | es: esConfig, 13 | umd: umdConfig, 14 | min: minConfig, 15 | }; 16 | const newConfig = newConfigs[env]; 17 | 18 | for (let key in newConfig) { 19 | if (newConfig.hasOwnProperty(key)) { 20 | if (Array.isArray(config[key]) && Array.isArray(newConfig[key])) { 21 | config[key] = newConfig[key]; 22 | 23 | } else if (typeof config[key] === 'object' && typeof newConfig[key] === 'object') { 24 | Object.assign(config[key], newConfig[key]); 25 | 26 | } else { 27 | config[key] = newConfig[key]; 28 | } 29 | } 30 | } 31 | 32 | return config; 33 | } 34 | -------------------------------------------------------------------------------- /.config/helpers/licenseBanner.js: -------------------------------------------------------------------------------- 1 | export function addLicenseBanner(config) { 2 | const path = require('path'); 3 | const fs = require('fs'); 4 | const packageBody = require(`./package.json`); 5 | 6 | let licenseBody = fs.readFileSync(path.resolve(__dirname, './LICENSE'), 'utf8'); 7 | licenseBody += `\nVersion: ${packageBody.version} (built at ${new Date().toString()})`; 8 | 9 | config.output.banner = `/*!\n${licenseBody.replace(/^/gm, ' * ')}\n */`; 10 | 11 | return config; 12 | } 13 | -------------------------------------------------------------------------------- /.config/minified.js: -------------------------------------------------------------------------------- 1 | import { baseConfig } from './base'; 2 | import { addLicenseBanner } from './helpers/licenseBanner'; 3 | import replace from 'rollup-plugin-replace'; 4 | import { uglify } from 'rollup-plugin-uglify'; 5 | 6 | const minFilename = 'react-handsontable.min.js'; 7 | 8 | const minConfig = { 9 | output: { 10 | format: 'umd', 11 | name: 'Handsontable.react', 12 | indent: false, 13 | sourcemap: true, 14 | exports: 'named', 15 | file: `./dist/${minFilename}`, 16 | globals: { 17 | react: 'React', 18 | 'react-dom': 'ReactDOM', 19 | handsontable: 'Handsontable' 20 | } 21 | }, 22 | plugins: baseConfig.plugins.concat([ 23 | replace({ 24 | 'process.env.NODE_ENV': JSON.stringify('production') 25 | }), 26 | uglify({ 27 | output: { 28 | comments: /^!/ 29 | }, 30 | compress: { 31 | pure_getters: true, 32 | unsafe: true, 33 | unsafe_comps: true, 34 | } 35 | }) 36 | ]) 37 | }; 38 | 39 | addLicenseBanner(minConfig); 40 | 41 | export { minConfig }; 42 | -------------------------------------------------------------------------------- /.config/umd.js: -------------------------------------------------------------------------------- 1 | import { addLicenseBanner } from './helpers/licenseBanner'; 2 | import { baseConfig } from './base'; 3 | import replace from 'rollup-plugin-replace'; 4 | 5 | const env = process.env.NODE_ENV; 6 | const filename = 'react-handsontable.js'; 7 | 8 | const umdConfig = { 9 | output: { 10 | format: env, 11 | name: 'Handsontable.react', 12 | indent: false, 13 | sourcemap: true, 14 | exports: 'named', 15 | file: `./dist/${filename}`, 16 | globals: { 17 | react: 'React', 18 | 'react-dom': 'ReactDOM', 19 | handsontable: 'Handsontable' 20 | } 21 | }, 22 | plugins: baseConfig.plugins.concat([ 23 | replace({ 24 | 'process.env.NODE_ENV': JSON.stringify('production') 25 | }) 26 | ]) 27 | }; 28 | 29 | addLicenseBanner(umdConfig); 30 | 31 | export { umdConfig }; 32 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_style = space 3 | indent_size = 2 4 | charset = utf-8 5 | trim_trailing_whitespace = true 6 | insert_final_newline = true 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 3 | 4 | ### Steps to reproduce 5 | 6 | 1. 7 | 2. 8 | 3. 9 | 10 | ### Demo 11 | 12 | https://jsfiddle.net/handsoncode/c6rgz08x/ 13 | 14 | ### Your environment 15 | * React wrapper version: 16 | * Handsontable version: 17 | * Browser Name and version: 18 | * Operating System: 19 | 20 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Context 2 | 3 | 4 | ### How has this been tested? 5 | 6 | 7 | ### Types of changes 8 | 9 | - [ ] Bug fix (non-breaking change which fixes an issue) 10 | - [ ] New feature or improvement (non-breaking change which adds functionality) 11 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 12 | 13 | ### Related issue(s): 14 | 1. 15 | 2. 16 | 3. 17 | 18 | ### Checklist: 19 | 20 | 21 | - [ ] My code follows the code style of this project, 22 | - [ ] My change requires a change to the documentation. 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bower_components 3 | dist 4 | commonjs 5 | es 6 | 7 | .DS_Store 8 | .idea 9 | .rpt2_cache 10 | *.log 11 | /*.d.ts 12 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | 3 | !commonjs/* 4 | !dist/* 5 | !es/* 6 | !package.json 7 | !*.d.ts 8 | !README.md 9 | !LICENSE 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | sudo: false 4 | 5 | node_js: 6 | - '8' 7 | 8 | before_script: 9 | - export TZ=Europe/Warsaw 10 | 11 | notifications: 12 | email: false 13 | slack: 14 | secure: cDGwvetvROdzjr+rM8KYU48ZQNNwoyeSDI5KnH0y8jrGj/KhbHOsoToNjensduI3afoyouRKc1rbxe7qfmikT0SSiJluyWM4VwXRkdye0I0POnLYISS/fTPFGDulFyiuxOhHzmiQav1+zNkYonGgOVBVt/NqTZkSdc/n6HNdKXIC+3KLdUQ3AI+eaoPaph4jqUdaojSQr26K49Y8us/F7ITI7pPyxG/aVLzLwobGeUaYho+R3PahYBnPlhzNIpeAQ98ZM0Dv4+3ZLzkOZi+/qKWTsaOYFpAlm41QohrcN47XtBCDVLxZ9UzEghkIfHJS68po1MuBDK+ULgm7+LkwEMd6/iTJsKa7ziVmY5ijXdFIc4i+kfMjwsQO7Fm5XAVpm66O+qXlRG5cveCEuLl2NEGBsZ/6caKD7AOqWzF2X5UL+O3qX0deWLT4AOnS/nGnlysPr94/Pthy2QMTt8/lqxpMtBEH9zy0mM7l1ZbeL/eDgUGzNXHxS0rpaE873Ea9Kaq4PZ20AISTGlni11gZbhpAOWhjl9X4sbXzk8ANTvnTf/CeYX8JXCuTEXPPLr4WkHS+1oSq4rfA7DNKrqMgs1LceIy07GogtgAGhJx2b1jFsvk8rO7G0/wOC+E1SIM/8NzQkRxpRmS8y2jkJ4BaQM/6Ee4lpNocC1fsG2VhCw4= 15 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Handsontable 2 | 3 | Your contributions to this project are very welcome. If you want to fix a bug or propose a new feature, you can open a new Pull Request but first make sure it follows these general rules: 4 | 5 | 1. Sign this [Contributor License Agreement](https://goo.gl/forms/yuutGuN0RjsikVpM2) to allow us to publish your changes to the code. 6 | 2. Make your changes on a separate branch. This will speed up the merging process. 7 | 3. Always make the target of your pull request the `develop` branch, not `master`. 8 | 4. Please review our [coding style](https://github.com/airbnb/javascript) for instructions on how to properly style the code. 9 | 5. Add a thorough description of all the changes. 10 | 11 | Thank you for your commitment! 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) Handsoncode sp. z o.o. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Important information 2 | 3 | ## We permanently moved this project to the main Handsontable repository at [https://github.com/handsontable/handsontable/tree/master/wrappers/react](https://github.com/handsontable/handsontable/tree/master/wrappers/react) 4 | 5 | ## It is still available under the same name in npm: [`@handsontable/react`](https://www.npmjs.com/package/@handsontable/react), so you don't have to update your dependency configuration. 6 | 7 | --------- 8 |


9 | 10 |
11 | 12 | ![Handsontable for React](https://raw.githubusercontent.com/handsontable/static-files/master/Images/Logo/Handsontable/handsontable-react.png) 13 | 14 | This is the official wrapper of [**Handsontable**](//github.com/handsontable/handsontable) data grid for React.
15 | It provides data binding, data validation, filtering, sorting and more.
16 | 17 | [![npm](https://img.shields.io/npm/dt/@handsontable/react.svg)](//npmjs.com/package/@handsontable/react) 18 | [![npm](https://img.shields.io/npm/dm/@handsontable/react.svg)](//npmjs.com/package/@handsontable/react) 19 | [![Build status](https://travis-ci.org/handsontable/react-handsontable.png?branch=master)](//travis-ci.org/handsontable/react-handsontable) 20 |
21 | 22 |
23 | 24 |
25 | 26 | A screenshot of a data grid for React 27 | 28 |
29 | 30 |
31 | 32 | ## Installation 33 | 34 | Use npm to install this wrapper together with Handsontable. 35 | ``` 36 | npm install handsontable @handsontable/react 37 | ``` 38 | 39 | You can load it directly from [jsDelivr](//jsdelivr.com/package/npm/@handsontable/react) as well. 40 | ```html 41 | 42 | 43 | 44 | 45 | ``` 46 | 47 | The component will be available as `Handsontable.react.HotTable`. 48 | 49 | ## Usage 50 | 51 | Use this data grid as you would any other component in your application. [Options](//handsontable.com/docs/Options.html) can be set as `HotTable` props. 52 | 53 | **Styles** 54 | ```css 55 | @import '~handsontable/dist/handsontable.full.css'; 56 | ``` 57 | 58 | **React Component** 59 | ```js 60 | import React from 'react'; 61 | import ReactDOM from 'react-dom'; 62 | import { HotTable } from '@handsontable/react'; 63 | 64 | class HotApp extends React.Component { 65 | constructor(props) { 66 | super(props); 67 | this.data = [ 68 | ['', 'Tesla', 'Mercedes', 'Toyota', 'Volvo'], 69 | ['2019', 10, 11, 12, 13], 70 | ['2020', 20, 11, 14, 13], 71 | ['2021', 30, 15, 12, 13] 72 | ]; 73 | } 74 | 75 | render() { 76 | return (); 77 | } 78 | } 79 | ``` 80 | 81 | ##### [See the live demo](//handsontable.com/docs/frameworks-wrapper-for-react-simple-examples.html) 82 | 83 | ## Features 84 | 85 | A list of some of the most popular features: 86 | 87 | - Multiple column sorting 88 | - Non-contiguous selection 89 | - Filtering data 90 | - Export to file 91 | - Validating data 92 | - Conditional formatting 93 | - Merging cells 94 | - Custom cell types 95 | - Freezing rows/columns 96 | - Moving rows/columns 97 | - Resizing rows/columns 98 | - Hiding rows/columns 99 | - Context menu 100 | - Comments 101 | - Auto-fill option 102 | 103 | ## Documentation 104 | 105 | - [Developer guides](//handsontable.com/docs/react) 106 | - [API Reference](//handsontable.com/docs/Core.html) 107 | - [Release notes](//handsontable.com/docs/tutorial-release-notes.html) 108 | - [Twitter](//twitter.com/handsontable) (News and updates) 109 | 110 | ## Support and contribution 111 | 112 | We provide support for all users through [GitHub issues](//github.com/handsontable/react-handsontable/issues). If you have a commercial license then you can add a new ticket through the [contact form](//handsontable.com/contact?category=technical_support). 113 | 114 | If you would like to contribute to this project, make sure you first read the [guide for contributors](//github.com/handsontable/react-handsontable/blob/master/CONTRIBUTING.md). 115 | 116 | ## Browser compatibility 117 | 118 | Handsontable is compatible with modern browsers such as Chrome, Firefox, Safari, Opera, and Edge. It also supports Internet Explorer 9 to 11 but with limited performance. 119 | 120 | ## License 121 | 122 | This wrapper is released under [the MIT license](//github.com/handsontable/react-handsontable/blob/master/LICENSE) but under the hood it uses [Handsontable](//github.com/handsontable/handsontable), which is dual-licensed. You can either use it for free in all your non-commercial projects or purchase a commercial license. 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 |
Free licensePaid license
For non-commercial purposes such as teaching, academic research, personal experimentation, and evaluating on development and testing servers.For all commercial purposes
All features are availableAll features are available
Community supportDedicated support
Read the licenseSee plans
150 | 151 | ## License key 152 | 153 | **The license key is obligatory since [Handsontable 7.0.0](//github.com/handsontable/handsontable/releases/tag/7.0.0) (released in March 2019).** 154 | 155 | If you use Handsontable for purposes not intended toward monetary compensation such as, but not limited to, teaching, academic research, evaluation, testing and experimentation, pass a phrase `'non-commercial-and-evaluation'`, as presented below. 156 | 157 | You can pass it in the `settings` object: 158 | 159 | ```js 160 | settings: { 161 | data: data, 162 | rowHeaders: true, 163 | colHeaders: true, 164 | licenseKey: 'non-commercial-and-evaluation' 165 | } 166 | ``` 167 | 168 | Alternatively, you can pass it to a `licenseKey` prop: 169 | 170 | ```jsx 171 | 172 | ``` 173 | 174 | If, on the other hand, you use Handsontable in a project that supports your commercial activity, then you must purchase the license key at [handsontable.com](//handsontable.com/pricing). 175 | 176 | The license key is validated in an offline mode. No connection is made to any server. [Learn more](//handsontable.com/docs/tutorial-license-key.html) about how it works. 177 | 178 |
179 |
180 | 181 | Created by [Handsoncode](//handsoncode.net) with ❤ and ☕ in [Tricity](//en.wikipedia.org/wiki/Tricity,_Poland). 182 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@handsontable/react", 3 | "version": "4.0.0", 4 | "description": "Best Data Grid for React with Spreadsheet Look and Feel.", 5 | "author": "Handsoncode (https://handsoncode.net)", 6 | "homepage": "https://handsontable.com", 7 | "license": "MIT", 8 | "main": "./commonjs/react-handsontable.js", 9 | "module": "./es/react-handsontable.js", 10 | "jsdelivr": "./dist/react-handsontable.min.js", 11 | "unpkg": "./dist/react-handsontable.min.js", 12 | "types": "./index.d.ts", 13 | "keywords": [ 14 | "handsontable", 15 | "component", 16 | "grid", 17 | "data", 18 | "table", 19 | "data table", 20 | "data grid", 21 | "spreadsheet", 22 | "sheet", 23 | "excel", 24 | "enterprise", 25 | "sort", 26 | "formulas", 27 | "filter", 28 | "search", 29 | "conditional", 30 | "formatting", 31 | "csv", 32 | "react", 33 | "reactjs", 34 | "react component", 35 | "react grid", 36 | "wrapper" 37 | ], 38 | "repository": { 39 | "type": "git", 40 | "url": "https://github.com/handsontable/react-handsontable.git" 41 | }, 42 | "bugs": { 43 | "url": "https://github.com/handsontable/react-handsontable/issues" 44 | }, 45 | "devDependencies": { 46 | "@babel/cli": "^7.8.4", 47 | "@babel/core": "^7.9.0", 48 | "@babel/plugin-proposal-class-properties": "^7.8.3", 49 | "@babel/plugin-transform-runtime": "^7.9.0", 50 | "@babel/polyfill": "^7.8.7", 51 | "@babel/preset-env": "^7.9.0", 52 | "@babel/preset-react": "^7.9.4", 53 | "@babel/preset-typescript": "^7.9.0", 54 | "@babel/runtime": "^7.9.2", 55 | "@types/enzyme": "^3.1.15", 56 | "@types/enzyme-adapter-react-16": "^1.0.3", 57 | "@types/jest": "^24.0.9", 58 | "@types/react": "^16.9.5", 59 | "@types/react-dom": "^16.9.1", 60 | "@types/react-redux": "^7.1.7", 61 | "babel-core": "^7.0.0-bridge.0", 62 | "cpy-cli": "^3.1.1", 63 | "cross-env": "^5.2.0", 64 | "del-cli": "^3.0.1", 65 | "enzyme": "^3.10.0", 66 | "enzyme-adapter-react-16": "^1.14.0", 67 | "enzyme-to-json": "^3.4.0", 68 | "handsontable": "^8.0.0", 69 | "jest": "^25.1.0", 70 | "prop-types": "^15.7.2", 71 | "react": "^16.10.2", 72 | "react-dom": "^16.10.2", 73 | "react-redux": "^7.1.1", 74 | "redux": "^4.0.4", 75 | "rollup": "^1.17.0", 76 | "rollup-plugin-alias": "^1.5.2", 77 | "rollup-plugin-babel": "^4.3.3", 78 | "rollup-plugin-commonjs": "^10.0.1", 79 | "rollup-plugin-json": "^4.0.0", 80 | "rollup-plugin-node-resolve": "^5.2.0", 81 | "rollup-plugin-replace": "^2.2.0", 82 | "rollup-plugin-typescript2": "^0.22.1", 83 | "rollup-plugin-uglify": "^6.0.4", 84 | "typescript": "^3.5.3", 85 | "uglify-js": "^3.4.9" 86 | }, 87 | "peerDependencies": { 88 | "handsontable": ">=8.0.0" 89 | }, 90 | "scripts": { 91 | "build": "npm run delete-build && npm run build:commonjs && npm run build:es && npm run build:umd && npm run build:min", 92 | "build:commonjs": "cross-env NODE_ENV=cjs rollup -c", 93 | "build:umd": "cross-env NODE_ENV=umd rollup -c", 94 | "build:es": "cross-env NODE_ENV=es rollup -c", 95 | "build:min": "cross-env NODE_ENV=min rollup -c", 96 | "delete-build": "del-cli ./es/ && del-cli ./commonjs/ && del-cli ./dist/ && del-cli ./*.d.ts", 97 | "publish-build": "npm publish", 98 | "publish-all": "npm run build && npm run publish-build && npm run delete-build", 99 | "test": "jest", 100 | "watch:commonjs": "cross-env NODE_ENV=cjs rollup -c --watch", 101 | "watch:es": "cross-env NODE_ENV=es rollup -c --watch" 102 | }, 103 | "jest": { 104 | "testURL": "http://localhost/", 105 | "setupFiles": [ 106 | "./test/jestsetup.ts" 107 | ], 108 | "snapshotSerializers": [ 109 | "enzyme-to-json/serializer" 110 | ], 111 | "transform": { 112 | "^.+\\.tsx?$": "babel-jest", 113 | "^.+\\.js$": "babel-jest" 114 | }, 115 | "testRegex": "(/test/(.*).(test|spec)).(jsx?|tsx?)$", 116 | "moduleFileExtensions": [ 117 | "ts", 118 | "tsx", 119 | "js", 120 | "jsx", 121 | "json", 122 | "node" 123 | ], 124 | "globals": { 125 | "ts-jest": { 126 | "tsConfig": "test-tsconfig.json", 127 | "babelConfig": true 128 | } 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import { createConfig } from './.config/factory'; 2 | 3 | export default createConfig(); 4 | -------------------------------------------------------------------------------- /src/baseEditorComponent.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Handsontable from 'handsontable'; 3 | import { HotEditorProps } from './types'; 4 | 5 | class BaseEditorComponent

extends React.Component

implements Handsontable._editors.Base { 6 | name = 'BaseEditorComponent'; 7 | instance = null; 8 | row = null; 9 | col = null; 10 | prop = null; 11 | TD = null; 12 | originalValue = null; 13 | cellProperties = null; 14 | state = null; 15 | hotInstance = null; 16 | hotCustomEditorInstance = null; 17 | hot = null; 18 | 19 | constructor(props) { 20 | super(props); 21 | 22 | if (props.emitEditorInstance) { 23 | props.emitEditorInstance(this); 24 | } 25 | } 26 | 27 | // BaseEditor methods: 28 | private _fireCallbacks(...args) { 29 | (Handsontable.editors.BaseEditor.prototype as any)._fireCallbacks.call(this.hotCustomEditorInstance, ...args); 30 | } 31 | 32 | beginEditing(...args) { 33 | return Handsontable.editors.BaseEditor.prototype.beginEditing.call(this.hotCustomEditorInstance, ...args); 34 | } 35 | 36 | cancelChanges(...args) { 37 | return Handsontable.editors.BaseEditor.prototype.cancelChanges.call(this.hotCustomEditorInstance, ...args); 38 | } 39 | 40 | checkEditorSection(...args) { 41 | return Handsontable.editors.BaseEditor.prototype.checkEditorSection.call(this.hotCustomEditorInstance, ...args); 42 | } 43 | 44 | close(...args) { 45 | return Handsontable.editors.BaseEditor.prototype.close.call(this.hotCustomEditorInstance, ...args); 46 | } 47 | 48 | discardEditor(...args) { 49 | return Handsontable.editors.BaseEditor.prototype.discardEditor.call(this.hotCustomEditorInstance, ...args); 50 | } 51 | 52 | enableFullEditMode(...args) { 53 | return Handsontable.editors.BaseEditor.prototype.enableFullEditMode.call(this.hotCustomEditorInstance, ...args); 54 | } 55 | 56 | extend(...args) { 57 | return Handsontable.editors.BaseEditor.prototype.extend.call(this.hotCustomEditorInstance, ...args); 58 | } 59 | 60 | finishEditing(...args) { 61 | return Handsontable.editors.BaseEditor.prototype.finishEditing.call(this.hotCustomEditorInstance, ...args); 62 | } 63 | 64 | focus(...args) { 65 | return Handsontable.editors.BaseEditor.prototype.focus.call(this.hotCustomEditorInstance, ...args); 66 | } 67 | 68 | getValue(...args) { 69 | return Handsontable.editors.BaseEditor.prototype.getValue.call(this.hotCustomEditorInstance, ...args); 70 | } 71 | 72 | init(...args) { 73 | return Handsontable.editors.BaseEditor.prototype.init.call(this.hotCustomEditorInstance, ...args); 74 | } 75 | 76 | isInFullEditMode(...args) { 77 | return Handsontable.editors.BaseEditor.prototype.isInFullEditMode.call(this.hotCustomEditorInstance, ...args); 78 | } 79 | 80 | isOpened(...args) { 81 | return Handsontable.editors.BaseEditor.prototype.isOpened.call(this.hotCustomEditorInstance, ...args); 82 | } 83 | 84 | isWaiting(...args) { 85 | return Handsontable.editors.BaseEditor.prototype.isWaiting.call(this.hotCustomEditorInstance, ...args); 86 | } 87 | 88 | open(...args) { 89 | return Handsontable.editors.BaseEditor.prototype.open.call(this.hotCustomEditorInstance, ...args); 90 | } 91 | 92 | prepare(row, col, prop, TD, originalValue, cellProperties) { 93 | this.hotInstance = cellProperties.instance; 94 | this.row = row; 95 | this.col = col; 96 | this.prop = prop; 97 | this.TD = TD; 98 | this.originalValue = originalValue; 99 | this.cellProperties = cellProperties; 100 | 101 | return Handsontable.editors.BaseEditor.prototype.prepare.call(this.hotCustomEditorInstance, row, col, prop, TD, originalValue, cellProperties); 102 | } 103 | 104 | saveValue(...args) { 105 | return Handsontable.editors.BaseEditor.prototype.saveValue.call(this.hotCustomEditorInstance, ...args); 106 | } 107 | 108 | setValue(...args) { 109 | return Handsontable.editors.BaseEditor.prototype.setValue.call(this.hotCustomEditorInstance, ...args); 110 | } 111 | 112 | addHook(...args) { 113 | return (Handsontable.editors.BaseEditor.prototype as any).addHook.call(this.hotCustomEditorInstance, ...args); 114 | } 115 | 116 | removeHooksByKey(...args) { 117 | return (Handsontable.editors.BaseEditor.prototype as any).removeHooksByKey.call(this.hotCustomEditorInstance, ...args); 118 | } 119 | 120 | clearHooks(...args) { 121 | return (Handsontable.editors.BaseEditor.prototype as any).clearHooks.call(this.hotCustomEditorInstance, ...args); 122 | } 123 | 124 | getEditedCell(...args) { 125 | return (Handsontable.editors.BaseEditor.prototype as any).getEditedCell.call(this.hotCustomEditorInstance, ...args); 126 | } 127 | 128 | getEditedCellsZIndex(...args) { 129 | return (Handsontable.editors.BaseEditor.prototype as any).getEditedCellsZIndex.call(this.hotCustomEditorInstance, ...args); 130 | } 131 | } 132 | 133 | export default BaseEditorComponent; 134 | export { BaseEditorComponent }; 135 | -------------------------------------------------------------------------------- /src/helpers.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { HotEditorElement } from './types'; 4 | 5 | let bulkComponentContainer = null; 6 | 7 | /** 8 | * Warning message for the `autoRowSize`/`autoColumnSize` compatibility check. 9 | */ 10 | export const AUTOSIZE_WARNING = 'Your `HotTable` configuration includes `autoRowSize`/`autoColumnSize` options, which are not compatible with ' + 11 | ' the component-based renderers`. Disable `autoRowSize` and `autoColumnSize` to prevent row and column misalignment.'; 12 | 13 | /** 14 | * Default classname given to the wrapper container. 15 | */ 16 | const DEFAULT_CLASSNAME = 'hot-wrapper-editor-container'; 17 | 18 | /** 19 | * Logs warn to the console if the `console` object is exposed. 20 | * 21 | * @param {...*} args Values which will be logged. 22 | */ 23 | export function warn(...args) { 24 | if (typeof console !== 'undefined') { 25 | console.warn(...args); 26 | } 27 | } 28 | 29 | /** 30 | * Filter out and return elements of the provided `type` from the `HotColumn` component's children. 31 | * 32 | * @param {React.ReactNode} children HotTable children array. 33 | * @param {String} type Either `'hot-renderer'` or `'hot-editor'`. 34 | * @returns {Object|null} A child (React node) or `null`, if no child of that type was found. 35 | */ 36 | export function getChildElementByType(children: React.ReactNode, type: string): React.ReactElement | null { 37 | const childrenArray: React.ReactNode[] = React.Children.toArray(children); 38 | const childrenCount: number = React.Children.count(children); 39 | let wantedChild: React.ReactNode | null = null; 40 | 41 | if (childrenCount !== 0) { 42 | if (childrenCount === 1 && (childrenArray[0] as React.ReactElement).props[type]) { 43 | wantedChild = childrenArray[0]; 44 | 45 | } else { 46 | wantedChild = childrenArray.find((child) => { 47 | return (child as React.ReactElement).props[type] !== void 0; 48 | }); 49 | } 50 | } 51 | 52 | return (wantedChild as React.ReactElement) || null; 53 | } 54 | 55 | /** 56 | * Get the reference to the original editor class. 57 | * 58 | * @param {React.ReactElement} editorElement React element of the editor class. 59 | * @returns {Function} Original class of the editor component. 60 | */ 61 | export function getOriginalEditorClass(editorElement: HotEditorElement) { 62 | if (!editorElement) { 63 | return null; 64 | } 65 | 66 | return editorElement.type.WrappedComponent ? editorElement.type.WrappedComponent : editorElement.type; 67 | } 68 | 69 | /** 70 | * Remove editor containers from DOM. 71 | * 72 | * @param {Document} [doc] Document to be used. 73 | * @param {Map} editorCache The editor cache reference. 74 | */ 75 | export function removeEditorContainers(doc = document): void { 76 | doc.querySelectorAll(`[class^="${DEFAULT_CLASSNAME}"]`).forEach((domNode) => { 77 | if (domNode.parentNode) { 78 | domNode.parentNode.removeChild(domNode); 79 | } 80 | }); 81 | } 82 | 83 | /** 84 | * Create an editor portal. 85 | * 86 | * @param {Document} [doc] Document to be used. 87 | * @param {React.ReactElement} editorElement Editor's element. 88 | * @param {Map} editorCache The editor cache reference. 89 | * @returns {React.ReactPortal} The portal for the editor. 90 | */ 91 | export function createEditorPortal(doc = document, editorElement: HotEditorElement, editorCache: Map): React.ReactPortal { 92 | if (editorElement === null) { 93 | return; 94 | } 95 | 96 | const editorContainer = doc.createElement('DIV'); 97 | const {id, className, style} = getContainerAttributesProps(editorElement.props, false); 98 | 99 | if (id) { 100 | editorContainer.id = id; 101 | } 102 | 103 | editorContainer.className = [DEFAULT_CLASSNAME, className].join(' '); 104 | 105 | if (style) { 106 | Object.assign(editorContainer.style, style); 107 | } 108 | 109 | doc.body.appendChild(editorContainer); 110 | 111 | return ReactDOM.createPortal(editorElement, editorContainer); 112 | } 113 | 114 | /** 115 | * Get an editor element extended with a instance-emitting method. 116 | * 117 | * @param {React.ReactNode} children Component children. 118 | * @param {Map} editorCache Component's editor cache. 119 | * @returns {React.ReactElement} An editor element containing the additional methods. 120 | */ 121 | export function getExtendedEditorElement(children: React.ReactNode, editorCache: Map): React.ReactElement | null { 122 | const editorElement = getChildElementByType(children, 'hot-editor'); 123 | const editorClass = getOriginalEditorClass(editorElement); 124 | 125 | if (!editorElement) { 126 | return null; 127 | } 128 | 129 | return React.cloneElement(editorElement, { 130 | emitEditorInstance: (editorInstance) => { 131 | editorCache.set(editorClass, editorInstance); 132 | }, 133 | isEditor: true 134 | } as object); 135 | } 136 | 137 | /** 138 | * Create a react component and render it to an external DOM done. 139 | * 140 | * @param {React.ReactElement} rElement React element to be used as a base for the component. 141 | * @param {Object} props Props to be passed to the cloned element. 142 | * @param {Function} callback Callback to be called after the component has been mounted. 143 | * @param {Document} [ownerDocument] The owner document to set the portal up into. 144 | * @returns {{portal: React.ReactPortal, portalContainer: HTMLElement}} An object containing the portal and its container. 145 | */ 146 | export function createPortal(rElement: React.ReactElement, props, callback: Function, ownerDocument: Document = document): { 147 | portal: React.ReactPortal, 148 | portalContainer: HTMLElement 149 | } { 150 | if (!ownerDocument) { 151 | ownerDocument = document; 152 | } 153 | 154 | if (!bulkComponentContainer) { 155 | bulkComponentContainer = ownerDocument.createDocumentFragment(); 156 | } 157 | 158 | const portalContainer = ownerDocument.createElement('DIV'); 159 | bulkComponentContainer.appendChild(portalContainer); 160 | 161 | const extendedRendererElement = React.cloneElement(rElement, { 162 | key: `${props.row}-${props.col}`, 163 | ...props 164 | }); 165 | 166 | return { 167 | portal: ReactDOM.createPortal(extendedRendererElement, portalContainer, `${props.row}-${props.col}-${Math.random()}`), 168 | portalContainer 169 | }; 170 | } 171 | 172 | /** 173 | * Get an object containing the `id`, `className` and `style` keys, representing the corresponding props passed to the 174 | * component. 175 | * 176 | * @param {Object} props Object containing the react element props. 177 | * @param {Boolean} randomizeId If set to `true`, the function will randomize the `id` property when no `id` was present in the `prop` object. 178 | * @returns An object containing the `id`, `className` and `style` keys, representing the corresponding props passed to the 179 | * component. 180 | */ 181 | export function getContainerAttributesProps(props, randomizeId: boolean = true): {id: string, className: string, style: object} { 182 | return { 183 | id: props.id || (randomizeId ? 'hot-' + Math.random().toString(36).substring(5) : void 0), 184 | className: props.className || '', 185 | style: props.style || {}, 186 | } 187 | } 188 | 189 | /** 190 | * Add the `UNSAFE_` prefixes to the deprecated lifecycle methods for React >= 16.3. 191 | * 192 | * @param {Object} instance Instance to have the methods renamed. 193 | */ 194 | export function addUnsafePrefixes(instance: { 195 | UNSAFE_componentWillUpdate?: Function, 196 | componentWillUpdate: Function, 197 | UNSAFE_componentWillMount?: Function, 198 | componentWillMount: Function 199 | }): void { 200 | const reactSemverArray = React.version.split('.').map((v) => parseInt(v)); 201 | const shouldPrefix = reactSemverArray[0] >= 16 && reactSemverArray[1] >= 3; 202 | 203 | if (shouldPrefix) { 204 | instance.UNSAFE_componentWillUpdate = instance.componentWillUpdate; 205 | instance.componentWillUpdate = void 0; 206 | 207 | instance.UNSAFE_componentWillMount = instance.componentWillMount; 208 | instance.componentWillMount = void 0; 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /src/hotColumn.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactPortal } from 'react'; 2 | import { HotTableProps, HotColumnProps } from './types'; 3 | import { 4 | addUnsafePrefixes, 5 | createEditorPortal, 6 | getExtendedEditorElement 7 | } from './helpers'; 8 | import { SettingsMapper } from './settingsMapper'; 9 | import Handsontable from 'handsontable'; 10 | 11 | class HotColumn extends React.Component { 12 | internalProps: string[]; 13 | columnSettings: Handsontable.GridSettings; 14 | 15 | /** 16 | * Local editor portal cache. 17 | * 18 | * @private 19 | * @type {ReactPortal} 20 | */ 21 | private localEditorPortal: ReactPortal = null; 22 | 23 | /** 24 | * HotColumn class constructor. 25 | * 26 | * @param {HotColumnProps} props Component props. 27 | * @param {*} [context] Component context. 28 | */ 29 | constructor(props: HotColumnProps, context?: any) { 30 | super(props, context); 31 | 32 | addUnsafePrefixes(this); 33 | } 34 | 35 | /** 36 | * Get the local editor portal cache property. 37 | * 38 | * @return {ReactPortal} Local editor portal. 39 | */ 40 | getLocalEditorPortal(): ReactPortal { 41 | return this.localEditorPortal; 42 | } 43 | 44 | /** 45 | * Set the local editor portal cache property. 46 | * 47 | * @param {ReactPortal} portal Local editor portal. 48 | */ 49 | setLocalEditorPortal(portal): void { 50 | this.localEditorPortal = portal; 51 | } 52 | 53 | /** 54 | * Filter out all the internal properties and return an object with just the Handsontable-related props. 55 | * 56 | * @returns {Object} 57 | */ 58 | getSettingsProps(): HotTableProps { 59 | this.internalProps = ['__componentRendererColumns', '_emitColumnSettings', '_columnIndex', '_getChildElementByType', '_getRendererWrapper', 60 | '_getEditorClass', '_getEditorCache', '_getOwnerDocument', 'hot-renderer', 'hot-editor', 'children']; 61 | 62 | return Object.keys(this.props) 63 | .filter(key => { 64 | return !this.internalProps.includes(key); 65 | }) 66 | .reduce((obj, key) => { 67 | obj[key] = this.props[key]; 68 | 69 | return obj; 70 | }, {}); 71 | } 72 | 73 | /** 74 | * Check whether the HotColumn component contains a provided prop. 75 | * 76 | * @param {String} propName Property name. 77 | * @returns {Boolean} 78 | */ 79 | hasProp(propName: string): boolean { 80 | return !!this.props[propName]; 81 | } 82 | 83 | /** 84 | * Get the editor element for the current column. 85 | * 86 | * @returns {React.ReactElement} React editor component element. 87 | */ 88 | getLocalEditorElement(): React.ReactElement | null { 89 | return getExtendedEditorElement(this.props.children, this.props._getEditorCache()); 90 | } 91 | 92 | /** 93 | * Create the column settings based on the data provided to the `HotColumn` component and it's child components. 94 | */ 95 | createColumnSettings(): void { 96 | const rendererElement: React.ReactElement = this.props._getChildElementByType(this.props.children, 'hot-renderer'); 97 | const editorElement: React.ReactElement = this.getLocalEditorElement(); 98 | 99 | this.columnSettings = SettingsMapper.getSettings(this.getSettingsProps()); 100 | 101 | if (rendererElement !== null) { 102 | this.columnSettings.renderer = this.props._getRendererWrapper(rendererElement); 103 | this.props._componentRendererColumns.set(this.props._columnIndex, true); 104 | 105 | } else if (this.hasProp('renderer')) { 106 | this.columnSettings.renderer = this.props.renderer; 107 | 108 | } else { 109 | this.columnSettings.renderer = void 0; 110 | } 111 | 112 | if (editorElement !== null) { 113 | this.columnSettings.editor = this.props._getEditorClass(editorElement); 114 | 115 | } else if (this.hasProp('editor')) { 116 | this.columnSettings.editor = this.props.editor; 117 | 118 | } else { 119 | this.columnSettings.editor = void 0; 120 | } 121 | } 122 | 123 | /** 124 | * Create the local editor portal and its destination HTML element if needed. 125 | * 126 | * @param {React.ReactNode} [children] Children of the HotTable instance. Defaults to `this.props.children`. 127 | */ 128 | createLocalEditorPortal(children = this.props.children): void { 129 | const editorCache = this.props._getEditorCache(); 130 | const localEditorElement: React.ReactElement = getExtendedEditorElement(children, editorCache); 131 | 132 | if (localEditorElement) { 133 | this.setLocalEditorPortal(createEditorPortal(this.props._getOwnerDocument(), localEditorElement, editorCache)); 134 | } 135 | } 136 | 137 | /** 138 | * Emit the column settings to the parent using a prop passed from the parent. 139 | */ 140 | emitColumnSettings(): void { 141 | this.props._emitColumnSettings(this.columnSettings, this.props._columnIndex); 142 | } 143 | 144 | /* 145 | --------------------------------------- 146 | ------- React lifecycle methods ------- 147 | --------------------------------------- 148 | */ 149 | 150 | /** 151 | * Logic performed before the mounting of the HotColumn component. 152 | */ 153 | componentWillMount(): void { 154 | this.createLocalEditorPortal(); 155 | } 156 | 157 | /** 158 | * Logic performed after the mounting of the HotColumn component. 159 | */ 160 | componentDidMount(): void { 161 | this.createColumnSettings(); 162 | this.emitColumnSettings(); 163 | } 164 | 165 | /** 166 | * Logic performed before the updating of the HotColumn component. 167 | */ 168 | componentWillUpdate(nextProps: Readonly, nextState: Readonly<{}>, nextContext: any): void { 169 | this.createLocalEditorPortal(nextProps.children); 170 | } 171 | 172 | /** 173 | * Logic performed after the updating of the HotColumn component. 174 | */ 175 | componentDidUpdate(): void { 176 | this.createColumnSettings(); 177 | this.emitColumnSettings(); 178 | } 179 | 180 | /** 181 | * Render the portals of the editors, if there are any. 182 | * 183 | * @returns {React.ReactElement} 184 | */ 185 | render(): React.ReactElement { 186 | return ( 187 | 188 | {this.getLocalEditorPortal()} 189 | 190 | ) 191 | } 192 | } 193 | 194 | export { HotColumn }; 195 | -------------------------------------------------------------------------------- /src/hotTable.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Handsontable from 'handsontable'; 3 | import { SettingsMapper } from './settingsMapper'; 4 | import { PortalManager } from './portalManager'; 5 | import { HotColumn } from './hotColumn'; 6 | import * as packageJson from '../package.json'; 7 | import { 8 | HotTableProps, 9 | HotEditorElement 10 | } from './types'; 11 | import { 12 | AUTOSIZE_WARNING, 13 | createEditorPortal, 14 | createPortal, 15 | getChildElementByType, 16 | getContainerAttributesProps, 17 | getExtendedEditorElement, 18 | getOriginalEditorClass, 19 | addUnsafePrefixes, 20 | removeEditorContainers, 21 | warn 22 | } from './helpers'; 23 | import PropTypes from 'prop-types'; 24 | 25 | /** 26 | * A Handsontable-ReactJS wrapper. 27 | * 28 | * To implement, use the `HotTable` tag with properties corresponding to Handsontable options. 29 | * For example: 30 | * 31 | * ```js 32 | * 33 | * 34 | * // is analogous to 35 | * let hot = new Handsontable(document.getElementById('hot'), { 36 | * data: dataObject, 37 | * contextMenu: true, 38 | * colHeaders: true, 39 | * width: 600 40 | * height: 300 41 | * }); 42 | * 43 | * ``` 44 | * 45 | * @class HotTable 46 | */ 47 | class HotTable extends React.Component { 48 | /** 49 | * The `id` of the main Handsontable DOM element. 50 | * 51 | * @type {String} 52 | */ 53 | id: string = null; 54 | /** 55 | * Reference to the Handsontable instance. 56 | * 57 | * @type {Object} 58 | */ 59 | hotInstance: Handsontable = null; 60 | /** 61 | * Reference to the main Handsontable DOM element. 62 | * 63 | * @type {HTMLElement} 64 | */ 65 | hotElementRef: HTMLElement = null; 66 | /** 67 | * Class name added to the component DOM element. 68 | * 69 | * @type {String} 70 | */ 71 | className: string; 72 | /** 73 | * Style object passed to the component. 74 | * 75 | * @type {React.CSSProperties} 76 | */ 77 | style: React.CSSProperties; 78 | 79 | /** 80 | * Array of object containing the column settings. 81 | * 82 | * @type {Array} 83 | */ 84 | columnSettings: Handsontable.ColumnSettings[] = []; 85 | 86 | /** 87 | * Component used to manage the renderer portals. 88 | * 89 | * @type {React.Component} 90 | */ 91 | portalManager: PortalManager = null; 92 | 93 | /** 94 | * Array containing the portals cashed to be rendered in bulk after Handsontable's render cycle. 95 | */ 96 | portalCacheArray: React.ReactPortal[] = []; 97 | 98 | /** 99 | * Global editor portal cache. 100 | * 101 | * @private 102 | * @type {React.ReactPortal} 103 | */ 104 | private globalEditorPortal: React.ReactPortal = null; 105 | 106 | /** 107 | * The rendered cells cache. 108 | * 109 | * @private 110 | * @type {Map} 111 | */ 112 | private renderedCellCache: Map = new Map(); 113 | 114 | /** 115 | * Editor cache. 116 | * 117 | * @private 118 | * @type {Map} 119 | */ 120 | private editorCache: Map = new Map(); 121 | 122 | /** 123 | * Map with column indexes (or a string = 'global') as keys, and booleans as values. Each key represents a component-based editor 124 | * declared for the used column index, or a global one, if the key is the `global` string. 125 | * 126 | * @private 127 | * @type {Map} 128 | */ 129 | private componentRendererColumns: Map = new Map(); 130 | 131 | /** 132 | * HotTable class constructor. 133 | * 134 | * @param {HotTableProps} props Component props. 135 | * @param {*} [context] Component context. 136 | */ 137 | constructor(props: HotTableProps, context?: any) { 138 | super(props, context); 139 | 140 | addUnsafePrefixes(this); 141 | } 142 | 143 | /** 144 | * Package version getter. 145 | * 146 | * @returns The version number of the package. 147 | */ 148 | static get version(): string { 149 | return (packageJson as any).version; 150 | } 151 | 152 | /** 153 | * Prop types to be checked at runtime. 154 | */ 155 | static propTypes: object = { 156 | style: PropTypes.object, 157 | id: PropTypes.string, 158 | className: PropTypes.string 159 | }; 160 | 161 | /** 162 | * Get the rendered table cell cache. 163 | * 164 | * @returns {Map} 165 | */ 166 | getRenderedCellCache(): Map { 167 | return this.renderedCellCache; 168 | } 169 | 170 | /** 171 | * Get the editor cache and return it. 172 | * 173 | * @returns {Map} 174 | */ 175 | getEditorCache(): Map { 176 | return this.editorCache; 177 | } 178 | 179 | /** 180 | * Get the global editor portal property. 181 | * 182 | * @return {React.ReactPortal} The global editor portal. 183 | */ 184 | getGlobalEditorPortal(): React.ReactPortal { 185 | return this.globalEditorPortal; 186 | } 187 | 188 | /** 189 | * Set the private editor portal cache property. 190 | * 191 | * @param {React.ReactPortal} portal Global editor portal. 192 | */ 193 | setGlobalEditorPortal(portal: React.ReactPortal): void { 194 | this.globalEditorPortal = portal; 195 | } 196 | 197 | /** 198 | * Clear both the editor and the renderer cache. 199 | */ 200 | clearCache(): void { 201 | const renderedCellCache = this.getRenderedCellCache(); 202 | 203 | this.setGlobalEditorPortal(null); 204 | removeEditorContainers(this.getOwnerDocument()); 205 | this.getEditorCache().clear(); 206 | 207 | renderedCellCache.clear(); 208 | 209 | this.componentRendererColumns.clear(); 210 | } 211 | 212 | /** 213 | * Get the `Document` object corresponding to the main component element. 214 | * 215 | * @returns The `Document` object used by the component. 216 | */ 217 | getOwnerDocument() { 218 | return this.hotElementRef ? this.hotElementRef.ownerDocument : document; 219 | } 220 | 221 | /** 222 | * Set the reference to the main Handsontable DOM element. 223 | * 224 | * @param {HTMLElement} element The main Handsontable DOM element. 225 | */ 226 | private setHotElementRef(element: HTMLElement): void { 227 | this.hotElementRef = element; 228 | } 229 | 230 | /** 231 | * Return a renderer wrapper function for the provided renderer component. 232 | * 233 | * @param {React.ReactElement} rendererElement React renderer component. 234 | * @returns {Handsontable.renderers.Base} The Handsontable rendering function. 235 | */ 236 | getRendererWrapper(rendererElement: React.ReactElement): Handsontable.renderers.Base | any { 237 | const hotTableComponent = this; 238 | 239 | return function (instance, TD, row, col, prop, value, cellProperties) { 240 | const renderedCellCache = hotTableComponent.getRenderedCellCache(); 241 | 242 | if (renderedCellCache.has(`${row}-${col}`)) { 243 | TD.innerHTML = renderedCellCache.get(`${row}-${col}`).innerHTML; 244 | } 245 | 246 | if (TD && !TD.getAttribute('ghost-table')) { 247 | 248 | const {portal, portalContainer} = createPortal(rendererElement, { 249 | TD, 250 | row, 251 | col, 252 | prop, 253 | value, 254 | cellProperties, 255 | isRenderer: true 256 | }, () => { 257 | }, TD.ownerDocument); 258 | 259 | while (TD.firstChild) { 260 | TD.removeChild(TD.firstChild); 261 | } 262 | 263 | TD.appendChild(portalContainer); 264 | 265 | hotTableComponent.portalCacheArray.push(portal); 266 | } 267 | 268 | renderedCellCache.set(`${row}-${col}`, TD); 269 | 270 | return TD; 271 | }; 272 | } 273 | 274 | /** 275 | * Create a fresh class to be used as an editor, based on the provided editor React element. 276 | * 277 | * @param {React.ReactElement} editorElement React editor component. 278 | * @returns {Function} A class to be passed to the Handsontable editor settings. 279 | */ 280 | getEditorClass(editorElement: HotEditorElement): typeof Handsontable.editors.BaseEditor { 281 | const editorClass = getOriginalEditorClass(editorElement); 282 | const editorCache = this.getEditorCache(); 283 | let cachedComponent: React.Component = editorCache.get(editorClass); 284 | 285 | return this.makeEditorClass(cachedComponent); 286 | } 287 | 288 | /** 289 | * Create a class to be passed to the Handsontable's settings. 290 | * 291 | * @param {React.ReactElement} editorComponent React editor component. 292 | * @returns {Function} A class to be passed to the Handsontable editor settings. 293 | */ 294 | makeEditorClass(editorComponent: React.Component): typeof Handsontable.editors.BaseEditor { 295 | const customEditorClass = class CustomEditor extends Handsontable.editors.BaseEditor implements Handsontable._editors.Base { 296 | editorComponent: React.Component; 297 | 298 | constructor(hotInstance, row, col, prop, TD, cellProperties) { 299 | super(hotInstance, row, col, prop, TD, cellProperties); 300 | 301 | (editorComponent as any).hotCustomEditorInstance = this; 302 | 303 | this.editorComponent = editorComponent; 304 | } 305 | 306 | focus() { 307 | } 308 | 309 | getValue() { 310 | } 311 | 312 | setValue() { 313 | } 314 | 315 | open() { 316 | } 317 | 318 | close() { 319 | } 320 | } as any; 321 | 322 | // Fill with the rest of the BaseEditor methods 323 | Object.getOwnPropertyNames(Handsontable.editors.BaseEditor.prototype).forEach(propName => { 324 | if (propName === 'constructor') { 325 | return; 326 | } 327 | 328 | customEditorClass.prototype[propName] = function (...args) { 329 | return editorComponent[propName].call(editorComponent, ...args); 330 | } 331 | }); 332 | 333 | return customEditorClass; 334 | } 335 | 336 | /** 337 | * Get the renderer element for the entire HotTable instance. 338 | * 339 | * @returns {React.ReactElement} React renderer component element. 340 | */ 341 | getGlobalRendererElement(): React.ReactElement { 342 | const hotTableSlots: React.ReactNode = this.props.children; 343 | 344 | return getChildElementByType(hotTableSlots, 'hot-renderer'); 345 | } 346 | 347 | /** 348 | * Get the editor element for the entire HotTable instance. 349 | * 350 | * @param {React.ReactNode} [children] Children of the HotTable instance. Defaults to `this.props.children`. 351 | * @returns {React.ReactElement} React editor component element. 352 | */ 353 | getGlobalEditorElement(children: React.ReactNode = this.props.children): HotEditorElement | null { 354 | return getExtendedEditorElement(children, this.getEditorCache()); 355 | } 356 | 357 | /** 358 | * Create the global editor portal and its destination HTML element if needed. 359 | * 360 | * @param {React.ReactNode} [children] Children of the HotTable instance. Defaults to `this.props.children`. 361 | */ 362 | createGlobalEditorPortal(children: React.ReactNode = this.props.children): void { 363 | const globalEditorElement: HotEditorElement = this.getGlobalEditorElement(children); 364 | 365 | if (globalEditorElement) { 366 | this.setGlobalEditorPortal(createEditorPortal(this.getOwnerDocument() ,globalEditorElement, this.getEditorCache())) 367 | } 368 | } 369 | 370 | /** 371 | * Create a new settings object containing the column settings and global editors and renderers. 372 | * 373 | * @returns {Handsontable.GridSettings} New global set of settings for Handsontable. 374 | */ 375 | createNewGlobalSettings(): Handsontable.GridSettings { 376 | const newSettings = SettingsMapper.getSettings(this.props); 377 | const globalRendererNode = this.getGlobalRendererElement(); 378 | const globalEditorNode = this.getGlobalEditorElement(); 379 | 380 | newSettings.columns = this.columnSettings.length ? this.columnSettings : newSettings.columns; 381 | 382 | if (globalEditorNode) { 383 | newSettings.editor = this.getEditorClass(globalEditorNode); 384 | 385 | } else { 386 | newSettings.editor = this.props.editor || (this.props.settings ? this.props.settings.editor : void 0); 387 | } 388 | 389 | if (globalRendererNode) { 390 | newSettings.renderer = this.getRendererWrapper(globalRendererNode); 391 | this.componentRendererColumns.set('global', true); 392 | 393 | } else { 394 | newSettings.renderer = this.props.renderer || (this.props.settings ? this.props.settings.renderer : void 0); 395 | } 396 | 397 | return newSettings; 398 | } 399 | 400 | /** 401 | * Detect if `autoRowSize` or `autoColumnSize` is defined, and if so, throw an incompatibility warning. 402 | * 403 | * @param {Handsontable.GridSettings} newGlobalSettings New global settings passed as Handsontable config. 404 | */ 405 | displayAutoSizeWarning(newGlobalSettings: Handsontable.GridSettings): void { 406 | if (this.hotInstance.getPlugin('autoRowSize').enabled || this.hotInstance.getPlugin('autoColumnSize').enabled) { 407 | if (this.componentRendererColumns.size > 0) { 408 | warn(AUTOSIZE_WARNING); 409 | } 410 | } 411 | } 412 | 413 | /** 414 | * Sets the column settings based on information received from HotColumn. 415 | * 416 | * @param {HotTableProps} columnSettings Column settings object. 417 | * @param {Number} columnIndex Column index. 418 | */ 419 | setHotColumnSettings(columnSettings: Handsontable.ColumnSettings, columnIndex: number): void { 420 | this.columnSettings[columnIndex] = columnSettings; 421 | } 422 | 423 | /** 424 | * Handsontable's `beforeRender` hook callback. 425 | */ 426 | handsontableBeforeRender(): void { 427 | this.getRenderedCellCache().clear(); 428 | } 429 | 430 | /** 431 | * Handsontable's `afterRender` hook callback. 432 | */ 433 | handsontableAfterRender(): void { 434 | this.portalManager.setState(() => { 435 | return Object.assign({}, { 436 | portals: this.portalCacheArray 437 | }); 438 | 439 | }, () => { 440 | this.portalCacheArray.length = 0; 441 | }); 442 | } 443 | 444 | /** 445 | * Call the `updateSettings` method for the Handsontable instance. 446 | * 447 | * @param {Object} newSettings The settings object. 448 | */ 449 | private updateHot(newSettings: Handsontable.GridSettings): void { 450 | this.hotInstance.updateSettings(newSettings, false); 451 | } 452 | 453 | /** 454 | * Set the portal manager ref. 455 | * 456 | * @param {React.ReactComponent} pmComponent The PortalManager component. 457 | */ 458 | private setPortalManagerRef(pmComponent: PortalManager): void { 459 | this.portalManager = pmComponent; 460 | } 461 | 462 | /* 463 | --------------------------------------- 464 | ------- React lifecycle methods ------- 465 | --------------------------------------- 466 | */ 467 | 468 | /** 469 | * Logic performed before the mounting of the component. 470 | */ 471 | componentWillMount(): void { 472 | this.clearCache(); 473 | this.createGlobalEditorPortal(); 474 | } 475 | 476 | /** 477 | * Initialize Handsontable after the component has mounted. 478 | */ 479 | componentDidMount(): void { 480 | const hotTableComponent = this; 481 | const newGlobalSettings = this.createNewGlobalSettings(); 482 | 483 | this.hotInstance = new Handsontable.Core(this.hotElementRef, newGlobalSettings); 484 | 485 | this.hotInstance.addHook('beforeRender', function (isForced) { 486 | hotTableComponent.handsontableBeforeRender(); 487 | }); 488 | 489 | this.hotInstance.addHook('afterRender', function () { 490 | hotTableComponent.handsontableAfterRender(); 491 | }); 492 | 493 | // `init` missing in Handsontable's type definitions. 494 | (this.hotInstance as any).init(); 495 | 496 | this.displayAutoSizeWarning(newGlobalSettings); 497 | } 498 | 499 | /** 500 | * Logic performed before the component update. 501 | */ 502 | componentWillUpdate(nextProps: Readonly, nextState: Readonly<{}>, nextContext: any): void { 503 | this.clearCache(); 504 | removeEditorContainers(this.getOwnerDocument()); 505 | this.createGlobalEditorPortal(nextProps.children); 506 | } 507 | 508 | /** 509 | * Logic performed after the component update. 510 | */ 511 | componentDidUpdate(): void { 512 | const newGlobalSettings = this.createNewGlobalSettings(); 513 | this.updateHot(newGlobalSettings); 514 | 515 | this.displayAutoSizeWarning(newGlobalSettings); 516 | } 517 | 518 | /** 519 | * Destroy the Handsontable instance when the parent component unmounts. 520 | */ 521 | componentWillUnmount(): void { 522 | this.hotInstance.destroy(); 523 | removeEditorContainers(this.getOwnerDocument()); 524 | } 525 | 526 | /** 527 | * Render the component. 528 | */ 529 | render(): React.ReactElement { 530 | const {id, className, style} = getContainerAttributesProps(this.props); 531 | const isHotColumn = (childNode: any) => childNode.type === HotColumn; 532 | let children = React.Children.toArray(this.props.children); 533 | 534 | // filter out anything that's not a HotColumn 535 | children = children.filter(function (childNode: any) { 536 | return isHotColumn(childNode); 537 | }); 538 | 539 | // clone the HotColumn nodes and extend them with the callbacks 540 | let childClones = children.map((childNode: React.ReactElement, columnIndex: number) => { 541 | return React.cloneElement(childNode, { 542 | _componentRendererColumns: this.componentRendererColumns, 543 | _emitColumnSettings: this.setHotColumnSettings.bind(this), 544 | _columnIndex: columnIndex, 545 | _getChildElementByType: getChildElementByType.bind(this), 546 | _getRendererWrapper: this.getRendererWrapper.bind(this), 547 | _getEditorClass: this.getEditorClass.bind(this), 548 | _getOwnerDocument: this.getOwnerDocument.bind(this), 549 | _getEditorCache: this.getEditorCache.bind(this), 550 | children: childNode.props.children 551 | } as object); 552 | }); 553 | 554 | // add the global editor to the list of children 555 | childClones.push(this.getGlobalEditorPortal()); 556 | 557 | return ( 558 | 559 |

560 | {childClones} 561 |
562 | 563 | 564 | ) 565 | } 566 | } 567 | 568 | export default HotTable; 569 | export { HotTable }; 570 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './hotColumn'; 2 | export * from './hotTable'; 3 | export * from './types'; 4 | export * from './baseEditorComponent'; 5 | export { default } from './hotTable'; 6 | -------------------------------------------------------------------------------- /src/json.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.json" { 2 | const value: any; 3 | export default value; 4 | } 5 | 6 | declare module "json!*" { 7 | const value: any; 8 | export default value; 9 | } 10 | -------------------------------------------------------------------------------- /src/portalManager.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | /** 4 | * Component class used to manage the renderer component portals. 5 | */ 6 | export class PortalManager extends React.Component<{}, {portals?: React.ReactPortal[]}> { 7 | constructor(props) { 8 | super(props); 9 | 10 | this.state = { 11 | portals: [] 12 | }; 13 | } 14 | 15 | render(): React.ReactNode { 16 | return ( 17 | 18 | {this.state.portals} 19 | 20 | ) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/settingsMapper.ts: -------------------------------------------------------------------------------- 1 | import Handsontable from 'handsontable'; 2 | import { HotTableProps } from './types'; 3 | 4 | export class SettingsMapper { 5 | /** 6 | * Parse component settings into Handosntable-compatible settings. 7 | * 8 | * @param {Object} properties Object containing properties from the HotTable object. 9 | * @returns {Object} Handsontable-compatible settings object. 10 | */ 11 | static getSettings(properties: HotTableProps): Handsontable.GridSettings { 12 | let newSettings: Handsontable.GridSettings = {}; 13 | 14 | if (properties.settings) { 15 | let settings = properties.settings; 16 | for (const key in settings) { 17 | if (settings.hasOwnProperty(key)) { 18 | newSettings[key] = settings[key]; 19 | } 20 | } 21 | } 22 | 23 | for (const key in properties) { 24 | if (key !== 'settings' && key !== 'children' && properties.hasOwnProperty(key)) { 25 | newSettings[key] = properties[key]; 26 | } 27 | } 28 | 29 | return newSettings; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/types.tsx: -------------------------------------------------------------------------------- 1 | import Handsontable from 'handsontable'; 2 | import React from 'react'; 3 | import { ConnectedComponent } from 'react-redux'; 4 | 5 | /** 6 | * Type of the editor component's ReactElement. 7 | */ 8 | export type HotEditorElement = React.ReactElement<{}, ConnectedComponent | any>; 9 | 10 | /** 11 | * Interface for the `prop` of the HotTable component - extending the default Handsontable settings with additional, 12 | * component-related properties. 13 | */ 14 | export interface HotTableProps extends Handsontable.GridSettings { 15 | id?: string, 16 | className?: string, 17 | style?: React.CSSProperties, 18 | settings?: Handsontable.GridSettings 19 | children?: React.ReactNode 20 | } 21 | 22 | /** 23 | * Interface for the props of the component-based editors. 24 | */ 25 | export interface HotEditorProps { 26 | "hot-editor": any, 27 | id?: string, 28 | className?: string, 29 | style?: React.CSSProperties, 30 | } 31 | 32 | /** 33 | * Properties related to the HotColumn architecture. 34 | */ 35 | export interface HotColumnProps extends Handsontable.GridSettings { 36 | _componentRendererColumns?: Map; 37 | _emitColumnSettings?: (columnSettings: Handsontable.GridSettings, columnIndex: number) => void; 38 | _columnIndex?: number, 39 | _getChildElementByType?: (children: React.ReactNode, type: string) => React.ReactElement; 40 | _getRendererWrapper?: (rendererNode: React.ReactElement) => Handsontable.renderers.Base; 41 | _getEditorClass?: (editorElement: React.ReactElement) => typeof Handsontable.editors.BaseEditor; 42 | _getEditorCache?: () => Map; 43 | _getOwnerDocument?: () => Document; 44 | children?: React.ReactNode; 45 | } 46 | -------------------------------------------------------------------------------- /test-tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "target": "esnext", 5 | "jsx": "react", 6 | "module": "esnext", 7 | "allowSyntheticDefaultImports": true, 8 | "esModuleInterop": true, 9 | "baseUrl": "." 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/_helpers.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { HotTable } from '../src/hotTable'; 3 | import { addUnsafePrefixes } from '../src/helpers'; 4 | import { BaseEditorComponent } from '../src/baseEditorComponent'; 5 | 6 | export function sleep(delay = 100) { 7 | return Promise.resolve({ 8 | then(resolve) { 9 | if (delay === 0) { 10 | setImmediate(resolve); 11 | } else { 12 | setTimeout(resolve, delay); 13 | } 14 | } 15 | }); 16 | } 17 | 18 | export function mockElementDimensions(element, width, height) { 19 | Object.defineProperty(element, 'clientWidth', { 20 | value: width 21 | }); 22 | Object.defineProperty(element, 'clientHeight', { 23 | value: height 24 | }); 25 | 26 | Object.defineProperty(element, 'offsetWidth', { 27 | value: width 28 | }); 29 | Object.defineProperty(element, 'offsetHeight', { 30 | value: height 31 | }); 32 | } 33 | 34 | export function simulateKeyboardEvent(type, keyCode) { 35 | const event = document.createEvent('KeyboardEvent'); 36 | const init = (event as any).initKeyboardEvent !== void 0 ? 'initKeyboardEvent' : 'initKeyEvent'; 37 | 38 | event[init](type, true, true, window, false, false, false, false, keyCode, 0); 39 | 40 | document.activeElement.dispatchEvent(event); 41 | } 42 | 43 | export function simulateMouseEvent(element, type) { 44 | const event = document.createEvent('Events'); 45 | event.initEvent(type, true, false); 46 | 47 | element.dispatchEvent(event); 48 | } 49 | 50 | class IndividualPropsWrapper extends React.Component<{ref?: string, id?: string}, {hotSettings?: object}> { 51 | hotTable: typeof HotTable; 52 | 53 | constructor(props) { 54 | super(props); 55 | 56 | addUnsafePrefixes(this); 57 | } 58 | 59 | componentWillMount() { 60 | this.setState({}); 61 | } 62 | 63 | private setHotElementRef(component: typeof HotTable): void { 64 | this.hotTable = component; 65 | } 66 | 67 | render(): React.ReactElement { 68 | return ( 69 |
70 | 77 |
78 | ); 79 | } 80 | } 81 | 82 | export { IndividualPropsWrapper }; 83 | 84 | class SingleObjectWrapper extends React.Component<{ref?: string, id?: string}, {hotSettings?: object}> { 85 | hotTable: typeof HotTable; 86 | 87 | constructor(props) { 88 | super(props); 89 | 90 | addUnsafePrefixes(this); 91 | } 92 | 93 | private setHotElementRef(component: typeof HotTable): void { 94 | this.hotTable = component; 95 | } 96 | 97 | componentWillMount() { 98 | this.setState({}); 99 | } 100 | 101 | render(): React.ReactElement { 102 | return ( 103 |
104 | 112 |
113 | ); 114 | } 115 | } 116 | 117 | export { SingleObjectWrapper }; 118 | 119 | export class RendererComponent extends React.Component { 120 | render(): React.ReactElement { 121 | return ( 122 | <> 123 | value: {this.props.value} 124 | 125 | ); 126 | } 127 | } 128 | 129 | export class EditorComponent extends BaseEditorComponent<{}, {value?: any}> { 130 | mainElementRef: any; 131 | containerStyle: any; 132 | 133 | constructor(props) { 134 | super(props); 135 | 136 | this.mainElementRef = React.createRef(); 137 | 138 | this.state = { 139 | value: '' 140 | }; 141 | 142 | this.containerStyle = { 143 | display: 'none' 144 | }; 145 | } 146 | 147 | getValue() { 148 | return this.state.value; 149 | } 150 | 151 | setValue(value, callback) { 152 | this.setState((state, props) => { 153 | return {value: value}; 154 | }, callback); 155 | } 156 | 157 | setNewValue() { 158 | this.setValue('new-value', () => { 159 | this.finishEditing(); 160 | }) 161 | } 162 | 163 | open() { 164 | this.mainElementRef.current.style.display = 'block'; 165 | } 166 | 167 | close() { 168 | this.mainElementRef.current.style.display = 'none'; 169 | } 170 | 171 | render(): React.ReactElement { 172 | return ( 173 |
174 | 175 |
176 | ); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /test/autoSizeWarning.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | mount, 4 | ReactWrapper 5 | } from 'enzyme'; 6 | import { 7 | HotTable 8 | } from '../src/hotTable'; 9 | import { 10 | HotColumn 11 | } from '../src/hotColumn'; 12 | import { 13 | mockElementDimensions, 14 | sleep 15 | } from './_helpers'; 16 | import { 17 | AUTOSIZE_WARNING 18 | } from '../src/helpers'; 19 | import Handsontable from 'handsontable'; 20 | 21 | beforeEach(() => { 22 | let container = document.createElement('DIV'); 23 | container.id = 'hotContainer'; 24 | document.body.appendChild(container); 25 | }); 26 | 27 | describe('`autoRowSize`/`autoColumns` warning', () => { 28 | it('should recognize whether `autoRowSize` or `autoColumnSize` is enabled and throw a warning, if a global component-based renderer' + 29 | 'is defined (using the default Handsontable settings - autoColumnSize is enabled by default)', async (done) => { 30 | console.warn = jasmine.createSpy('warn'); 31 | 32 | const RendererComponent = function (props) { 33 | return <>test 34 | }; 35 | 36 | const wrapper: ReactWrapper<{}, {}, typeof HotTable> = mount( 37 | 45 | 46 | , {attachTo: document.body.querySelector('#hotContainer')} 47 | ); 48 | 49 | await sleep(100); 50 | 51 | expect(console.warn).toHaveBeenCalledWith(AUTOSIZE_WARNING); 52 | 53 | wrapper.detach(); 54 | done(); 55 | }); 56 | 57 | it('should recognize whether `autoRowSize` or `autoColumnSize` is enabled and throw a warning, if a global component-based renderer' + 58 | 'is defined', async (done) => { 59 | console.warn = jasmine.createSpy('warn'); 60 | 61 | const RendererComponent = function (props) { 62 | return <>test 63 | }; 64 | 65 | const wrapper: ReactWrapper<{}, {}, typeof HotTable> = mount( 66 | 76 | 77 | , {attachTo: document.body.querySelector('#hotContainer')} 78 | ); 79 | 80 | await sleep(100); 81 | 82 | expect(console.warn).toHaveBeenCalledWith(AUTOSIZE_WARNING); 83 | 84 | wrapper.detach(); 85 | done(); 86 | }); 87 | 88 | it('should recognize whether `autoRowSize` or `autoColumnSize` is enabled and throw a warning, if a component-based renderer' + 89 | 'is defined for any column (using the default Handsontable settings - autoColumnSize enabled by default)', async (done) => { 90 | console.warn = jasmine.createSpy('warn'); 91 | 92 | const RendererComponent = function (props) { 93 | return <>test 94 | }; 95 | 96 | const wrapper: ReactWrapper<{}, {}, typeof HotTable> = mount( 97 | 105 | 106 | 107 | 108 | 109 | 110 | , {attachTo: document.body.querySelector('#hotContainer')} 111 | ); 112 | 113 | await sleep(100); 114 | 115 | expect(console.warn).toHaveBeenCalledWith(AUTOSIZE_WARNING); 116 | 117 | wrapper.detach(); 118 | done(); 119 | }); 120 | 121 | it('should recognize whether `autoRowSize` or `autoColumnSize` is enabled and throw a warning, if a component-based renderer' + 122 | 'is defined for any column', async (done) => { 123 | console.warn = jasmine.createSpy('warn'); 124 | 125 | const RendererComponent = function (props) { 126 | return <>test 127 | }; 128 | 129 | const wrapper: ReactWrapper<{}, {}, typeof HotTable> = mount( 130 | 140 | 141 | 142 | 143 | 144 | 145 | , {attachTo: document.body.querySelector('#hotContainer')} 146 | ); 147 | 148 | await sleep(100); 149 | 150 | expect(console.warn).toHaveBeenCalledWith(AUTOSIZE_WARNING); 151 | 152 | wrapper.detach(); 153 | done(); 154 | }); 155 | 156 | it('should throw a warning, when `autoRowSize` or `autoColumnSize` is defined, and both function-based and component-based renderers are defined', async (done) => { 157 | console.warn = jasmine.createSpy('warn'); 158 | 159 | const RendererComponent = function (props) { 160 | return <>test 161 | }; 162 | 163 | const wrapper: ReactWrapper<{}, {}, typeof HotTable> = mount( 164 | 179 | 180 | 181 | 182 | 183 | 184 | , {attachTo: document.body.querySelector('#hotContainer')} 185 | ); 186 | 187 | await sleep(100); 188 | 189 | expect(console.warn).toHaveBeenCalledWith(AUTOSIZE_WARNING); 190 | 191 | wrapper.detach(); 192 | done(); 193 | }); 194 | 195 | it('should NOT throw any warnings, when `autoRowSize` or `autoColumnSize` is defined, but only global function-based renderers were defined', async (done) => { 196 | console.warn = jasmine.createSpy('warn'); 197 | 198 | const wrapper: ReactWrapper<{}, {}, typeof HotTable> = mount( 199 | 210 | 211 | 212 | 213 | , {attachTo: document.body.querySelector('#hotContainer')} 214 | ); 215 | 216 | await sleep(100); 217 | 218 | expect(console.warn).not.toHaveBeenCalled(); 219 | 220 | wrapper.detach(); 221 | done(); 222 | }); 223 | 224 | it('should NOT throw any warnings, when `autoRowSize` or `autoColumnSize` is defined, but only function-based renderers were defined for columns', async (done) => { 225 | console.warn = jasmine.createSpy('warn'); 226 | 227 | const wrapper: ReactWrapper<{}, {}, typeof HotTable> = mount( 228 | 239 | 240 | 241 | 242 | , {attachTo: document.body.querySelector('#hotContainer')} 243 | ); 244 | 245 | await sleep(100); 246 | 247 | expect(console.warn).not.toHaveBeenCalled(); 248 | 249 | wrapper.detach(); 250 | done(); 251 | }); 252 | 253 | it('should NOT throw any warnings, when `autoRowSize` or `autoColumnSize` is defined, but only function-based renderers were defined for columns, when ' + 254 | 'the `columns` option is defined as a function', async (done) => { 255 | console.warn = jasmine.createSpy('warn'); 256 | 257 | const wrapper: ReactWrapper<{}, {}, typeof HotTable> = mount( 258 | 273 | 274 | 275 | 276 | , {attachTo: document.body.querySelector('#hotContainer')} 277 | ); 278 | 279 | await sleep(100); 280 | 281 | expect(console.warn).not.toHaveBeenCalled(); 282 | 283 | wrapper.detach(); 284 | done(); 285 | }); 286 | }); 287 | -------------------------------------------------------------------------------- /test/componentInternals.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | mount, 4 | ReactWrapper 5 | } from 'enzyme'; 6 | import { 7 | HotTable 8 | } from '../src/hotTable'; 9 | import { 10 | HotColumn 11 | } from '../src/hotColumn'; 12 | import { 13 | mockElementDimensions, 14 | sleep, 15 | RendererComponent, 16 | EditorComponent 17 | } from './_helpers'; 18 | import { BaseEditorComponent } from '../src/baseEditorComponent'; 19 | import Handsontable from 'handsontable'; 20 | 21 | beforeEach(() => { 22 | let container = document.createElement('DIV'); 23 | container.id = 'hotContainer'; 24 | document.body.appendChild(container); 25 | }); 26 | 27 | describe('Subcomponent state', () => { 28 | it('should be possible to set the state of the renderer components passed to HotTable and HotColumn', async (done) => { 29 | class RendererComponent2 extends React.Component { 30 | constructor(props) { 31 | super(props); 32 | 33 | this.state = { 34 | value: 'initial' 35 | } 36 | } 37 | 38 | render(): React.ReactElement { 39 | return ( 40 | <> 41 | {this.state.value} 42 | 43 | ); 44 | } 45 | } 46 | 47 | let zeroRendererInstance = null; 48 | let oneRendererInstance = null; 49 | const wrapper: ReactWrapper<{}, {}, typeof HotTable> = mount( 50 | 62 | 67 | 68 | 69 | 74 | 75 | , {attachTo: document.body.querySelector('#hotContainer')} 76 | ); 77 | 78 | await sleep(100); 79 | 80 | const hotTableInstance = wrapper.instance(); 81 | const hotInstance = hotTableInstance.hotInstance; 82 | 83 | expect(hotInstance.getCell(0, 0).innerHTML).toEqual('
initial
'); 84 | expect(hotInstance.getCell(0, 1).innerHTML).toEqual('
initial
'); 85 | 86 | zeroRendererInstance.setState({ 87 | value: 'altered' 88 | }); 89 | 90 | oneRendererInstance.setState({ 91 | value: 'altered as well' 92 | }); 93 | 94 | expect(hotInstance.getCell(0, 0).innerHTML).toEqual('
altered
'); 95 | expect(hotInstance.getCell(0, 1).innerHTML).toEqual('
altered as well
'); 96 | 97 | wrapper.detach(); 98 | done(); 99 | }); 100 | 101 | it('should be possible to set the state of the editor components passed to HotTable and HotColumn', async (done) => { 102 | class RendererEditor2 extends BaseEditorComponent { 103 | constructor(props) { 104 | super(props); 105 | 106 | this.state = { 107 | value: 'initial' 108 | } 109 | } 110 | 111 | render(): React.ReactElement { 112 | return ( 113 |
114 | {this.state.value} 115 |
116 | ); 117 | } 118 | } 119 | 120 | let globalEditorInstance = null; 121 | let columnEditorInstance = null; 122 | const wrapper: ReactWrapper<{}, {}, typeof HotTable> = mount( 123 | 133 | 136 | 137 | 138 | 141 | 142 | , {attachTo: document.body.querySelector('#hotContainer')} 143 | ); 144 | 145 | await sleep(100); 146 | 147 | const hotTableInstance = wrapper.instance(); 148 | const hotInstance = hotTableInstance.hotInstance; 149 | 150 | expect(document.querySelector('#first-editor').innerHTML).toEqual('initial'); 151 | expect(document.querySelector('#second-editor').innerHTML).toEqual('initial'); 152 | 153 | globalEditorInstance.setState({ 154 | value: 'altered' 155 | }); 156 | 157 | columnEditorInstance.setState({ 158 | value: 'altered as well' 159 | }); 160 | 161 | expect(document.querySelector('#first-editor').innerHTML).toEqual('altered'); 162 | expect(document.querySelector('#second-editor').innerHTML).toEqual('altered as well'); 163 | 164 | wrapper.detach(); 165 | done(); 166 | }); 167 | }); 168 | 169 | describe('Component lifecyle', () => { 170 | it('renderer components should trigger their lifecycle methods', async (done) => { 171 | class RendererComponent2 extends React.Component { 172 | constructor(props) { 173 | super(props); 174 | 175 | rendererCounters.set(`${this.props.row}-${this.props.col}`, { 176 | willMount: 0, 177 | didMount: 0, 178 | willUnmount: 0 179 | }); 180 | } 181 | 182 | UNSAFE_componentWillMount(): void { 183 | const counters = rendererCounters.get(`${this.props.row}-${this.props.col}`); 184 | counters.willMount++; 185 | } 186 | 187 | componentDidMount(): void { 188 | const counters = rendererCounters.get(`${this.props.row}-${this.props.col}`); 189 | counters.didMount++; 190 | } 191 | 192 | componentWillUnmount(): void { 193 | const counters = rendererCounters.get(`${this.props.row}-${this.props.col}`); 194 | counters.willUnmount++; 195 | } 196 | 197 | render(): React.ReactElement { 198 | return ( 199 | <> 200 | test 201 | 202 | ); 203 | } 204 | } 205 | 206 | let secondGo = false; 207 | const rendererRefs = new Map(); 208 | const rendererCounters = new Map(); 209 | const wrapper: ReactWrapper<{}, {}, typeof HotTable> = mount( 210 | 222 | 227 | 228 | 229 | 234 | 235 | , {attachTo: document.body.querySelector('#hotContainer')} 236 | ); 237 | 238 | await sleep(100); 239 | 240 | const hotTableInstance = wrapper.instance(); 241 | const hotInstance = hotTableInstance.hotInstance; 242 | 243 | rendererCounters.forEach((counters) => { 244 | expect(counters.willMount).toEqual(1); 245 | expect(counters.didMount).toEqual(1); 246 | expect(counters.willUnmount).toEqual(0); 247 | }); 248 | 249 | secondGo = true; 250 | 251 | hotInstance.render(); 252 | await sleep(300); 253 | 254 | rendererCounters.forEach((counters) => { 255 | expect(counters.willMount).toEqual(1); 256 | expect(counters.didMount).toEqual(1); 257 | expect(counters.willUnmount).toEqual(1); 258 | }); 259 | 260 | wrapper.detach(); 261 | done(); 262 | }); 263 | 264 | it('editor components should trigger their lifecycle methods', async (done) => { 265 | class EditorComponent2 extends BaseEditorComponent { 266 | constructor(props) { 267 | super(props); 268 | 269 | editorCounters.set(`${this.props.row}-${this.props.col}`, { 270 | willMount: 0, 271 | didMount: 0, 272 | willUnmount: 0 273 | }); 274 | } 275 | 276 | UNSAFE_componentWillMount(): void { 277 | const counters = editorCounters.get(`${this.props.row}-${this.props.col}`); 278 | counters.willMount++; 279 | } 280 | 281 | componentDidMount(): void { 282 | const counters = editorCounters.get(`${this.props.row}-${this.props.col}`); 283 | counters.didMount++; 284 | } 285 | 286 | componentWillUnmount(): void { 287 | const counters = editorCounters.get(`${this.props.row}-${this.props.col}`); 288 | counters.willUnmount++; 289 | } 290 | 291 | render(): React.ReactElement { 292 | return ( 293 | <> 294 | test 295 | 296 | ); 297 | } 298 | } 299 | 300 | let secondGo = false; 301 | const editorRefs = new Map(); 302 | const editorCounters = new Map(); 303 | const childrenArray = [ 304 | 309 | ]; 310 | const wrapper: ReactWrapper<{}, {}, typeof HotTable> = mount( 311 | 321 | {childrenArray} 322 | , {attachTo: document.body.querySelector('#hotContainer')} 323 | ); 324 | 325 | await sleep(100); 326 | 327 | const hotTableInstance = wrapper.instance(); 328 | 329 | editorCounters.forEach((counters) => { 330 | expect(counters.willMount).toEqual(1); 331 | expect(counters.didMount).toEqual(1); 332 | expect(counters.willUnmount).toEqual(0); 333 | }); 334 | 335 | secondGo = true; 336 | 337 | childrenArray.length = 0; 338 | hotTableInstance.forceUpdate(); 339 | await sleep(100); 340 | 341 | editorCounters.forEach((counters) => { 342 | expect(counters.willMount).toEqual(1); 343 | expect(counters.didMount).toEqual(1); 344 | expect(counters.willUnmount).toEqual(1); 345 | }); 346 | 347 | wrapper.detach(); 348 | done(); 349 | }); 350 | }); 351 | -------------------------------------------------------------------------------- /test/hotColumn.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | mount, 4 | ReactWrapper 5 | } from 'enzyme'; 6 | import Handsontable from 'handsontable'; 7 | import { HotTable } from '../src/hotTable'; 8 | import { HotColumn } from '../src/hotColumn'; 9 | import { 10 | RendererComponent, 11 | mockElementDimensions, 12 | sleep, 13 | EditorComponent, 14 | simulateKeyboardEvent, 15 | simulateMouseEvent 16 | } from './_helpers'; 17 | 18 | beforeEach(() => { 19 | let container = document.createElement('DIV'); 20 | container.id = 'hotContainer'; 21 | document.body.appendChild(container); 22 | }); 23 | 24 | describe('Passing column settings using HotColumn', () => { 25 | it('should apply the Handsontable settings passed as HotColumn arguments to the Handsontable instance', async (done) => { 26 | const wrapper: ReactWrapper<{}, {}, typeof HotTable> = mount( 27 | 32 | 33 | 34 | , {attachTo: document.body.querySelector('#hotContainer')} 35 | ); 36 | 37 | await sleep(300); 38 | 39 | let hotInstance = wrapper.instance().hotInstance; 40 | 41 | expect(hotInstance.getSettings().columns[0].title).toEqual('test title'); 42 | expect(hotInstance.getCellMeta(0, 0).readOnly).toEqual(false); 43 | 44 | expect(hotInstance.getSettings().columns[1].title).toEqual(void 0); 45 | expect(hotInstance.getCellMeta(0, 1).readOnly).toEqual(true); 46 | 47 | expect(hotInstance.getSettings().licenseKey).toEqual('non-commercial-and-evaluation'); 48 | 49 | wrapper.detach(); 50 | 51 | done(); 52 | }); 53 | }); 54 | 55 | describe('Renderer configuration using React components', () => { 56 | it('should use the renderer component as Handsontable renderer, when it\'s nested under HotColumn and assigned the \'hot-renderer\' attribute', async (done) => { 57 | const wrapper: ReactWrapper<{}, {}, typeof HotTable> = mount( 58 | 70 | 71 | 72 | 73 | 74 | , {attachTo: document.body.querySelector('#hotContainer')} 75 | ); 76 | 77 | await sleep(300); 78 | 79 | let hotInstance = wrapper.instance().hotInstance; 80 | 81 | expect(hotInstance.getCell(0, 0).innerHTML).toEqual('A1'); 82 | expect(hotInstance.getCell(0, 1).innerHTML).toEqual('
value: B1
'); 83 | 84 | hotInstance.scrollViewportTo(99, 0); 85 | hotInstance.render(); 86 | 87 | await sleep(300); 88 | 89 | expect(hotInstance.getCell(99, 0).innerHTML).toEqual('A100'); 90 | expect(hotInstance.getCell(99, 1).innerHTML).toEqual('
value: B100
'); 91 | 92 | wrapper.detach(); 93 | 94 | done(); 95 | }); 96 | }); 97 | 98 | describe('Editor configuration using React components', () => { 99 | it('should use the editor component as Handsontable editor, when it\'s nested under HotTable and assigned the \'hot-editor\' attribute', async (done) => { 100 | const wrapper: ReactWrapper<{}, {}, typeof HotTable> = mount( 101 | 111 | 112 | 113 | 114 | 115 | , {attachTo: document.body.querySelector('#hotContainer')} 116 | ); 117 | 118 | await sleep(100); 119 | 120 | const hotInstance = wrapper.instance().hotInstance; 121 | 122 | expect((document.querySelector('#editorComponentContainer') as any).style.display).toEqual('none'); 123 | 124 | hotInstance.selectCell(0, 1); 125 | simulateKeyboardEvent('keydown', 13); 126 | 127 | expect((document.querySelector('#editorComponentContainer') as any).style.display).toEqual('block'); 128 | 129 | expect(hotInstance.getDataAtCell(0, 1)).toEqual('B1'); 130 | 131 | simulateMouseEvent(document.querySelector('#editorComponentContainer button'), 'click'); 132 | 133 | expect(hotInstance.getDataAtCell(0, 1)).toEqual('new-value'); 134 | 135 | hotInstance.getActiveEditor().close(); 136 | 137 | expect((document.querySelector('#editorComponentContainer') as any).style.display).toEqual('none'); 138 | 139 | hotInstance.selectCell(0, 0); 140 | simulateKeyboardEvent('keydown', 13); 141 | 142 | expect((document.querySelector('#editorComponentContainer') as any).style.display).toEqual('none'); 143 | 144 | wrapper.detach(); 145 | 146 | done(); 147 | }); 148 | }); 149 | 150 | describe('Dynamic HotColumn configuration changes', () => { 151 | it('should be possible to rearrange and change the column + editor + renderer configuration dynamically', async (done) => { 152 | function RendererComponent2(props) { 153 | return ( 154 | <>r2: {props.value} 155 | ); 156 | } 157 | 158 | class WrapperComponent extends React.Component { 159 | constructor(props) { 160 | super(props); 161 | 162 | this.state = { 163 | setup: [ 164 | , 165 | 166 | 167 | , 168 | 169 | 170 | 171 | ] 172 | } 173 | } 174 | 175 | render() { 176 | return ( 177 | 190 | {this.state.setup} 191 | 192 | ); 193 | }; 194 | } 195 | 196 | let hotTableInstanceRef = React.createRef(); 197 | 198 | const wrapper: ReactWrapper<{}, {}, typeof HotTable> = mount( 199 | 200 | , {attachTo: document.body.querySelector('#hotContainer')} 201 | ); 202 | 203 | await sleep(300); 204 | 205 | let hotInstance = (hotTableInstanceRef.current as any).hotInstance; 206 | let editorElement = document.querySelector('#editorComponentContainer'); 207 | 208 | expect(hotInstance.getSettings().columns[0].title).toEqual('test title'); 209 | expect(hotInstance.getSettings().columns[0].className).toEqual('first-column-class-name'); 210 | expect(hotInstance.getCell(0, 0).innerHTML).toEqual('
value: A1
'); 211 | expect(hotInstance.getCell(1, 0).innerHTML).toEqual('
value: A2
'); 212 | hotInstance.selectCell(0, 0); 213 | hotInstance.getActiveEditor().open(); 214 | expect(hotInstance.getActiveEditor().constructor.name).toEqual('CustomEditor'); 215 | expect(hotInstance.getActiveEditor().editorComponent.__proto__.constructor.name).toEqual('EditorComponent'); 216 | expect(editorElement.style.display).toEqual('block'); 217 | expect(editorElement.parentNode.style.background).toEqual('red'); 218 | expect(editorElement.parentNode.id).toEqual('editor-id-1'); 219 | expect(editorElement.parentNode.className.includes('editor-className-1')).toBe(true); 220 | 221 | hotInstance.getActiveEditor().close(); 222 | 223 | expect(hotInstance.getSettings().columns[1].title).toEqual('test title 2'); 224 | expect(hotInstance.getSettings().columns[1].className).toEqual(void 0); 225 | expect(hotInstance.getCell(0, 1).innerHTML).toEqual('
r2: B1
'); 226 | expect(hotInstance.getCell(1, 1).innerHTML).toEqual('
r2: B2
'); 227 | hotInstance.selectCell(0, 1); 228 | expect(hotInstance.getActiveEditor().constructor.name).toEqual('TextEditor'); 229 | expect(hotInstance.getActiveEditor().editorComponent).toEqual(void 0); 230 | expect((document.querySelector('#editorComponentContainer') as any).style.display).toEqual('none'); 231 | 232 | wrapper.instance().setState({ 233 | setup: [ 234 | , 235 | 236 | 237 | , 238 | 239 | 240 | 241 | ] 242 | }); 243 | 244 | await sleep(100); 245 | 246 | editorElement = document.querySelector('#editorComponentContainer'); 247 | 248 | expect(hotInstance.getSettings().columns[0].title).toEqual('test title 2'); 249 | expect(hotInstance.getSettings().columns[0].className).toEqual(void 0); 250 | expect(hotInstance.getCell(0, 0).innerHTML).toEqual('
r2: A1
'); 251 | expect(hotInstance.getCell(1, 0).innerHTML).toEqual('
r2: A2
'); 252 | hotInstance.selectCell(0, 0); 253 | hotInstance.getActiveEditor().open(); 254 | expect(hotInstance.getActiveEditor().constructor.name).toEqual('CustomEditor'); 255 | expect(hotInstance.getActiveEditor().editorComponent.__proto__.constructor.name).toEqual('EditorComponent'); 256 | expect(editorElement.style.display).toEqual('block'); 257 | expect(editorElement.parentNode.style.background).toEqual('blue'); 258 | expect(editorElement.parentNode.id).toEqual('editor-id-2'); 259 | expect(editorElement.parentNode.className.includes('editor-className-2')).toBe(true); 260 | hotInstance.getActiveEditor().close(); 261 | 262 | expect(hotInstance.getSettings().columns[1].title).toEqual('test title'); 263 | expect(hotInstance.getSettings().columns[1].className).toEqual('first-column-class-name'); 264 | expect(hotInstance.getCell(0, 1).innerHTML).toEqual('
value: B1
'); 265 | expect(hotInstance.getCell(1, 1).innerHTML).toEqual('
value: B2
'); 266 | hotInstance.selectCell(0, 1); 267 | hotInstance.getActiveEditor().open(); 268 | expect(hotInstance.getActiveEditor().constructor.name).toEqual('CustomEditor'); 269 | expect(hotInstance.getActiveEditor().editorComponent.__proto__.constructor.name).toEqual('EditorComponent'); 270 | expect((document.querySelector('#editorComponentContainer') as any).style.display).toEqual('block'); 271 | hotInstance.getActiveEditor().close(); 272 | 273 | expect(hotInstance.getSettings().licenseKey).toEqual('non-commercial-and-evaluation'); 274 | 275 | wrapper.detach(); 276 | 277 | done(); 278 | }); 279 | }); 280 | -------------------------------------------------------------------------------- /test/hotTable.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | mount, 4 | ReactWrapper 5 | } from 'enzyme'; 6 | import { 7 | HotTable 8 | } from '../src/hotTable'; 9 | import { 10 | IndividualPropsWrapper, 11 | mockElementDimensions, 12 | RendererComponent, 13 | EditorComponent, 14 | SingleObjectWrapper, 15 | sleep, 16 | simulateKeyboardEvent, 17 | simulateMouseEvent 18 | } from './_helpers'; 19 | import Handsontable from 'handsontable'; 20 | 21 | beforeEach(() => { 22 | let container = document.createElement('DIV'); 23 | container.id = 'hotContainer'; 24 | document.body.appendChild(container); 25 | }); 26 | 27 | describe('Handsontable initialization', () => { 28 | it('should render Handsontable when using the HotTable component', async (done) => { 29 | const wrapper: ReactWrapper<{}, {}, typeof HotTable> = mount( 30 | , {attachTo: document.body.querySelector('#hotContainer')} 35 | ); 36 | 37 | await sleep(300); 38 | 39 | let hotInstance = wrapper.instance().hotInstance; 40 | 41 | expect(hotInstance).not.toBe(null); 42 | expect(hotInstance).not.toBe(void 0); 43 | 44 | expect(hotInstance.rootElement.id).toEqual('test-hot'); 45 | 46 | wrapper.detach(); 47 | 48 | done(); 49 | }); 50 | 51 | it('should pass the provided properties to the Handsontable instance', async (done) => { 52 | const wrapper: ReactWrapper<{}, {}, typeof HotTable> = mount( 53 | , {attachTo: document.body.querySelector('#hotContainer')} 60 | ); 61 | 62 | await sleep(300); 63 | let hotInstance = wrapper.instance().hotInstance; 64 | 65 | expect(hotInstance.getPlugin('contextMenu').enabled).toBe(true); 66 | expect(hotInstance.getSettings().rowHeaders).toBe(true); 67 | expect(hotInstance.getSettings().colHeaders).toBe(true); 68 | expect(JSON.stringify(hotInstance.getData())).toEqual('[[2]]'); 69 | wrapper.detach(); 70 | 71 | done(); 72 | }); 73 | }); 74 | 75 | describe('Updating the Handsontable settings', () => { 76 | it('should call the updateSettings method of Handsontable, when the component properties get updated (when providing properties individually)', async (done) => { 77 | const wrapper: ReactWrapper<{}, {}, typeof IndividualPropsWrapper> = mount( 78 | , {attachTo: document.body.querySelector('#hotContainer')} 79 | ); 80 | 81 | await sleep(300); 82 | const hotInstance = wrapper.instance().hotTable.hotInstance; 83 | 84 | let updateSettingsCount = 0; 85 | 86 | hotInstance.addHook('afterUpdateSettings', () => { 87 | updateSettingsCount++; 88 | }); 89 | 90 | await sleep(300); 91 | wrapper.instance().setState({hotSettings: {data: [[2]], contextMenu: true, readOnly: true}}); 92 | 93 | expect(updateSettingsCount).toEqual(1); 94 | wrapper.detach(); 95 | done(); 96 | }); 97 | 98 | it('should call the updateSettings method of Handsontable, when the component properties get updated (when providing properties as a single settings object)', async (done) => { 99 | const wrapper: ReactWrapper<{}, {}, typeof SingleObjectWrapper> = mount( 100 | , {attachTo: document.body.querySelector('#hotContainer')} 101 | ); 102 | 103 | await sleep(300); 104 | 105 | const hotInstance = wrapper.instance().hotTable.hotInstance; 106 | let updateSettingsCount = 0; 107 | 108 | hotInstance.addHook('afterUpdateSettings', () => { 109 | updateSettingsCount++; 110 | }); 111 | 112 | await sleep(300); 113 | wrapper.instance().setState({hotSettings: {data: [[2]], contextMenu: true, readOnly: true}}); 114 | 115 | expect(updateSettingsCount).toEqual(1); 116 | wrapper.detach(); 117 | done(); 118 | }); 119 | 120 | it('should update the Handsontable options, when the component properties get updated (when providing properties individually)', async (done) => { 121 | const wrapper: ReactWrapper<{}, {}, typeof IndividualPropsWrapper> = mount( 122 | , {attachTo: document.body.querySelector('#hotContainer')} 123 | ); 124 | 125 | await sleep(300); 126 | const hotInstance = wrapper.instance().hotTable.hotInstance; 127 | 128 | expect(hotInstance.getSettings().contextMenu).toEqual(void 0); 129 | expect(hotInstance.getSettings().readOnly).toEqual(false); 130 | expect(JSON.stringify(hotInstance.getSettings().data)).toEqual('[[null,null,null,null,null],[null,null,null,null,null],[null,null,null,null,null],[null,null,null,null,null],[null,null,null,null,null]]'); 131 | 132 | await sleep(300); 133 | wrapper.instance().setState({hotSettings: {data: [[2]], contextMenu: true, readOnly: true}}); 134 | 135 | expect(hotInstance.getSettings().contextMenu).toBe(true); 136 | expect(hotInstance.getSettings().readOnly).toBe(true); 137 | expect(JSON.stringify(hotInstance.getSettings().data)).toEqual('[[2]]'); 138 | wrapper.detach(); 139 | 140 | done(); 141 | 142 | }); 143 | 144 | it('should update the Handsontable options, when the component properties get updated (when providing properties as a single settings object)', async (done) => { 145 | const wrapper: ReactWrapper<{}, {}, typeof SingleObjectWrapper> = mount( 146 | , {attachTo: document.body.querySelector('#hotContainer')} 147 | ); 148 | 149 | await sleep(300); 150 | const hotInstance = wrapper.instance().hotTable.hotInstance; 151 | 152 | expect(hotInstance.getSettings().contextMenu).toEqual(void 0); 153 | expect(hotInstance.getSettings().readOnly).toEqual(false); 154 | expect(JSON.stringify(hotInstance.getSettings().data)).toEqual('[[null,null,null,null,null],[null,null,null,null,null],[null,null,null,null,null],[null,null,null,null,null],[null,null,null,null,null]]'); 155 | 156 | await sleep(300); 157 | wrapper.instance().setState({hotSettings: {data: [[2]], contextMenu: true, readOnly: true}}); 158 | 159 | 160 | expect(hotInstance.getSettings().contextMenu).toBe(true); 161 | expect(hotInstance.getSettings().readOnly).toBe(true); 162 | expect(JSON.stringify(hotInstance.getSettings().data)).toEqual('[[2]]'); 163 | wrapper.detach(); 164 | 165 | done(); 166 | }); 167 | }); 168 | 169 | describe('Renderer configuration using React components', () => { 170 | it('should use the renderer component as Handsontable renderer, when it\'s nested under HotTable and assigned the \'hot-renderer\' attribute', async (done) => { 171 | const wrapper: ReactWrapper<{}, {}, typeof HotTable> = mount( 172 | 184 | 185 | , {attachTo: document.body.querySelector('#hotContainer')} 186 | ); 187 | 188 | await sleep(100); 189 | 190 | let hotInstance = wrapper.instance().hotInstance; 191 | 192 | expect(hotInstance.getCell(0, 0).innerHTML).toEqual('
value: A1
'); 193 | 194 | hotInstance.scrollViewportTo(99, 0); 195 | // For some reason it needs another render 196 | hotInstance.render(); 197 | await sleep(100); 198 | 199 | expect(hotInstance.getCell(99, 1).innerHTML).toEqual('
value: B100
'); 200 | 201 | hotInstance.scrollViewportTo(99, 99); 202 | hotInstance.render(); 203 | await sleep(100); 204 | 205 | expect(hotInstance.getCell(99, 99).innerHTML).toEqual('
value: CV100
'); 206 | 207 | wrapper.detach(); 208 | 209 | done(); 210 | }); 211 | }); 212 | 213 | describe('Editor configuration using React components', () => { 214 | it('should use the editor component as Handsontable editor, when it\'s nested under HotTable and assigned the \'hot-editor\' attribute', async (done) => { 215 | const wrapper: ReactWrapper<{}, {}, typeof HotTable> = mount( 216 | 226 | 227 | , {attachTo: document.body.querySelector('#hotContainer')} 228 | ); 229 | 230 | await sleep(100); 231 | 232 | const hotInstance = wrapper.instance().hotInstance; 233 | 234 | expect((document.querySelector('#editorComponentContainer') as any).style.display).toEqual('none'); 235 | 236 | hotInstance.selectCell(0,0); 237 | simulateKeyboardEvent('keydown', 13); 238 | 239 | expect((document.querySelector('#editorComponentContainer') as any).style.display).toEqual('block'); 240 | 241 | expect(hotInstance.getDataAtCell(0,0)).toEqual('A1'); 242 | 243 | simulateMouseEvent(document.querySelector('#editorComponentContainer button'), 'click'); 244 | 245 | expect(hotInstance.getDataAtCell(0,0)).toEqual('new-value'); 246 | 247 | hotInstance.getActiveEditor().close(); 248 | 249 | expect((document.querySelector('#editorComponentContainer') as any).style.display).toEqual('none'); 250 | 251 | done(); 252 | }); 253 | }); 254 | -------------------------------------------------------------------------------- /test/jestsetup.ts: -------------------------------------------------------------------------------- 1 | import { 2 | configure 3 | } from 'enzyme'; 4 | import ReactSixteenAdapter from 'enzyme-adapter-react-16'; 5 | 6 | configure({adapter: new ReactSixteenAdapter()}); 7 | -------------------------------------------------------------------------------- /test/reactContext.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | mount, 4 | ReactWrapper 5 | } from 'enzyme'; 6 | import { 7 | HotTable 8 | } from '../src/hotTable'; 9 | import { 10 | HotColumn 11 | } from '../src/hotColumn'; 12 | import { 13 | mockElementDimensions, 14 | sleep 15 | } from './_helpers'; 16 | import { BaseEditorComponent } from '../src/baseEditorComponent'; 17 | import Handsontable from 'handsontable'; 18 | 19 | beforeEach(() => { 20 | let container = document.createElement('DIV'); 21 | container.id = 'hotContainer'; 22 | document.body.appendChild(container); 23 | }); 24 | 25 | describe('React Context', () => { 26 | it('should be possible to declare a context and use it inside both renderers and editors', async (done) => { 27 | let hotTableInstance = null; 28 | const TestContext = React.createContext('def-test-val'); 29 | 30 | function RendererComponent2() { 31 | return ( 32 | 33 | {(context) => <>{context}} 34 | 35 | ); 36 | } 37 | 38 | class EditorComponent2 extends BaseEditorComponent { 39 | render(): React.ReactElement { 40 | return ( 41 | 42 | {(context) => <>{context}} 43 | 44 | ); 45 | } 46 | } 47 | 48 | class RendererComponent3 extends React.Component { 49 | render() { 50 | return ( 51 | <> 52 | {this.context} 53 | 54 | ) 55 | } 56 | } 57 | RendererComponent3.contextType = TestContext; 58 | 59 | class EditorComponent3 extends React.Component { 60 | render() { 61 | return ( 62 | <> 63 | {this.context} 64 | 65 | ) 66 | } 67 | } 68 | EditorComponent3.contextType = TestContext; 69 | 70 | const wrapper: ReactWrapper<{}, {}, typeof HotTable> = mount( 71 | 72 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | , {attachTo: document.body.querySelector('#hotContainer')} 97 | ); 98 | 99 | await sleep(100); 100 | 101 | const hotInstance = hotTableInstance.hotInstance; 102 | 103 | expect(hotInstance.getCell(0, 0).innerHTML).toEqual('
testContextValue
'); 104 | expect(hotInstance.getCell(1, 0).innerHTML).toEqual('
testContextValue
'); 105 | 106 | expect(document.querySelector('.ec2').innerHTML).toEqual('testContextValue'); 107 | 108 | expect(hotInstance.getCell(0, 1).innerHTML).toEqual('
testContextValue
'); 109 | expect(hotInstance.getCell(1, 1).innerHTML).toEqual('
testContextValue
'); 110 | 111 | expect(document.querySelector('.ec3').innerHTML).toEqual('testContextValue'); 112 | 113 | wrapper.detach(); 114 | done(); 115 | }); 116 | }); 117 | -------------------------------------------------------------------------------- /test/reactHooks.spec.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { 3 | mount, 4 | ReactWrapper 5 | } from 'enzyme'; 6 | import { 7 | HotTable 8 | } from '../src/hotTable'; 9 | import { 10 | mockElementDimensions, 11 | sleep, 12 | simulateMouseEvent 13 | } from './_helpers'; 14 | import Handsontable from 'handsontable'; 15 | 16 | 17 | beforeEach(() => { 18 | let container = document.createElement('DIV'); 19 | container.id = 'hotContainer'; 20 | document.body.appendChild(container); 21 | }); 22 | 23 | describe('Using hooks within HotTable renderers', () => { 24 | it('should be possible to use hook-enabled components as renderers', async (done) => { 25 | function HookEnabledRenderer(props) { 26 | const [count, setCount] = useState(0); 27 | 28 | return ( 29 |
30 |

{props.value}

: {count} 31 | 34 |
35 | ); 36 | } 37 | 38 | const wrapper: ReactWrapper<{}, {}, typeof HotTable> = mount( 39 | 51 | 52 | , {attachTo: document.body.querySelector('#hotContainer')} 53 | ); 54 | 55 | await sleep(100); 56 | 57 | const hotInstance = wrapper.instance().hotInstance; 58 | 59 | expect(hotInstance.getCell(0,0).querySelectorAll('.hook-enabled-renderer-container').length).toEqual(1); 60 | expect(hotInstance.getCell(1,1).querySelectorAll('.hook-enabled-renderer-container').length).toEqual(1); 61 | 62 | simulateMouseEvent(hotInstance.getCell(0,0).querySelector('button'), 'click'); 63 | simulateMouseEvent(hotInstance.getCell(0,0).querySelector('button'), 'click'); 64 | simulateMouseEvent(hotInstance.getCell(0,0).querySelector('button'), 'click'); 65 | 66 | expect(hotInstance.getCell(0,0).querySelector('span').innerHTML).toEqual('3'); 67 | expect(hotInstance.getCell(1,1).querySelector('span').innerHTML).toEqual('0'); 68 | 69 | wrapper.detach(); 70 | done(); 71 | }); 72 | }); 73 | 74 | /* 75 | Editor components cannot be used with React Hooks, as they need to be classes derived from BaseEditorComponent. 76 | */ 77 | -------------------------------------------------------------------------------- /test/reactLazy.spec.tsx: -------------------------------------------------------------------------------- 1 | import React, { Suspense, lazy } from 'react'; 2 | import { 3 | mount, 4 | ReactWrapper 5 | } from 'enzyme'; 6 | import { 7 | HotTable 8 | } from '../src/hotTable'; 9 | import { 10 | mockElementDimensions, 11 | sleep, 12 | } from './_helpers'; 13 | import Handsontable from 'handsontable'; 14 | 15 | beforeEach(() => { 16 | let container = document.createElement('DIV'); 17 | container.id = 'hotContainer'; 18 | document.body.appendChild(container); 19 | }); 20 | 21 | describe('React.lazy', () => { 22 | it('should be possible to lazy-load components and utilize Suspend', async (done) => { 23 | function RendererComponent2(props) { 24 | return ( 25 | <> 26 | lazy value: {props.value} 27 | 28 | ); 29 | } 30 | 31 | let promiseResolve = null; 32 | 33 | function SuspendedRenderer(props) { 34 | const customImportPromise = new Promise(function (resolve, reject) { 35 | promiseResolve = resolve; 36 | } 37 | ) as any; 38 | 39 | const LazierRenderer = lazy(() => customImportPromise); 40 | 41 | return ( 42 | loading-message}> 43 | 44 | 45 | ) 46 | } 47 | 48 | const wrapper: ReactWrapper<{}, {}, any> = mount( 49 | 61 | 62 | , {attachTo: document.body.querySelector('#hotContainer')} 63 | ); 64 | 65 | await sleep(100); 66 | 67 | const hotTableInstance = wrapper.instance(); 68 | const hotInstance = hotTableInstance.hotInstance; 69 | 70 | expect(hotInstance.getCell(0, 0).innerHTML).toEqual('
loading-message
'); 71 | 72 | promiseResolve({ 73 | default: RendererComponent2, 74 | __esModule: true 75 | }); 76 | 77 | await sleep(40); 78 | 79 | expect(hotInstance.getCell(0, 0).innerHTML).toEqual('
lazy value: A1
'); 80 | 81 | wrapper.detach(); 82 | 83 | done(); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /test/reactMemo.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | mount, 4 | ReactWrapper 5 | } from 'enzyme'; 6 | import { 7 | HotTable 8 | } from '../src/hotTable'; 9 | import { 10 | mockElementDimensions, 11 | sleep, 12 | } from './_helpers'; 13 | import Handsontable from 'handsontable'; 14 | 15 | beforeEach(() => { 16 | let container = document.createElement('DIV'); 17 | container.id = 'hotContainer'; 18 | document.body.appendChild(container); 19 | }); 20 | 21 | /** 22 | * Worth noting, that although it's possible to use React.memo on renderer components, it doesn't do much, as currently they're recreated on every 23 | * Handsontable's `render`. 24 | */ 25 | describe('React.memo', () => { 26 | it('should be possible to use React.memo on renderer components.', async (done) => { 27 | function RendererComponent2 (props) { 28 | return ( 29 | <> 30 | value: {props.value} 31 | 32 | ); 33 | } 34 | 35 | const MemoizedRendererComponent2 = React.memo(RendererComponent2); 36 | 37 | const wrapper: ReactWrapper<{}, {}, any> = mount( 38 | 50 | 51 | , {attachTo: document.body.querySelector('#hotContainer')} 52 | ); 53 | 54 | await sleep(100); 55 | 56 | const hotTableInstance = wrapper.instance(); 57 | const hotInstance = hotTableInstance.hotInstance; 58 | 59 | hotInstance.render(); 60 | 61 | await sleep(100); 62 | 63 | expect(hotInstance.getCell(0, 0).innerHTML).toEqual('
value: A1
'); 64 | 65 | wrapper.detach(); 66 | 67 | done(); 68 | }); 69 | 70 | /* 71 | Editors cannot use React.memo, as they're derived from the BaseEditorComponent class, thus not being function components. 72 | */ 73 | }); 74 | -------------------------------------------------------------------------------- /test/reactPureComponent.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | mount, 4 | ReactWrapper 5 | } from 'enzyme'; 6 | import { 7 | HotTable 8 | } from '../src/hotTable'; 9 | import { 10 | mockElementDimensions, 11 | sleep, 12 | } from './_helpers'; 13 | import Handsontable from 'handsontable'; 14 | 15 | beforeEach(() => { 16 | let container = document.createElement('DIV'); 17 | container.id = 'hotContainer'; 18 | document.body.appendChild(container); 19 | }); 20 | 21 | /** 22 | * Worth noting, that although it's possible to use React's Pure Components on renderer components, it doesn't do much, as currently they're recreated on every 23 | * Handsontable's `render`. 24 | */ 25 | describe('React PureComponents', () => { 26 | it('should be possible to declare the renderer as PureComponent', async (done) => { 27 | class RendererComponent2 extends React.PureComponent { 28 | render(): React.ReactElement { 29 | return ( 30 | <> 31 | value: {this.props.value} 32 | 33 | ); 34 | } 35 | } 36 | 37 | const wrapper: ReactWrapper<{}, {}, any> = mount( 38 | 50 | 51 | , {attachTo: document.body.querySelector('#hotContainer')} 52 | ); 53 | 54 | await sleep(100); 55 | 56 | const hotTableInstance = wrapper.instance(); 57 | const hotInstance = hotTableInstance.hotInstance; 58 | 59 | expect(hotInstance.getCell(0, 0).innerHTML).toEqual('
value: A1
'); 60 | 61 | wrapper.detach(); 62 | 63 | done(); 64 | }); 65 | 66 | /* 67 | Editors cannot be declared as PureComponents, as they're derived from the BaseEditorComponent class. 68 | */ 69 | }); 70 | -------------------------------------------------------------------------------- /test/redux.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createStore, combineReducers } from 'redux'; 3 | import { Provider, connect } from 'react-redux'; 4 | import { 5 | mount, 6 | ReactWrapper 7 | } from 'enzyme'; 8 | import { 9 | HotTable 10 | } from '../src/hotTable'; 11 | import { 12 | mockElementDimensions, 13 | sleep, 14 | RendererComponent, 15 | EditorComponent 16 | } from './_helpers'; 17 | import Handsontable from 'handsontable'; 18 | 19 | const initialReduxStoreState = { 20 | hexColor: '#fff' 21 | }; 22 | 23 | const appReducer = (state = initialReduxStoreState, action) => { 24 | switch (action.type) { 25 | case 'updateColor': 26 | const newColor = action.hexColor; 27 | 28 | return Object.assign({}, state, { 29 | hexColor: newColor 30 | }); 31 | default: 32 | return state; 33 | } 34 | }; 35 | const actionReducers = combineReducers({appReducer}); 36 | const reduxStore = createStore(actionReducers); 37 | 38 | beforeEach(() => { 39 | let container = document.createElement('DIV'); 40 | container.id = 'hotContainer'; 41 | document.body.appendChild(container); 42 | 43 | reduxStore.dispatch({ 44 | type: 'updateColor', 45 | hexColor: '#fff' 46 | }); 47 | }); 48 | 49 | describe('Using Redux store within HotTable renderers and editors', () => { 50 | it('should be possible to use redux-enabled components as renderers', async (done) => { 51 | // let reduxStore = mockStore(initialReduxStoreState); 52 | 53 | const ReduxEnabledRenderer = connect(function (state: any) { 54 | return { 55 | bgColor: state.appReducer.hexColor 56 | } 57 | }, () => { 58 | return {}; 59 | }, 60 | null, 61 | { 62 | forwardRef: true 63 | })(RendererComponent); 64 | let rendererInstances = new Map(); 65 | 66 | const wrapper: ReactWrapper<{}, {}, any> = mount( 67 | 68 | 80 | 88 | 89 | , {attachTo: document.body.querySelector('#hotContainer')} 90 | ); 91 | 92 | await sleep(100); 93 | 94 | rendererInstances.forEach((component, key, map) => { 95 | expect(component.props.bgColor).toEqual('#fff'); 96 | }); 97 | 98 | reduxStore.dispatch({ 99 | type: 'updateColor', 100 | hexColor: '#B57267' 101 | }); 102 | 103 | rendererInstances.forEach((component, key, map) => { 104 | expect(component.props.bgColor).toEqual('#B57267'); 105 | }); 106 | 107 | wrapper.detach(); 108 | 109 | done(); 110 | }); 111 | 112 | it('should be possible to use redux-enabled components as editors', async (done) => { 113 | const ReduxEnabledEditor = connect(function (state: any) { 114 | return { 115 | bgColor: state.appReducer.hexColor 116 | } 117 | }, () => { 118 | return {}; 119 | }, 120 | null, 121 | { 122 | forwardRef: true 123 | })(EditorComponent); 124 | let editorInstances = new Map(); 125 | 126 | const wrapper: ReactWrapper<{}, {}, any> = mount( 127 | 128 | 138 | 146 | 147 | , {attachTo: document.body.querySelector('#hotContainer')} 148 | ); 149 | 150 | await sleep(100); 151 | 152 | editorInstances.forEach((value, key, map) => { 153 | expect(value.props.bgColor).toEqual('#fff'); 154 | }); 155 | 156 | reduxStore.dispatch({ 157 | type: 'updateColor', 158 | hexColor: '#B57267' 159 | }); 160 | 161 | editorInstances.forEach((value, key, map) => { 162 | expect(value.props.bgColor).toEqual('#B57267'); 163 | }); 164 | 165 | wrapper.detach(); 166 | 167 | done(); 168 | }); 169 | }); 170 | -------------------------------------------------------------------------------- /test/settingsMapper.spec.tsx: -------------------------------------------------------------------------------- 1 | import { SettingsMapper } from '../src/settingsMapper'; 2 | import { HotTableProps } from '../src/types'; 3 | 4 | describe('Settings mapper unit tests', () => { 5 | describe('getSettings', () => { 6 | it('should return a valid settings object, when provided an object with settings (including the hooks prefixed with "on")', () => { 7 | const settingsMapper = new SettingsMapper(); 8 | 9 | const initial: HotTableProps = { 10 | width: 300, 11 | height: 300, 12 | contextMenu: true, 13 | columns: [ 14 | {label: 'first label'}, 15 | {label: 'second label'} 16 | ], 17 | afterChange: () => { 18 | return 'works!'; 19 | }, 20 | afterRender: () => { 21 | return 'also works!'; 22 | } 23 | }; 24 | const result: {[key: string]: any} = SettingsMapper.getSettings(initial); 25 | 26 | expect(!!result.width && !!result.height && !!result.contextMenu && !!result.columns && !!result.afterChange && !!result.afterRender).toEqual(true); 27 | expect(Object.keys(initial).length).toEqual(Object.keys(result).length); 28 | expect(result.width).toEqual(300); 29 | expect(result.height).toEqual(300); 30 | expect(result.contextMenu).toEqual(true); 31 | expect(JSON.stringify(initial.columns)).toEqual(JSON.stringify(result.columns)); 32 | expect(JSON.stringify(result.afterChange)).toEqual(JSON.stringify(initial.afterChange)); 33 | expect(JSON.stringify(result.afterRender)).toEqual(JSON.stringify(initial.afterRender)); 34 | expect(result.afterChange()).toEqual('works!'); 35 | expect(result.afterRender()).toEqual('also works!'); 36 | }); 37 | 38 | it('should return a valid settings object, when provided an object with settings inside a "settings" property (including the hooks prefixed with "on")', () => { 39 | const settingsMapper = new SettingsMapper(); 40 | const initial = { 41 | settings: { 42 | width: 300, 43 | height: 300, 44 | contextMenu: true, 45 | columns: [ 46 | {label: 'first label'}, 47 | {label: 'second label'} 48 | ], 49 | afterChange: () => { 50 | return 'works!'; 51 | }, 52 | afterRender: () => { 53 | return 'also works!'; 54 | } 55 | } 56 | }; 57 | const result: {[key: string]: any} = SettingsMapper.getSettings(initial); 58 | 59 | expect(!!result.width && !!result.height && !!result.contextMenu && !!result.columns && !!result.afterChange && !!result.afterRender).toEqual(true); 60 | expect(Object.keys(initial.settings).length).toEqual(Object.keys(result).length); 61 | expect(result.width).toEqual(300); 62 | expect(result.height).toEqual(300); 63 | expect(result.contextMenu).toEqual(true); 64 | expect(JSON.stringify(initial.settings.columns)).toEqual(JSON.stringify(result.columns)); 65 | expect(JSON.stringify(result.afterChange)).toEqual(JSON.stringify(initial.settings.afterChange)); 66 | expect(JSON.stringify(result.afterRender)).toEqual(JSON.stringify(initial.settings.afterRender)); 67 | expect(result.afterChange()).toEqual('works!'); 68 | expect(result.afterRender()).toEqual('also works!'); 69 | expect(result.settings).toEqual(void 0); 70 | }); 71 | 72 | it('should return a valid settings object, when provided an object with settings inside a "settings" property as well as individually (including the hooks prefixed with "on")', () => { 73 | const settingsMapper = new SettingsMapper(); 74 | const initial = { 75 | width: 300, 76 | height: 300, 77 | settings: { 78 | contextMenu: true, 79 | columns: [ 80 | {label: 'first label'}, 81 | {label: 'second label'} 82 | ], 83 | afterChange: () => { 84 | return 'works!'; 85 | }, 86 | afterRender: () => { 87 | return 'also works!'; 88 | } 89 | } 90 | }; 91 | const result: {[key: string]: any} = SettingsMapper.getSettings(initial); 92 | 93 | expect(!!result.width && !!result.height && !!result.contextMenu && !!result.columns && !!result.afterChange && !!result.afterRender).toEqual(true); 94 | expect(Object.keys(initial.settings).length + Object.keys(initial).length - 1).toEqual(Object.keys(result).length); 95 | expect(result.width).toEqual(300); 96 | expect(result.height).toEqual(300); 97 | expect(result.contextMenu).toEqual(true); 98 | expect(JSON.stringify(initial.settings.columns)).toEqual(JSON.stringify(result.columns)); 99 | expect(JSON.stringify(result.afterChange)).toEqual(JSON.stringify(initial.settings.afterChange)); 100 | expect(JSON.stringify(result.afterRender)).toEqual(JSON.stringify(initial.settings.afterRender)); 101 | expect(result.afterChange()).toEqual('works!'); 102 | expect(result.afterRender()).toEqual('also works!'); 103 | expect(result.settings).toEqual(void 0); 104 | }); 105 | }); 106 | }); 107 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "jsx": "react", 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "allowSyntheticDefaultImports": true, 8 | "esModuleInterop": true, 9 | "sourceMap": true, 10 | "baseUrl": ".", 11 | "declarationDir": "." 12 | }, 13 | "include": [ 14 | "src/*" 15 | ], 16 | "exclude": [ 17 | "**/node_modules/*" 18 | ] 19 | } 20 | --------------------------------------------------------------------------------