diff --git a/lib/backbone-provider.js b/lib/backbone-provider.js index 9b0236e..303b92f 100644 --- a/lib/backbone-provider.js +++ b/lib/backbone-provider.js @@ -1,15 +1,28 @@ -const { Component, Children } = require('react'); +const React = require('react'); +const {Children, Component} = React; const PropTypes = require('prop-types'); +const ConnectBackboneToReactContext = require('./context.js'); // eslint-disable-line no-unused-vars class BackboneProvider extends Component { - getChildContext() { - return { - models: this.props.models, + constructor(props) { + super(props); + + this.state = { + models: props.models, }; } + componentDidUpdate(prevProps) { + if (this.props.models === prevProps.models) return; + this.setState({ models: this.props.models }); + } + render() { - return Children.only(this.props.children); + return ( + + {Children.only(this.props.children)} + + ); } } @@ -17,9 +30,6 @@ BackboneProvider.propTypes = { models: PropTypes.object, children: PropTypes.element.isRequired, }; -BackboneProvider.childContextTypes = { - models: PropTypes.object, -}; BackboneProvider.displayName = 'BackboneProvider'; module.exports = BackboneProvider; diff --git a/lib/connect-backbone-to-react.js b/lib/connect-backbone-to-react.js index 7f524fe..bdda2d4 100644 --- a/lib/connect-backbone-to-react.js +++ b/lib/connect-backbone-to-react.js @@ -1,7 +1,9 @@ const hoistStatics = require('hoist-non-react-statics'); -const { Component, createElement } = require('react'); +const React = require('react'); +const {Component} = React; const PropTypes = require('prop-types'); const debounceFn = require('lodash.debounce'); +const ConnectBackboneToReactContext = require('./context.js'); // eslint-disable-line no-unused-vars function getDisplayName(name) { return `connectBackboneToReact(${name})`; @@ -66,39 +68,26 @@ module.exports = function connectBackboneToReact( const displayName = getDisplayName(wrappedComponentName); class ConnectBackboneToReact extends Component { - constructor(props, context) { - super(props, context); + constructor(props) { + super(props); - this.setModels(props, context); - - this.state = mapModelsToProps(this.models, this.props); + this.models = {}; + this.state = {}; this.createNewProps = this.createNewProps.bind(this); + this.renderChild = this.renderChild.bind(this); if (debounce) { const debounceWait = typeof debounce === 'number' ? debounce : 0; this.createNewProps = debounceFn(this.createNewProps, debounceWait); } - - this.createEventListeners(); } - setModels(props, context) { - const models = Object.assign({}, context.models, props.models); + setModels(models = {}) { validateModelTypes(models); this.models = models; } - createEventListeners() { - Object.keys(this.models).forEach(mapKey => { - const model = this.models[mapKey]; - // Do not attempt to create event listeners on an undefined model. - if (!model) return; - - this.createEventListener(mapKey, model); - }); - } - createEventListener(modelName, model) { getEventNames(modelName).forEach(name => { model.on(name, this.createNewProps, this); @@ -110,27 +99,34 @@ module.exports = function connectBackboneToReact( // The only case where this flag is encountered is when this component // is unmounted within an event handler but the 'all' event is still triggered. // It is covered in a test case. - if (this.hasBeenUnmounted) { - return; - } - + if (this.hasBeenUnmounted) return; this.setState(mapModelsToProps(this.models, this.props)); } - componentWillReceiveProps(nextProps, nextContext) { - this.setModels(nextProps, nextContext); - this.createNewProps(); + renderChild({ models } = {}) { + const newModels = Object.assign({}, this.models, models, this.props.models); // Bind event listeners for each model that changed. - Object.keys(this.models).forEach(mapKey => { - const model = this.models[mapKey]; - if ((this.props.models && this.props.models[mapKey] === this.models[mapKey]) || - (this.context.models && this.context.models[mapKey] === this.models[mapKey])) { + Object.keys(newModels).forEach(mapKey => { + const model = newModels[mapKey]; + if (this.models && this.models[mapKey] === newModels[mapKey]) { return; // Did not change. } - this.createEventListener(mapKey, model); + if (model) this.createEventListener(mapKey, model); + this.models[mapKey] = model; }); + validateModelTypes(this.models); + + const wrappedProps = Object.assign( + {}, + mapModelsToProps(this.models, this.props), + this.props + ); + + // Don't pass through models prop. + delete wrappedProps.models; + return React.createElement(WrappedComponent, wrappedProps); } componentWillUnmount() { @@ -152,16 +148,11 @@ module.exports = function connectBackboneToReact( } render() { - const wrappedProps = Object.assign( - {}, - this.state, - this.props + return ( + + {this.renderChild} + ); - - // Don't pass through models prop. - wrappedProps.models = undefined; - - return createElement(WrappedComponent, wrappedProps); } } diff --git a/lib/context.js b/lib/context.js new file mode 100644 index 0000000..2a6f24f --- /dev/null +++ b/lib/context.js @@ -0,0 +1,2 @@ +const React = require('react'); +module.exports = React.createContext('connectBackboneToReact'); diff --git a/package.json b/package.json index c91ed0a..df11787 100644 --- a/package.json +++ b/package.json @@ -27,10 +27,10 @@ "dependencies": { "hoist-non-react-statics": "^1.2.0", "lodash.debounce": "^4.0.8", - "prop-types": "^15.5.8" + "prop-types": "^15.7.2" }, "peerDependencies": { - "react": "^0.14.0 || ^15.0.0-0 || ^16.0.0-0" + "react": "^16.3.0-0" }, "devDependencies": { "babel-cli": "^6.22.2", @@ -38,16 +38,17 @@ "babel-preset-react": "^6.22.0", "babel-register": "^6.22.0", "backbone": "^1.3.3", - "enzyme": "^2.7.1", - "eslint-config-mongodb-js": "^2.2.0", + "enzyme": "^3.10.0", + "enzyme-adapter-react-16": "^1.15.1", + "eslint-config-mongodb-js": "^2.3.0", "jsdom": "^9.11.0", "mocha": "^3.2.0", "mongodb-js-fmt": "^0.0.3", "mongodb-js-precommit": "^0.2.8", "pre-commit": "^1.1.2", - "react": "^15.4.2", - "react-addons-test-utils": "^15.4.2", - "react-dom": "^15.4.2", + "react": "^16.12.0", + "react-dom": "^16.12.0", + "react-test-renderer": "^16.3.2", "sinon": "^1.17.7", "standard-version": "^4.0.0" }, diff --git a/test/backbone-provider.test.js b/test/backbone-provider.test.js index 6ab5381..17cb173 100644 --- a/test/backbone-provider.test.js +++ b/test/backbone-provider.test.js @@ -81,7 +81,7 @@ describe('BackboneProvider', function() { }); afterEach(function() { - wrapper.unmount(); + if (wrapper.exists()) wrapper.unmount(); }); it('passes mapped models and collections as properties to wrapped component', function() { @@ -139,11 +139,11 @@ describe('BackboneProvider', function() { const modelsFromContext = wrapper .find('.name') - .findWhere((n) => n.text() === userModel.get('name')) + .findWhere((n) => !n.type() && n.text() === userModel.get('name')) .length; const modelsFromParent = wrapper .find('.color') - .findWhere((n) => n.text() === settingsModel.get('color')) + .findWhere((n) => !n.type() && n.text() === settingsModel.get('color')) .length; // Check that we've rendered data from models passed by both context and the parent component. @@ -186,12 +186,12 @@ describe('BackboneProvider', function() { const modelsFromContext = wrapper .find('.name') - .findWhere((n) => n.text() === userModel.get('name')) + .findWhere((n) => !n.type() && n.text() === userModel.get('name')) .length; const modelsFromParent = wrapper .find('.child-wrapper') .find('.name') - .findWhere((n) => n.text() === otherUserModel.get('name')) + .findWhere((n) => !n.type() && n.text() === otherUserModel.get('name')) .length; // Check that we've given priority to models passed from the parent component. diff --git a/test/connect-backbone-to-react.test.js b/test/connect-backbone-to-react.test.js index f2b829a..b4eeab3 100644 --- a/test/connect-backbone-to-react.test.js +++ b/test/connect-backbone-to-react.test.js @@ -96,7 +96,7 @@ describe('connectBackboneToReact', function() { }); afterEach(function() { - wrapper.unmount(); + if (wrapper.exists()) wrapper.unmount(); }); it('passes mapped models and collections as properties to wrapped component', function() { @@ -115,9 +115,10 @@ describe('connectBackboneToReact', function() { it('updates properties when props function changes models and collections ', function() { const newName = 'The Loud One'; - stub.props().changeName(newName); + stub.prop('changeName')(newName); + wrapper.update(); assert.equal(userModel.get('name'), newName); - assert.equal(stub.props().name, newName); + assert.equal(wrapper.find(TestComponent).prop('name'), newName); assert.equal(setStateSpy.callCount, 4); }); @@ -125,9 +126,10 @@ describe('connectBackboneToReact', function() { it('updates properties when model and collections change', function() { const newName = 'Banana'; userModel.set('name', newName); + wrapper.update(); assert.equal(wrapper.find('.name').text(), 'Banana'); assert.equal(userModel.get('name'), newName); - assert.equal(stub.props().name, newName); + assert.equal(wrapper.find(TestComponent).prop('name'), newName); assert.equal(setStateSpy.callCount, 4); }); @@ -169,7 +171,7 @@ describe('connectBackboneToReact', function() { }); afterEach(function() { - wrapper.unmount(); + if (wrapper.exists()) wrapper.unmount(); }); it('updates properties when model and collections change', function(done) { @@ -177,9 +179,10 @@ describe('connectBackboneToReact', function() { userModel.set('name', newName); setTimeout(() => { + wrapper.update(); assert.equal(wrapper.find('.name').text(), 'Banana'); assert.equal(userModel.get('name'), newName); - assert.equal(stub.props().name, newName); + assert.equal(wrapper.find(TestComponent).prop('name'), newName); assert.equal(setStateSpy.callCount, 1); @@ -202,7 +205,7 @@ describe('connectBackboneToReact', function() { describe('when mounted with an undefined model', function() { afterEach(function() { - wrapper.unmount(); + if (wrapper.exists()) wrapper.unmount(); }); it('the default should mount and unmount the component successfully', function() { @@ -232,7 +235,7 @@ describe('connectBackboneToReact', function() { }); afterEach(function() { - wrapper.unmount(); + if (wrapper.exists()) wrapper.unmount(); }); it('sets one event handler on the userModel', function() { @@ -247,9 +250,10 @@ describe('connectBackboneToReact', function() { it('updates properties when model\'s name changes', function() { const newName = 'Banana'; userModel.set('name', newName); + wrapper.update(); assert.equal(userModel.get('name'), newName); - assert.equal(stub.props().name, newName); + assert.equal(wrapper.find(TestComponent).prop('name'), newName); }); it('rerenders when tracked property changes', function() { @@ -291,7 +295,7 @@ describe('connectBackboneToReact', function() { }); afterEach(function() { - wrapper.unmount(); + if (wrapper.exists()) wrapper.unmount(); }); it('sets 0 event handlers on the userModel', function() { @@ -318,7 +322,7 @@ describe('connectBackboneToReact', function() { }); afterEach(function() { - wrapper.unmount(); + if (wrapper.exists()) wrapper.unmount(); }); it('passes connectedProps through', function() { @@ -342,7 +346,7 @@ describe('connectBackboneToReact', function() { }); afterEach(function() { - wrapper.unmount(); + if (wrapper.exists()) wrapper.unmount(); }); it('uses default mapModelsToProps function', function() { @@ -362,8 +366,9 @@ describe('connectBackboneToReact', function() { it('re-renders props when model changes', function() { const newName = 'Banana'; userModel.set('name', newName); + wrapper.update(); - assert.equal(stub.props().user.name, 'Banana'); + assert.equal(wrapper.find(TestComponent).prop('user').name, 'Banana'); assert.equal(setStateSpy.callCount, 4); }); @@ -388,7 +393,7 @@ describe('connectBackboneToReact', function() { }); afterEach(function() { - wrapper.unmount(); + if (wrapper.exists()) wrapper.unmount(); }); it('uses default mapModelsToProps function', function() { @@ -409,8 +414,9 @@ describe('connectBackboneToReact', function() { it('re-renders props when model changes', function() { const newName = 'Banana'; userModel.set('name', newName); + wrapper.update(); - assert.equal(stub.props().user.name, 'Banana'); + assert.equal(wrapper.find(TestComponent).prop('user').name, 'Banana'); assert.equal(setStateSpy.callCount, 1); }); @@ -523,7 +529,7 @@ describe('connectBackboneToReact', function() { }); afterEach(function() { - wrapper.unmount(); + if (wrapper.exists()) wrapper.unmount(); }); it('retrieves the correct model based on props', function() { @@ -544,15 +550,15 @@ describe('connectBackboneToReact', function() { }); describe('when passed props change', function() { - let setStateSpy; let newName; let newAge; let newUserModel; beforeEach(function() { + // Disable no-unused-vars on the next line because the current version doesn't + // detect that is a usage. + // eslint-disable-next-line const ConnectedTest = connectBackboneToReact(mapModelsToProps)(TestComponent); - setStateSpy = sandbox.spy(ConnectedTest.prototype, 'setState'); - wrapper = mount(); stub = wrapper.find(TestComponent); @@ -573,11 +579,7 @@ describe('connectBackboneToReact', function() { }); afterEach(function() { - wrapper.unmount(); - }); - - it('calls setState once', function() { - assert.equal(setStateSpy.calledOnce, true); + if (wrapper.exists()) wrapper.unmount(); }); it('renders the new props', function() { diff --git a/test/test-setup.js b/test/test-setup.js index d63b469..1f9180b 100644 --- a/test/test-setup.js +++ b/test/test-setup.js @@ -1,4 +1,7 @@ require('babel-register'); +const Enzyme = require('enzyme'); +const Adapter = require('enzyme-adapter-react-16'); +Enzyme.configure({ adapter: new Adapter() }); const jsdom = require('jsdom').jsdom; global.document = jsdom('');