diff --git a/.babelrc b/.babelrc index 0b2bb39..23744b7 100644 --- a/.babelrc +++ b/.babelrc @@ -1,3 +1,3 @@ { - "presets": ["react", "es2015"], + "presets": ["react", "env"] } diff --git a/.eslintrc b/.eslintrc index 67b82c5..e7bb059 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,42 +1,298 @@ { + "extends": [ + "eslint:recommended", + "prettier" + ], + "parser": "babel-eslint", "parserOptions": { "ecmaVersion": 6, "sourceType": "module", "ecmaFeatures": { + "arrowFunctions": true, + "blockBindings": true, + "classes": true, + "defaultParams": true, + "destructuring": true, + "forOf": true, + "generators": true, + "modules": true, + "templateStrings": true, "jsx": true } }, - "rules": { - "semi": 2, - "no-unused-vars": 2, - "react/display-name": 2, - "react/jsx-key": 2, - "react/jsx-no-comment-textnodes": 2, - "react/jsx-no-duplicate-props": 2, - "react/jsx-no-target-blank": 2, - "react/jsx-no-undef": 2, - "react/jsx-uses-react": 2, - "react/jsx-uses-vars": 2, - "react/no-children-prop": 2, - "react/no-danger-with-children": 2, - "react/no-deprecated": 2, - "react/no-direct-mutation-state": 2, - "react/no-find-dom-node": 2, - "react/no-is-mounted": 2, - "react/no-render-return-value": 2, - "react/no-string-refs": 2, - "react/no-unescaped-entities": 2, - "react/no-unknown-property": 2, - "react/prop-types": 2, - "react/react-in-jsx-scope": 2, - "react/require-render-return": 2, + "env": { + "browser": true, + "es6": true, + "jasmine": true, + "jest": true, + "node": true + }, + "globals": { + "jest": true }, "plugins": [ - "react" + "react", + "import" ], - "settings": { - "react": { - "version": "^15.6.1" + "overrides": [ + { + "files": [ + "**/*.percy.{js,jsx}" + ], + "env": { + "react-percy/globals": true + } } + ], + "rules": { + "accessor-pairs": [ + "error" + ], + "block-scoped-var": [ + "error" + ], + "consistent-return": [ + "error" + ], + "curly": [ + "error", + "all" + ], + "default-case": [ + "error" + ], + "dot-location": [ + "off" + ], + "dot-notation": [ + "error" + ], + "eqeqeq": [ + "error" + ], + "guard-for-in": [ + "off" + ], + "import/named": [ + "off" + ], + "import/no-duplicates": [ + "error" + ], + "import/no-named-as-default": [ + "error" + ], + "new-cap": [ + "error" + ], + "no-alert": [ + 1 + ], + "no-caller": [ + "error" + ], + "no-case-declarations": [ + "error" + ], + "no-console": [ + "error" + ], + "no-div-regex": [ + "error" + ], + "no-dupe-keys": [ + "error" + ], + "no-else-return": [ + "error" + ], + "no-empty-pattern": [ + "error" + ], + "no-eq-null": [ + "error" + ], + "no-eval": [ + "error" + ], + "no-extend-native": [ + "error" + ], + "no-extra-bind": [ + "error" + ], + "no-extra-boolean-cast": [ + "error" + ], + "no-inline-comments": [ + "error" + ], + "no-implicit-coercion": [ + "error" + ], + "no-implied-eval": [ + "error" + ], + "no-inner-declarations": [ + "off" + ], + "no-invalid-this": [ + "error" + ], + "no-iterator": [ + "error" + ], + "no-labels": [ + "error" + ], + "no-lone-blocks": [ + "error" + ], + "no-loop-func": [ + "error" + ], + "no-multi-str": [ + "error" + ], + "no-native-reassign": [ + "error" + ], + "no-new": [ + "error" + ], + "no-new-func": [ + "error" + ], + "no-new-wrappers": [ + "error" + ], + "no-param-reassign": [ + "error" + ], + "no-process-env": [ + "warn" + ], + "no-proto": [ + "error" + ], + "no-redeclare": [ + "error" + ], + "no-return-assign": [ + "error" + ], + "no-script-url": [ + "error" + ], + "no-self-compare": [ + "error" + ], + "no-sequences": [ + "error" + ], + "no-shadow": [ + "off" + ], + "no-throw-literal": [ + "error" + ], + "no-undefined": [ + "error" + ], + "no-unused-expressions": [ + "error" + ], + "no-use-before-define": [ + "error", + "nofunc" + ], + "no-useless-call": [ + "error" + ], + "no-useless-concat": [ + "error" + ], + "no-with": [ + "error" + ], + "prefer-const": [ + "error" + ], + "radix": [ + "error" + ], + "react/jsx-no-duplicate-props": [ + "error" + ], + "react/jsx-no-undef": [ + "error" + ], + "react/jsx-uses-react": [ + "error" + ], + "react/jsx-uses-vars": [ + "error" + ], + "react/no-did-update-set-state": [ + "error" + ], + "react/no-direct-mutation-state": [ + "error" + ], + "react/no-is-mounted": [ + "error" + ], + "react/no-unknown-property": [ + "error" + ], + "react/prefer-es6-class": [ + "error", + "always" + ], + "react/prop-types": "error", + "valid-jsdoc": [ + "error" + ], + "yoda": [ + "error" + ], + "spaced-comment": [ + "error", + "always", + { + "block": { + "exceptions": [ + "*" + ] + } + } + ], + "no-unused-vars": [ + "error", + { + "args": "after-used", + "argsIgnorePattern": "^_", + "caughtErrorsIgnorePattern": "^e$" + } + ], + "no-magic-numbers": [ + "error", + { + "ignoreArrayIndexes": true, + "ignore": [ + -1, + 0, + 1, + 2, + 3, + 100, + 10, + 0.5 + ] + } + ], + "no-underscore-dangle": [ + "off" + ] } } diff --git a/.prettierrc b/.prettierrc index 9361dea..73d0133 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,5 +1,6 @@ { - "singleQuote": true, - "bracketSpacing": false, - "trailingComma": "es5" + "singleQuote": true, + "bracketSpacing": false, + "trailingComma": "es5", + "printWidth": 100 } diff --git a/README.md b/README.md index 4984770..23f2836 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # react-plotly.js -![plotly-react-logo](https://static1.squarespace.com/static/5a5adfdea9db09d594a841f3/t/5a5af2c5e2c48307ed4a21b6/1515975253370/) +![plotly-react-logo](https://images.plot.ly/plotly-documentation/thumbnail/react.png) > A [plotly.js](https://github.com/plotly/plotly.js) React component from > [Plotly](https://plot.ly/). The basis of Plotly's diff --git a/package.json b/package.json index 90ff178..7fd82ff 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,8 @@ "url": "https://github.com/plotly/react-plotly.js/issues" }, "scripts": { - "make:lib": "mkdirp lib && babel src --out-dir=lib --ignore __tests__/*.js,__mocks__/*.js --presets=es2015,react --source-maps --plugins babel-plugin-add-module-exports && mv lib/* ./ && rmdir lib", - "make:dist": "mkdirp dist && browserify src/factory.js -o ./dist/create-plotly-component.js -t [ babelify --presets [ es2015 react ] --plugins add-module-exports ] -t browserify-global-shim --standalone createPlotlyComponent && uglifyjs ./dist/create-plotly-component.js --compress --mangle --output ./dist/create-plotly-component.min.js --source-map filename=dist/create-plotly-component.min.js.map", + "make:lib": "mkdirp lib && babel src --out-dir=lib --ignore __tests__/*.js,__mocks__/*.js --presets=env,react --source-maps --plugins babel-plugin-add-module-exports && mv lib/* ./ && rmdir lib", + "make:dist": "mkdirp dist && browserify src/factory.js -o ./dist/create-plotly-component.js -t [ babelify --presets [ env react ] --plugins add-module-exports ] -t browserify-global-shim --standalone createPlotlyComponent && uglifyjs ./dist/create-plotly-component.js --compress --mangle --output ./dist/create-plotly-component.min.js --source-map filename=dist/create-plotly-component.min.js.map", "clean": "rimraf lib dist react-plotly.js react-plotly.js.map factory.js factory.js.map", "prepublishOnly": "npm run clean && npm run make:lib && npm run make:dist", "lint": "prettier --trailing-comma es5 --write \"src/**/*.js\" && eslint src", @@ -33,9 +33,10 @@ ], "devDependencies": { "babel-cli": "^6.24.1", + "babel-eslint": "^10.0.1", "babel-plugin-add-module-exports": "^0.2.1", "babel-plugin-transform-class-properties": "^6.24.1", - "babel-preset-es2015": "^6.24.1", + "babel-preset-env": "^1.7.0", "babel-preset-react": "^6.24.1", "babelify": "^7.3.0", "brfs": "^1.4.3", @@ -45,6 +46,8 @@ "dependency-check": "^2.9.1", "enzyme": "^2.9.1", "eslint": "^4.8.0", + "eslint-config-prettier": "^4.0.0", + "eslint-plugin-import": "^2.16.0", "eslint-plugin-react": "^7.4.0", "event-emitter": "^0.3.5", "jest": "^20.0.4", @@ -63,7 +66,7 @@ }, "peerDependencies": { "plotly.js": ">1.34.0", - "react": ">12.0.0" + "react": ">0.13.0" }, "browserify-global-shim": { "react": "React" diff --git a/src/__mocks__/plotly.js b/src/__mocks__/plotly.js index 8cc7d89..b6f1f38 100644 --- a/src/__mocks__/plotly.js +++ b/src/__mocks__/plotly.js @@ -12,7 +12,7 @@ export default { }), newPlot: jest.fn(gd => { state.gd = gd; - EventEmitter(state.gd); + EventEmitter(state.gd); // eslint-disable-line new-cap setTimeout(() => { state.gd.emit('plotly_afterplot'); @@ -20,7 +20,7 @@ export default { }), react: jest.fn(gd => { state.gd = gd; - EventEmitter(state.gd); + EventEmitter(state.gd); // eslint-disable-line new-cap setTimeout(() => { state.gd.emit('plotly_afterplot'); @@ -40,6 +40,6 @@ export default { }), update: jest.fn(), purge: jest.fn(() => { - state.gd = nll; + state.gd = null; }), }; diff --git a/src/__tests__/react-plotly.test.js b/src/__tests__/react-plotly.test.js index 4db358c..e6d6e6c 100644 --- a/src/__tests__/react-plotly.test.js +++ b/src/__tests__/react-plotly.test.js @@ -9,11 +9,7 @@ describe('', () => { function createPlot(props) { return new Promise((resolve, reject) => { const plot = mount( - resolve(plot)} - onError={reject} - /> + resolve(plot)} onError={reject} /> ); }); } @@ -24,9 +20,9 @@ describe('', () => { Object.assign( defaultArgs || { data: [], - config: undefined, - layout: undefined, - frames: undefined, + config: undefined, // eslint-disable-line no-undefined + layout: undefined, // eslint-disable-line no-undefined + frames: undefined, // eslint-disable-line no-undefined }, props || {} ) @@ -139,19 +135,11 @@ describe('', () => { }) .then(plot => { // Update with and without revision bumps: + /* eslint-disable no-magic-numbers */ setTimeout(() => plot.setProps({layout: {title: 'test test'}}), 10); - setTimeout( - () => plot.setProps({revision: 1, layout: {title: 'test test'}}), - 20 - ); - setTimeout( - () => plot.setProps({revision: 1, layout: {title: 'test test'}}), - 30 - ); - setTimeout( - () => plot.setProps({revision: 2, layout: {title: 'test test'}}), - 40 - ); + setTimeout(() => plot.setProps({revision: 1, layout: {title: 'test test'}}), 20); + setTimeout(() => plot.setProps({revision: 1, layout: {title: 'test test'}}), 30); + setTimeout(() => plot.setProps({revision: 2, layout: {title: 'test test'}}), 40); }) .catch(err => done.fail(err)); }); diff --git a/src/factory.js b/src/factory.js index c207fcb..23a0cb3 100644 --- a/src/factory.js +++ b/src/factory.js @@ -64,8 +64,20 @@ export default function plotComponentFactory(Plotly) { } componentDidMount() { + this.unmounting = false; + this.p = this.p .then(() => { + if (!this.el) { + let error; + if (this.unmounting) { + error = new Error('Component is unmounting'); + error.reason = 'unmounting'; + } else { + error = new Error('Missing element reference'); + } + throw error; + } return Plotly.newPlot(this.el, { data: this.props.data, layout: this.props.layout, @@ -78,28 +90,28 @@ export default function plotComponentFactory(Plotly) { .then(this.attachUpdateEvents) .then(() => this.figureCallback(this.props.onInitialized)) .catch(err => { - console.error('Error while plotting:', err); - return this.props.onError && this.props.onError(err); + if (err.reason === 'unmounting') { + return; + } + console.error('Error while plotting:', err); // eslint-disable-line no-console + if (this.props.onError) { + this.props.onError(err); + } }); } componentWillUpdate(nextProps) { - if ( - nextProps.revision !== void 0 && - nextProps.revision === this.props.revision - ) { + this.unmounting = false; + + if (nextProps.revision !== void 0 && nextProps.revision === this.props.revision) { // if revision is set and unchanged, do nothing return; } const numPrevFrames = - this.props.frames && this.props.frames.length - ? this.props.frames.length - : 0; + this.props.frames && this.props.frames.length ? this.props.frames.length : 0; const numNextFrames = - nextProps.frames && nextProps.frames.length - ? nextProps.frames.length - : 0; + nextProps.frames && nextProps.frames.length ? nextProps.frames.length : 0; if ( nextProps.layout === this.props.layout && nextProps.data === this.props.data && @@ -113,6 +125,16 @@ export default function plotComponentFactory(Plotly) { this.p = this.p .then(() => { + if (!this.el) { + let error; + if (this.unmounting) { + error = new Error('Component is unmounting'); + error.reason = 'unmounting'; + } else { + error = new Error('Missing element reference'); + } + throw error; + } return Plotly.react(this.el, { data: nextProps.data, layout: nextProps.layout, @@ -124,12 +146,19 @@ export default function plotComponentFactory(Plotly) { .then(() => this.syncWindowResize(nextProps)) .then(() => this.figureCallback(nextProps.onUpdate)) .catch(err => { - console.error('Error while plotting:', err); - this.props.onError && this.props.onError(err); + if (err.reason === 'unmounting') { + return; + } + console.error('Error while plotting:', err); // eslint-disable-line no-console + if (this.props.onError) { + this.props.onError(err); + } }); } componentWillUnmount() { + this.unmounting = true; + this.figureCallback(this.props.onPurge); if (this.resizeHandler && isBrowser) { @@ -143,7 +172,9 @@ export default function plotComponentFactory(Plotly) { } attachUpdateEvents() { - if (!this.el || !this.el.removeListener) return; + if (!this.el || !this.el.removeListener) { + return; + } for (let i = 0; i < updateEvents.length; i++) { this.el.on(updateEvents[i], this.handleUpdate); @@ -151,7 +182,9 @@ export default function plotComponentFactory(Plotly) { } removeUpdateEvents() { - if (!this.el || !this.el.removeListener) return; + if (!this.el || !this.el.removeListener) { + return; + } for (let i = 0; i < updateEvents.length; i++) { this.el.removeListener(updateEvents[i], this.handleUpdate); @@ -165,17 +198,17 @@ export default function plotComponentFactory(Plotly) { figureCallback(callback) { if (typeof callback === 'function') { const {data, layout} = this.el; - const frames = this.el._transitionData - ? this.el._transitionData._frames - : null; - const figure = {data, layout, frames}; // for extra clarity! + const frames = this.el._transitionData ? this.el._transitionData._frames : null; + const figure = {data, layout, frames}; callback(figure, this.el); } } syncWindowResize(propsIn, invoke) { const props = propsIn || this.props; - if (!isBrowser) return; + if (!isBrowser) { + return; + } if (props.useResizeHandler && !this.resizeHandler) { this.resizeHandler = () => { @@ -207,20 +240,14 @@ export default function plotComponentFactory(Plotly) { for (let i = 0; i < eventNames.length; i++) { const eventName = eventNames[i]; const prop = props['on' + eventName]; - const hasHandler = !!this.handlers[eventName]; + const hasHandler = Boolean(this.handlers[eventName]); if (prop && !hasHandler) { this.handlers[eventName] = prop; - this.el.on( - 'plotly_' + eventName.toLowerCase(), - this.handlers[eventName] - ); + this.el.on('plotly_' + eventName.toLowerCase(), this.handlers[eventName]); } else if (!prop && hasHandler) { // Needs to be removed: - this.el.removeListener( - 'plotly_' + eventName.toLowerCase(), - this.handlers[eventName] - ); + this.el.removeListener('plotly_' + eventName.toLowerCase(), this.handlers[eventName]); delete this.handlers[eventName]; } }