├── index.js ├── test ├── mocha.opts ├── test-setup.js ├── backbone-provider.test.js └── connect-backbone-to-react.test.js ├── .npmignore ├── .gitignore ├── lib ├── context.js ├── index.js ├── backbone-provider.js └── connect-backbone-to-react.js ├── .travis.yml ├── .babelrc ├── .eslintrc ├── package.json ├── .jsfmtrc ├── CHANGELOG.md ├── README.md └── LICENSE /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dist'); 2 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | -R spec -r ./test/test-setup.js -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .jshintrc 2 | .jsfmtrc 3 | .travis.yml 4 | test/ 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | npm-debug.log 4 | .env 5 | dist/ -------------------------------------------------------------------------------- /lib/context.js: -------------------------------------------------------------------------------- 1 | const { createContext } = require('react'); 2 | 3 | module.exports = createContext(); 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - 6 5 | script: npm run-script ci 6 | cache: 7 | directories: 8 | - node_modules 9 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "react", 4 | ["env", { 5 | "targets": { 6 | "browsers": ["last 2 versions", "safari >= 7"], 7 | "node": 6 8 | } 9 | }] 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | const connectBackboneToReact = require('./connect-backbone-to-react'); 2 | const BackboneProvider = require('./backbone-provider'); 3 | 4 | exports.connectBackboneToReact = connectBackboneToReact; 5 | exports.BackboneProvider = BackboneProvider; 6 | -------------------------------------------------------------------------------- /lib/backbone-provider.js: -------------------------------------------------------------------------------- 1 | const { Children, createElement } = require('react'); 2 | const PropTypes = require('prop-types'); 3 | const { Provider } = require('./context'); 4 | 5 | function BackboneProvider(props) { 6 | return createElement(Provider, { value: props.models }, Children.only(props.children)); 7 | } 8 | 9 | BackboneProvider.propTypes = { 10 | models: PropTypes.object, 11 | children: PropTypes.element.isRequired, 12 | }; 13 | BackboneProvider.displayName = 'BackboneProvider'; 14 | 15 | module.exports = BackboneProvider; 16 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["mongodb-js/node"], 3 | "env": { 4 | "browser": true, 5 | "node": true, 6 | "mocha": true 7 | }, 8 | "parserOptions": { 9 | "ecmaVersion": 6, 10 | "sourceType": "module", 11 | "ecmaFeatures": { 12 | "jsx": true 13 | } 14 | }, 15 | "rules": { 16 | "comma-dangle": ["error", { 17 | "arrays": "always-multiline", 18 | "objects": "always-multiline", 19 | "imports": "always-multiline", 20 | "exports": "always-multiline", 21 | }] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/test-setup.js: -------------------------------------------------------------------------------- 1 | require('babel-register'); 2 | const Enzyme = require('enzyme'); 3 | const Adapter = require('enzyme-adapter-react-16'); 4 | Enzyme.configure({ adapter: new Adapter() }); 5 | const jsdom = require('jsdom').jsdom; 6 | 7 | global.document = jsdom(''); 8 | global.window = document.defaultView; 9 | Object.keys(document.defaultView).forEach((property) => { 10 | if (typeof global[property] === 'undefined') { 11 | global[property] = document.defaultView[property]; 12 | } 13 | }); 14 | 15 | global.navigator = { 16 | userAgent: 'node.js', 17 | }; 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "connect-backbone-to-react", 3 | "description": "Connect Backbone Models and Collections to React.", 4 | "version": "3.1.0", 5 | "scripts": { 6 | "fmt": "mongodb-js-fmt", 7 | "check": "mongodb-js-precommit", 8 | "test": "mocha", 9 | "ci": "npm run check && npm test", 10 | "compile": "babel lib/ --out-dir dist/", 11 | "watch": "babel --watch lib/ --out-dir dist/", 12 | "prepublish": "npm run compile", 13 | "release": "standard-version" 14 | }, 15 | "homepage": "http://github.com/mongodb-js/connect-backbone-to-react", 16 | "repository": { 17 | "type": "git", 18 | "url": "git://github.com/mongodb-js/connect-backbone-to-react.git" 19 | }, 20 | "bugs": { 21 | "url": "git://github.com/mongodb-js/connect-backbone-to-react.git/issues" 22 | }, 23 | "keywords": [ 24 | "backbone", 25 | "react" 26 | ], 27 | "dependencies": { 28 | "hoist-non-react-statics": "^1.2.0", 29 | "lodash.debounce": "^4.0.8", 30 | "prop-types": "^15.7.2" 31 | }, 32 | "peerDependencies": { 33 | "react": "^16.6.0-0 || ^17.0.0 || ^18.0.0" 34 | }, 35 | "devDependencies": { 36 | "babel-cli": "^6.22.2", 37 | "babel-preset-env": "^1.1.8", 38 | "babel-preset-react": "^6.22.0", 39 | "babel-register": "^6.22.0", 40 | "backbone": "^1.3.3", 41 | "enzyme": "^3.10.0", 42 | "enzyme-adapter-react-16": "^1.15.1", 43 | "eslint-config-mongodb-js": "^2.3.0", 44 | "jsdom": "^9.11.0", 45 | "mocha": "^3.2.0", 46 | "mongodb-js-fmt": "^0.0.3", 47 | "mongodb-js-precommit": "^0.2.8", 48 | "pre-commit": "^1.1.2", 49 | "react": "^16.12.0", 50 | "react-test-renderer": "^16.12.0", 51 | "react-dom": "^16.12.0", 52 | "sinon": "^1.17.7", 53 | "standard-version": "^4.0.0" 54 | }, 55 | "license": "Apache-2.0", 56 | "precommit": [ 57 | "check" 58 | ] 59 | } 60 | -------------------------------------------------------------------------------- /.jsfmtrc: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "default", 3 | "plugins": [ 4 | "esformatter-quotes", 5 | "esformatter-semicolons", 6 | "esformatter-braces", 7 | "esformatter-quote-props", 8 | "esformatter-dot-notation", 9 | "esformatter-var-each", 10 | "esformatter-parseint", 11 | "esformatter-eol-last", 12 | "esformatter-spaced-lined-comment", 13 | "esformatter-remove-trailing-commas" 14 | ], 15 | "quotes": { 16 | "type": "single", 17 | "avoidEscape": false 18 | }, 19 | "indent": { 20 | "value": " " 21 | }, 22 | "whiteSpace" : { 23 | "value" : " ", 24 | "removeTrailing" : 1, 25 | "before" : { 26 | "ArrayExpressionOpening" : 0, 27 | "ArrayExpressionClosing" : 0, 28 | "ArrayExpressionComma" : 0, 29 | "ArgumentComma" : 0, 30 | "ArgumentList" : 0, 31 | "ArgumentListArrayExpression" : 0, 32 | "ArgumentListFunctionExpression" : 0, 33 | "ArgumentListObjectExpression" : 0, 34 | "AssignmentOperator" : 1, 35 | "BinaryExpression": 0, 36 | "BinaryExpressionOperator" : 1, 37 | "BlockComment" : 1, 38 | "CallExpression" : -1, 39 | "CatchParameterList" : 0, 40 | "CatchOpeningBrace" : 1, 41 | "CatchClosingBrace" : 1, 42 | "CatchKeyword" : 1, 43 | "CommaOperator" : 0, 44 | "ConditionalExpressionConsequent" : 1, 45 | "ConditionalExpressionAlternate" : 1, 46 | "DoWhileStatementOpeningBrace" : 1, 47 | "DoWhileStatementClosingBrace" : 1, 48 | "DoWhileStatementConditional" : 1, 49 | "EmptyStatement" : 0, 50 | "ExpressionClosingParentheses" : 0, 51 | "FinallyKeyword" : -1, 52 | "FinallyOpeningBrace" : 1, 53 | "FinallyClosingBrace" : 1, 54 | "ForInStatement" : 1, 55 | "ForInStatementExpressionOpening" : 1, 56 | "ForInStatementExpressionClosing" : 0, 57 | "ForInStatementOpeningBrace" : 1, 58 | "ForInStatementClosingBrace" : 1, 59 | "ForStatement" : 1, 60 | "ForStatementExpressionOpening" : 1, 61 | "ForStatementExpressionClosing" : 0, 62 | "ForStatementOpeningBrace" : 1, 63 | "ForStatementClosingBrace" : 1, 64 | "ForStatementSemicolon" : 0, 65 | "FunctionDeclarationOpeningBrace" : 1, 66 | "FunctionDeclarationClosingBrace" : 1, 67 | "FunctionExpressionOpeningBrace" : 1, 68 | "FunctionExpressionClosingBrace" : 1, 69 | "IfStatementConditionalOpening" : 1, 70 | "IfStatementConditionalClosing" : 0, 71 | "IfStatementOpeningBrace" : 1, 72 | "IfStatementClosingBrace" : 1, 73 | "ElseStatementOpeningBrace" : 1, 74 | "ElseStatementClosingBrace" : 1, 75 | "ElseIfStatementOpeningBrace" : 1, 76 | "ElseIfStatementClosingBrace" : 1, 77 | "MemberExpressionClosing" : 0, 78 | "LineComment" : 1, 79 | "LogicalExpressionOperator" : 1, 80 | "Property" : 1, 81 | "PropertyValue" : 1, 82 | "ParameterComma" : 0, 83 | "ParameterList" : 0, 84 | "SwitchDiscriminantOpening" : 1, 85 | "SwitchDiscriminantClosing" : 0, 86 | "ThrowKeyword": 1, 87 | "TryKeyword": -1, 88 | "TryOpeningBrace" : 1, 89 | "TryClosingBrace" : 1, 90 | "UnaryExpressionOperator": 0, 91 | "VariableName" : 1, 92 | "VariableValue" : 1, 93 | "WhileStatementConditionalOpening" : 1, 94 | "WhileStatementConditionalClosing" : 0, 95 | "WhileStatementOpeningBrace" : 1, 96 | "WhileStatementClosingBrace" : 1 97 | }, 98 | 99 | "after" : { 100 | "ArrayExpressionOpening" : 0, 101 | "ArrayExpressionClosing" : 0, 102 | "ArrayExpressionComma" : 1, 103 | "ArgumentComma" : 1, 104 | "ArgumentList" : 0, 105 | "ArgumentListArrayExpression" : 0, 106 | "ArgumentListFunctionExpression" : 0, 107 | "ArgumentListObjectExpression" : 0, 108 | "AssignmentOperator" : 1, 109 | "BinaryExpression": 0, 110 | "BinaryExpressionOperator" : 1, 111 | "BlockComment" : 1, 112 | "CallExpression" : 0, 113 | "CatchParameterList" : 0, 114 | "CatchOpeningBrace" : 1, 115 | "CatchClosingBrace" : 1, 116 | "CatchKeyword" : 1, 117 | "CommaOperator" : 1, 118 | "ConditionalExpressionConsequent" : 1, 119 | "ConditionalExpressionTest" : 1, 120 | "DoWhileStatementOpeningBrace" : 1, 121 | "DoWhileStatementClosingBrace" : 1, 122 | "DoWhileStatementBody" : 1, 123 | "EmptyStatement" : 0, 124 | "ExpressionOpeningParentheses" : 0, 125 | "FinallyKeyword" : -1, 126 | "FinallyOpeningBrace" : 1, 127 | "FinallyClosingBrace" : 1, 128 | "ForInStatement" : 1, 129 | "ForInStatementExpressionOpening" : 0, 130 | "ForInStatementExpressionClosing" : 1, 131 | "ForInStatementOpeningBrace" : 1, 132 | "ForInStatementClosingBrace" : 1, 133 | "ForStatement" : 1, 134 | "ForStatementExpressionOpening" : 0, 135 | "ForStatementExpressionClosing" : 1, 136 | "ForStatementClosingBrace" : 1, 137 | "ForStatementOpeningBrace" : 1, 138 | "ForStatementSemicolon" : 1, 139 | "FunctionReservedWord": 0, 140 | "FunctionName" : 0, 141 | "FunctionExpressionOpeningBrace" : 1, 142 | "FunctionExpressionClosingBrace" : 0, 143 | "FunctionDeclarationOpeningBrace" : 0, 144 | "FunctionDeclarationClosingBrace" : 0, 145 | "IfStatementConditionalOpening" : 0, 146 | "IfStatementConditionalClosing" : 1, 147 | "IfStatementOpeningBrace" : 1, 148 | "IfStatementClosingBrace" : 1, 149 | "ElseStatementOpeningBrace" : 1, 150 | "ElseStatementClosingBrace" : 1, 151 | "ElseIfStatementOpeningBrace" : 1, 152 | "ElseIfStatementClosingBrace" : 1, 153 | "MemberExpressionOpening" : 0, 154 | "LogicalExpressionOperator" : 1, 155 | "ObjectExpressionClosingBrace": 0, 156 | "PropertyName" : 0, 157 | "PropertyValue" : 0, 158 | "ParameterComma" : 1, 159 | "ParameterList" : 0, 160 | "SwitchDiscriminantOpening" : 0, 161 | "SwitchDiscriminantClosing" : 1, 162 | "ThrowKeyword": 1, 163 | "TryKeyword": -1, 164 | "TryOpeningBrace" : 1, 165 | "TryClosingBrace" : 1, 166 | "UnaryExpressionOperator": 0, 167 | "VariableName" : 1, 168 | "WhileStatementConditionalOpening" : 0, 169 | "WhileStatementConditionalClosing" : 1, 170 | "WhileStatementOpeningBrace" : 1, 171 | "WhileStatementClosingBrace" : 1 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | 6 | # [3.1.0](https://github.com/mongodb-js/connect-backbone-to-react/compare/v3.0.0...v3.1.0) (2022-07-05) 7 | 8 | 9 | ### Features 10 | 11 | * Support React 17 and 18 as peerDependencies ([eee15fb](https://github.com/mongodb-js/connect-backbone-to-react/commit/eee15fb)) 12 | 13 | 14 | 15 | 16 | # [3.0.0](https://github.com/mongodb-js/connect-backbone-to-react/compare/v2.0.0...v3.0.0) (2020-01-30) 17 | 18 | * Now requires at least React v16.6.0 19 | * Removed usage of legacy Context API 20 | * Moved to new React Context API 21 | * Functionality should be equivalent, but with all things be sure to test your application. 22 | 23 | 24 | # [2.0.0](https://github.com/mongodb-js/connect-backbone-to-react/compare/v1.6.1...v2.0.0) (2020-01-15) 25 | 26 | * Now requires at least React v16.3.0. 27 | 28 | 29 | ## [1.6.1](https://github.com/mongodb-js/connect-backbone-to-react/compare/v1.6.0...v1.6.1) (2018-10-26) 30 | 31 | 32 | ### Bug Fixes 33 | 34 | * adds check for null model to componentWillReceiveProps ([#21](https://github.com/mongodb-js/connect-backbone-to-react/issues/21)) ([9793020](https://github.com/mongodb-js/connect-backbone-to-react/commit/9793020)) 35 | 36 | 37 | 38 | 39 | # [1.6.0](https://github.com/mongodb-js/connect-backbone-to-react/compare/v1.5.0...v1.6.0) (2018-06-07) 40 | 41 | 42 | ### Features 43 | 44 | * Add access to wrapped component's ref ([#20](https://github.com/mongodb-js/connect-backbone-to-react/issues/20)) ([b33b567](https://github.com/mongodb-js/connect-backbone-to-react/commit/b33b567)) 45 | 46 | 47 | 48 | 49 | # [1.5.0](https://github.com/mongodb-js/connect-backbone-to-react/compare/v1.4.0...v1.5.0) (2017-12-11) 50 | 51 | 52 | ### Bug Fixes 53 | 54 | * Add missing `assert` to test ([127d808](https://github.com/mongodb-js/connect-backbone-to-react/commit/127d808)) 55 | 56 | 57 | ### Features 58 | 59 | * Add watch mode for local development ([4e7fe43](https://github.com/mongodb-js/connect-backbone-to-react/commit/4e7fe43)) 60 | * Merge models passed from context and props ([#15](https://github.com/mongodb-js/connect-backbone-to-react/issues/15)) ([cf2b9e8](https://github.com/mongodb-js/connect-backbone-to-react/commit/cf2b9e8)), closes [#14](https://github.com/mongodb-js/connect-backbone-to-react/issues/14) 61 | 62 | 63 | 64 | 65 | # [1.4.0](https://github.com/mongodb-js/connect-backbone-to-react/compare/v1.3.0...v1.4.0) (2017-09-20) 66 | 67 | 68 | ### Features 69 | 70 | * Add props as second arg to mapModelsToProps ([d971b884e](https://github.com/mongodb-js/connect-backbone-to-react/commit/d971b884e)) 71 | 72 | 73 | 74 | 75 | 76 | ## [1.3.2](https://github.com/mongodb-js/connect-backbone-to-react/compare/v1.3.1...v1.3.2) (2017-09-18) 77 | 78 | 79 | ### Bug Fixes 80 | 81 | * Handle error when passed an undefined model ([e5f09c9](https://github.com/mongodb-js/connect-backbone-to-react/commit/e5f09c9)) 82 | * Handle null models in default mapping ([ab0a0e2](https://github.com/mongodb-js/connect-backbone-to-react/commit/ab0a0e2)) 83 | 84 | 85 | 86 | 87 | ## [1.3.1](https://github.com/mongodb-js/connect-backbone-to-react/compare/v1.3.0...v1.3.1) (2017-09-15) 88 | 89 | 90 | ### Bug Fixes 91 | 92 | * Add models existence check on props update ([469073c](https://github.com/mongodb-js/connect-backbone-to-react/commit/469073c)) 93 | 94 | 95 | 96 | 97 | # [1.3.0](https://github.com/mongodb-js/connect-backbone-to-react/compare/v1.2.0...v1.3.0) (2017-09-13) 98 | 99 | 100 | ### Features 101 | 102 | * Fix bug where models were not listened to when they were set as properties, after initial construction. ([7187bc7](https://github.com/mongodb-js/connect-backbone-to-react/commit/7187bc7)) 103 | 104 | 105 | 106 | 107 | # [1.2.0](https://github.com/mongodb-js/connect-backbone-to-react/compare/v1.1.0...v1.2.0) (2017-06-21) 108 | 109 | 110 | ### Features 111 | 112 | * Update state when props passed to connected components change ([3e2f59f](https://github.com/mongodb-js/connect-backbone-to-react/commit/3e2f59f)) 113 | 114 | 115 | 116 | 117 | # [1.1.0](https://github.com/mongodb-js/connect-backbone-to-react/compare/v1.0.1...v1.1.0) (2017-04-28) 118 | 119 | 120 | ### Features 121 | 122 | * Support React 15.5+ ([bbea532](https://github.com/mongodb-js/connect-backbone-to-react/commit/bbea532)) 123 | 124 | 125 | 126 | 127 | ## [1.0.1](https://github.com/mongodb-js/connect-backbone-to-react/compare/v1.0.0...v1.0.1) (2017-03-17) 128 | 129 | 130 | ### Bug Fixes 131 | 132 | * Don't call createNewProps if component has been unmounted. ([1a83663](https://github.com/mongodb-js/connect-backbone-to-react/commit/1a83663)) 133 | * Rename Connect to ConnectBackboneToReact ([f2899c6](https://github.com/mongodb-js/connect-backbone-to-react/commit/f2899c6)), closes [#4](https://github.com/mongodb-js/connect-backbone-to-react/issues/4) 134 | * Spy on setState for most test assertions. ([f3253d2](https://github.com/mongodb-js/connect-backbone-to-react/commit/f3253d2)) 135 | 136 | 137 | 138 | 139 | # 1.0.0 (2017-03-01) 140 | 141 | 142 | ### Bug Fixes 143 | 144 | * Actually add packages we depend on to our dependencies list. ([3783799](https://github.com/mongodb-js/connect-backbone-to-react/commit/3783799)) 145 | * PR feedback from Justin ([f5764d0](https://github.com/mongodb-js/connect-backbone-to-react/commit/f5764d0)) 146 | 147 | 148 | ### Features 149 | 150 | * Add modelTypes as an option you can set. ([0ce0b46](https://github.com/mongodb-js/connect-backbone-to-react/commit/0ce0b46)) 151 | * Add standard-version for automating changelog creation. ([214ae1f](https://github.com/mongodb-js/connect-backbone-to-react/commit/214ae1f)) 152 | * Create BackboneProvider ([52c41c0](https://github.com/mongodb-js/connect-backbone-to-react/commit/52c41c0)) 153 | * Initial import of all functionality! ([0b16681](https://github.com/mongodb-js/connect-backbone-to-react/commit/0b16681)) 154 | * Move react to peerDep ([0d62b8b](https://github.com/mongodb-js/connect-backbone-to-react/commit/0d62b8b)) 155 | * Update API for connectBackboneToReact to require passing modelsMap through as a prop. ([2a23d8d](https://github.com/mongodb-js/connect-backbone-to-react/commit/2a23d8d)) 156 | -------------------------------------------------------------------------------- /test/backbone-provider.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const { mount } = require('enzyme'); 3 | const sinon = require('sinon'); 4 | const React = require('react'); 5 | const { Component } = React; 6 | const { Model, Collection } = require('backbone'); 7 | const BackboneProvider = require('../lib/backbone-provider'); // eslint-disable-line no-unused-vars 8 | const connectBackboneToReact = require('../lib/connect-backbone-to-react'); 9 | 10 | describe('BackboneProvider', function() { 11 | let sandbox; 12 | let wrapper; 13 | let componentStub; 14 | let modelsMap; 15 | 16 | let userModel; 17 | let userCollection; 18 | 19 | class Child extends Component { 20 | render() { 21 | return ( 22 |
23 | {this.props.user.name} 24 |
25 | ); 26 | } 27 | } 28 | 29 | // eslint-disable-next-line no-unused-vars 30 | const ConnectedChild = connectBackboneToReact()(Child); 31 | 32 | class Parent extends Component { 33 | render() { 34 | return ( 35 |
36 |
37 | {this.props.user.name} 38 |
39 | 40 |
41 | ); 42 | } 43 | } 44 | 45 | beforeEach(function() { 46 | sandbox = sinon.sandbox.create(); 47 | 48 | userModel = new Model({ 49 | name: 'Harry', 50 | age: 25, 51 | hungry: true, 52 | }); 53 | 54 | userCollection = new Collection([userModel]); 55 | 56 | modelsMap = { 57 | user: userModel, 58 | coll: userCollection, 59 | }; 60 | }); 61 | 62 | afterEach(function() { 63 | sandbox.restore(); 64 | }); 65 | 66 | describe('when "modelsMap" is provided via BackboneProvider', function() { 67 | let renderSpy; 68 | beforeEach(function() { 69 | const ConnectedParent = connectBackboneToReact()(Parent); 70 | renderSpy = sandbox.spy(ConnectedParent.prototype, 'render'); 71 | 72 | wrapper = mount( 73 | 74 | 75 | 76 | ); 77 | componentStub = wrapper.find(Parent); 78 | 79 | // Don't track initial render. 80 | renderSpy.reset(); 81 | }); 82 | 83 | afterEach(function() { 84 | wrapper.unmount(); 85 | }); 86 | 87 | it('passes mapped models and collections as properties to wrapped component', function() { 88 | assert.deepEqual(componentStub.props().user, userModel.toJSON()); 89 | assert.deepEqual(componentStub.props().coll, userCollection.toJSON()); 90 | }); 91 | 92 | it('two instances of Harry text are rendered', function() { 93 | assert.equal(wrapper.find('.name').length, 2); 94 | }); 95 | 96 | it('every connected Component is given the same "modelsMap"', function() { 97 | assert( 98 | wrapper.find('.name').everyWhere(n => n.text() === 'Harry') 99 | ); 100 | }); 101 | 102 | it('should handle updates to passed props', function() { 103 | const model = new Model({ name: 'Jill' }); 104 | wrapper.setProps({ models: { user: model }}); 105 | 106 | assert(wrapper.find('.name').everyWhere(n => n.text() === 'Jill')); 107 | }); 108 | }); 109 | 110 | describe('when "modelsMap" is provided via BackboneProvider and a parent component', function() { 111 | it('merges models passed via BackboneProvider and a parent component', function() { 112 | class UserAndSettings extends Component { 113 | render() { 114 | return ( 115 |
116 |
117 | {this.props.user.name} 118 |
119 |
120 | {this.props.settings.color} 121 |
122 |
123 | ); 124 | } 125 | } 126 | // eslint-disable-next-line no-unused-vars 127 | const ConnectedUserAndSettings = connectBackboneToReact()(UserAndSettings); 128 | 129 | const settingsModel = new Model({ 130 | color: 'purple', 131 | }); 132 | const propsModelsMap = { settings: settingsModel }; 133 | 134 | wrapper = mount( 135 | 136 | 137 | 138 | ); 139 | 140 | const modelsFromContext = wrapper 141 | .find('.name') 142 | .findWhere((n) => !n.type() && n.text() === userModel.get('name')) 143 | .length; 144 | const modelsFromParent = wrapper 145 | .find('.color') 146 | .findWhere((n) => !n.type() && n.text() === settingsModel.get('color')) 147 | .length; 148 | 149 | // Check that we've rendered data from models passed by both context and the parent component. 150 | assert.equal(modelsFromContext, 1); 151 | assert.equal(modelsFromParent, 1); 152 | }); 153 | 154 | it('gives priority to models passed via a parent component', function() { 155 | const otherUserModel = new Model({ 156 | name: 'Spencer', 157 | age: 22, 158 | hungry: true, 159 | }); 160 | 161 | class PassingParent extends Component { 162 | render() { 163 | // We're using the same key (`user`) as the modelsMap passed via context. 164 | const propsModelsMap = { user: otherUserModel }; 165 | 166 | return ( 167 |
168 |
169 | {this.props.user.name} 170 |
171 |
172 | 173 |
174 |
175 | ); 176 | } 177 | } 178 | // eslint-disable-next-line no-unused-vars 179 | const ConnectedPassingParent = connectBackboneToReact()(PassingParent); 180 | 181 | wrapper = mount( 182 | 183 | 184 | 185 | ); 186 | 187 | const modelsFromContext = wrapper 188 | .find('.name') 189 | .findWhere((n) => !n.type() && n.text() === userModel.get('name')) 190 | .length; 191 | const modelsFromParent = wrapper 192 | .find('.child-wrapper') 193 | .find('.name') 194 | .findWhere((n) => !n.type() && n.text() === otherUserModel.get('name')) 195 | .length; 196 | 197 | // Check that we've given priority to models passed from the parent component. 198 | assert.equal(modelsFromContext, 1); 199 | assert.equal(modelsFromParent, 1); 200 | }); 201 | }); 202 | }); 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # connect-backbone-to-react [![travis][travis_img]][travis_url] [![npm][npm_img]][npm_url] 2 | 3 | > Connect Backbone Models and Collections to React. 4 | 5 | ## Usage 6 | 7 | `npm install connect-backbone-to-react` or `yarn add connect-backbone-to-react` in your React/Backbone project. See code samples below to how to integrate into your code. 8 | 9 | ## Example 10 | 11 | [![Edit connectBackboneToReact](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/l5n4m0qk79?module=%2FDemo.js) 12 | 13 | ### connectBackboneToReact 14 | 15 | ```javascript 16 | const UserModel = Backbone.Model.extend(); 17 | const UserCollection = Backbone.Collection.extend({ model: UserModel }); 18 | 19 | const userInstance = new UserModel({ name: "Harry", laughs: true }); 20 | const anotherUserInstance = new UserModel({ name: "Samantha", laughs: false }); 21 | const userCollection = new UserCollection([userInstance, anotherUserInstance]); 22 | 23 | class MyComponent extends React.Component { 24 | render() { 25 | return ( 26 |
27 |

My user laughs: {this.props.doesUserLaugh ? "yes" : "no"}

28 | 33 |

All Users

34 | 39 |
40 | ); 41 | } 42 | } 43 | 44 | // Maps Models to properties to give to the React Component. Optional. 45 | // Default behavior is to call `.toJSON()` on every Model and Collection. 46 | // Second argument are props given to the React Component. 47 | const mapModelsToProps = (models, props) => { 48 | const { user, allUsers } = models; 49 | const { showOnlyLaughingUsers } = props; 50 | 51 | // Everything returned from this function will be given as a prop to your Component. 52 | return { 53 | doesUserLaugh: user.get("laughs"), 54 | users: showOnlyLaughingUsers 55 | ? allUsers.toJSON().filter(user => user.laughs === true) 56 | : allUsers.toJSON(), 57 | setUserLaughs(newVal) { 58 | user.set("laughs", newVal); 59 | } 60 | }; 61 | }; 62 | 63 | // Options. 64 | const options = { 65 | // Should our event handler function be wrapped in a debounce function 66 | // to prevent many re-renders. 67 | debounce: false, // or `true`, or a number that will be used in the debounce function. 68 | 69 | // Define what events you want to listen to on your Backbone Model or Collection 70 | // that will cause your React Component to re-render. 71 | // By default it's ['all'] for every Model and Collection given. 72 | events: { 73 | user: ["change:name", "change:laughs"] 74 | // You can disable listening to events by passing in `false` or an empty array. 75 | }, 76 | 77 | // Define what modelTypes you expect to be contained on your `modelsMap` object. 78 | // Useful for validating that you'll be given what model type you expect. 79 | // Uses instanceof, and throws an error if instanceof returns false. 80 | // By default no modelTypes are defined. 81 | modelTypes: { 82 | user: UserModel, 83 | allUsers: UserCollection 84 | }, 85 | 86 | // Enable access to the wrapped component's ref with the `withRef` option. 87 | // You can then access the wrapped component from the connected component's `getWrappedInstance()`. 88 | // This is similar to react-redux's connectAdvanced() HOC. 89 | // By default, `withRef` is false. 90 | withRef: true 91 | }; 92 | 93 | const { connectBackboneToReact } = require("connect-backbone-to-react"); 94 | 95 | // Create our Connected Higher order Component (HOC). 96 | const MyComponentConnected = connectBackboneToReact( 97 | mapModelsToProps, 98 | options 99 | )(MyComponent); 100 | ``` 101 | 102 | Now that you've created your HOC you can use it! 103 | 104 | ```javascript 105 | // Map your Backbone Model and Collections to names that will be provided to 106 | // your mapModelsToProps function. 107 | const modelsMap = { 108 | user: userInstance, 109 | allUsers: userCollection 110 | }; 111 | 112 | ReactDOM.render( 113 | // Pass the modelsMap to the HOC via the models prop. 114 | , 115 | document.getElementById("app") 116 | ); 117 | ``` 118 | 119 | ### BackboneProvider 120 | 121 | Alternatively you might have a tree of connected Components. We shouldn't pass that `modelsMap` object from one component to another. Instead we can take inspiration from [react-redux's Provider component](https://github.com/reactjs/react-redux/blob/master/docs/api.md#provider-store). 122 | 123 | ```javascript 124 | const { BackboneProvider } = require('connect-backbone-to-react'); 125 | 126 | const modelsMap = { 127 | user: userInstance, 128 | allUsers: userCollection, 129 | }, 130 | 131 | ReactDOM.render( 132 | // Pass the modelsMap to the BackboneProvider via the models prop. 133 | // It will then get shared to every child connected component via React's context. 134 | 135 | 136 | 137 | 138 | , 139 | document.getElementById('app') 140 | ); 141 | ``` 142 | 143 | ## Rendering React Within Backbone.View 144 | 145 | This library's focus is on sharing Backbone.Models with React Components. It is not concerned with how to render React Components within Backbone.Views. [The React docs provide a possible implementation for this interopt.](https://reactjs.org/docs/integrating-with-other-libraries.html#embedding-react-in-a-backbone-view) 146 | 147 | ## Local development 148 | 149 | To develop this library locally, run the following commands in the project root directory: 150 | 151 | 1. `npm run watch`. The library will be automatically compiled in the background as you make changes. 152 | 2. `npm link` and then follow the instructions to use the local version of this library in another project that uses `connect-backbone-to-react`. 153 | 154 | Run `npm test` to run the unit tests. 155 | 156 | ### Releasing a new version 157 | 158 | 1. Make sure you have up to date `node_modules` before you proceed. Can be done via `npm ci` 159 | 2. Update the version via: `npm run release -- --release-as=major|minor|patch` 160 | 3. Optionally manually edit the revised `CHANGELOG.md` file. Commit changes. 161 | 4. Follow the directions from step 2: run `git push --follow-tags origin master; npm publish` to publish 162 | 5. Rejoice! 163 | 164 | ## License 165 | 166 | Apache 2.0 167 | 168 | [travis_img]: https://img.shields.io/travis/mongodb-js/connect-backbone-to-react.svg 169 | [travis_url]: https://travis-ci.org/mongodb-js/connect-backbone-to-react 170 | [npm_img]: https://img.shields.io/npm/v/connect-backbone-to-react.svg 171 | [npm_url]: https://npmjs.org/package/connect-backbone-to-react 172 | -------------------------------------------------------------------------------- /lib/connect-backbone-to-react.js: -------------------------------------------------------------------------------- 1 | const hoistStatics = require('hoist-non-react-statics'); 2 | const { Component, createElement } = require('react'); 3 | const PropTypes = require('prop-types'); 4 | const debounceFn = require('lodash.debounce'); 5 | const BackboneToReactContext = require('./context'); 6 | 7 | function getDisplayName(name) { 8 | return `connectBackboneToReact(${name})`; 9 | } 10 | 11 | function defaultMapModelsToProps(models) { 12 | return Object.keys(models).reduce((acc, modelKey) => { 13 | const model = models[modelKey]; 14 | if (!model) return; 15 | 16 | acc[modelKey] = model.toJSON(); 17 | return acc; 18 | }, {}); 19 | } 20 | 21 | module.exports = function connectBackboneToReact( 22 | mapModelsToProps, 23 | options = {} 24 | ) { 25 | if (typeof mapModelsToProps !== 'function') { 26 | mapModelsToProps = defaultMapModelsToProps; 27 | } 28 | 29 | const { 30 | debounce = false, 31 | events = {}, 32 | modelTypes = {}, 33 | withRef = false, 34 | } = options; 35 | 36 | function getEventNames(modelName) { 37 | let eventNames = events[modelName]; 38 | 39 | // Allow turning off event handlers by setting events to false. 40 | if (eventNames === false) { 41 | return []; 42 | } 43 | 44 | if (!Array.isArray(eventNames)) { 45 | return ['all']; 46 | } 47 | 48 | return eventNames; 49 | } 50 | 51 | function validateModelTypes(modelsMap) { 52 | return Object.keys(modelTypes).forEach(modelKey => { 53 | const ModelConstructor = modelTypes[modelKey]; 54 | const modelInstance = modelsMap[modelKey]; 55 | 56 | const isInstanceOfModel = modelInstance instanceof ModelConstructor; 57 | if (!isInstanceOfModel) { 58 | throw new Error(`"${modelKey}" model found on modelsMap does not match type required.`); 59 | } 60 | }); 61 | } 62 | 63 | return function createWrapper(WrappedComponent) { 64 | const wrappedComponentName = WrappedComponent.displayName 65 | || WrappedComponent.name 66 | || 'Component'; 67 | 68 | const displayName = getDisplayName(wrappedComponentName); 69 | 70 | class ConnectBackboneToReact extends Component { 71 | constructor(props, context) { 72 | super(props, context); 73 | 74 | this.componentIsMounted = false; 75 | 76 | this.createNewProps = this.createNewProps.bind(this); 77 | this.setWrappedInstance = this.setWrappedInstance.bind(this); 78 | 79 | if (debounce) { 80 | const debounceWait = typeof debounce === 'number' ? debounce : 0; 81 | this.createNewProps = debounceFn(this.createNewProps, debounceWait); 82 | } 83 | 84 | this.createEventListeners(); 85 | } 86 | 87 | getModels() { 88 | const models = Object.assign({}, this.context, this.props.models); 89 | validateModelTypes(models); 90 | return models; 91 | } 92 | 93 | createEventListeners() { 94 | const models = this.getModels(); 95 | Object.keys(models).forEach(mapKey => { 96 | const model = models[mapKey]; 97 | // Do not attempt to create event listeners on an undefined model. 98 | if (!model) return; 99 | 100 | this.createEventListener(mapKey, model); 101 | }); 102 | 103 | // Store a reference to the models with event listeners for the next update. 104 | this.prevModels = models; 105 | } 106 | 107 | createEventListener(modelName, model) { 108 | getEventNames(modelName).forEach(name => { 109 | model.on(name, this.createNewProps, this); 110 | }); 111 | } 112 | 113 | removeEventListener(modelName, model) { 114 | getEventNames(modelName).forEach(name => { 115 | model.off(name, this.createNewProps, this); 116 | }); 117 | } 118 | 119 | createNewProps() { 120 | // Bail out if our component has been unmounted. 121 | // The only case where this flag is encountered is when this component 122 | // is unmounted within an event handler but the 'all' event is still triggered. 123 | // It is covered in a test case. 124 | // Also bails if we haven't yet mounted, to avoid warnings in strict mode. 125 | if (!this.componentIsMounted) { 126 | return; 127 | } 128 | this.forceUpdate(); 129 | } 130 | 131 | setWrappedInstance(ref) { 132 | this.wrappedInstance = ref; 133 | } 134 | 135 | getWrappedInstance() { 136 | if (!withRef) { 137 | throw new Error('getWrappedInstance() requires withRef to be true.'); 138 | } 139 | 140 | return this.wrappedInstance; 141 | } 142 | 143 | componentDidMount() { 144 | this.componentIsMounted = true; 145 | } 146 | 147 | componentDidUpdate() { 148 | // add and remove listeners 149 | const models = this.getModels(); 150 | const prevModels = this.prevModels; 151 | 152 | // Bind event listeners for each model that changed. 153 | Object.keys(Object.assign({}, models, prevModels)).forEach(mapKey => { 154 | const model = models[mapKey]; 155 | const prevModel = prevModels[mapKey]; 156 | 157 | // Do not attempt to create event listeners on an undefined model. 158 | if (!model) { 159 | // Instead, if it was previously defined, remove the old listeners. 160 | if (prevModel) { 161 | this.removeEventListener(mapKey, prevModel); 162 | } 163 | return; 164 | } 165 | 166 | if (prevModel === model) return; // Did not change. 167 | 168 | this.createEventListener(mapKey, model); 169 | }); 170 | 171 | // Store a reference to the models with event listeners for the next update. 172 | this.prevModels = models; 173 | } 174 | 175 | componentWillUnmount() { 176 | if (debounce) { 177 | this.createNewProps.cancel(); 178 | } 179 | 180 | Object.keys(this.prevModels).forEach(mapKey => { 181 | const model = this.prevModels[mapKey]; 182 | // Do not attempt to remove event listeners on an undefined model. 183 | if (!model) return; 184 | this.removeEventListener(mapKey, model); 185 | }); 186 | 187 | this.componentIsMounted = false; 188 | } 189 | 190 | render() { 191 | const wrappedProps = Object.assign( 192 | {}, 193 | mapModelsToProps(this.getModels(), this.props), 194 | this.props 195 | ); 196 | 197 | // Don't pass through models prop. 198 | wrappedProps.models = undefined; 199 | 200 | if (withRef) { 201 | wrappedProps.ref = this.setWrappedInstance; 202 | } 203 | 204 | return createElement(WrappedComponent, wrappedProps); 205 | } 206 | } 207 | 208 | const propTypes = { 209 | models: PropTypes.object, 210 | }; 211 | 212 | ConnectBackboneToReact.WrappedComponent = WrappedComponent; 213 | ConnectBackboneToReact.displayName = displayName; 214 | ConnectBackboneToReact.propTypes = propTypes; 215 | ConnectBackboneToReact.contextType = BackboneToReactContext; 216 | 217 | return hoistStatics(ConnectBackboneToReact, WrappedComponent); 218 | }; 219 | }; 220 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2017 MongoDB Inc. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /test/connect-backbone-to-react.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const { mount } = require('enzyme'); 3 | const sinon = require('sinon'); 4 | const React = require('react'); 5 | const { Component } = React; 6 | const { Model, Collection } = require('backbone'); 7 | const connectBackboneToReact = require('../lib/connect-backbone-to-react'); 8 | 9 | describe('connectBackboneToReact', function() { 10 | let sandbox; 11 | let wrapper; 12 | let stub; 13 | let modelsMap; 14 | let mapModelsToProps; 15 | 16 | let userModel; 17 | const UserModel = Model.extend({}); 18 | 19 | let userCollection; 20 | const UserCollection = Collection.extend({ 21 | model: UserModel, 22 | }); 23 | 24 | let settingsModel; 25 | const SettingsModel = Model.extend({}); 26 | 27 | let userOnSpy; 28 | let userOffSpy; 29 | let collOnSpy; 30 | let collOffSpy; 31 | 32 | class TestComponent extends Component { 33 | render() { 34 | return ( 35 |
36 |
37 | {this.props.name} 38 |
39 |
40 | {this.props.age} 41 |
42 |
43 | {this.props.hungry ? 'hungry' : 'not hungry'} 44 |
45 |
46 | ); 47 | } 48 | } 49 | 50 | beforeEach(function() { 51 | sandbox = sinon.sandbox.create(); 52 | 53 | userModel = new UserModel({ 54 | name: 'Harry', 55 | age: 25, 56 | hungry: true, 57 | }); 58 | 59 | userCollection = new UserCollection([userModel]); 60 | 61 | userOnSpy = sandbox.spy(userModel, 'on'); 62 | collOnSpy = sandbox.spy(userCollection, 'on'); 63 | userOffSpy = sandbox.spy(userModel, 'off'); 64 | collOffSpy = sandbox.spy(userCollection, 'off'); 65 | 66 | modelsMap = { 67 | user: userModel, 68 | coll: userCollection, 69 | }; 70 | 71 | mapModelsToProps = function({ user, coll }) { 72 | return { 73 | name: user.get('name'), 74 | age: user.get('age'), 75 | hungry: user.get('hungry'), 76 | changeName: function(newName) { 77 | user.set('name', newName); 78 | }, 79 | users: coll.toJSON(), 80 | }; 81 | }; 82 | }); 83 | 84 | afterEach(function() { 85 | sandbox.restore(); 86 | }); 87 | 88 | describe('when mounted', function() { 89 | let forceUpdateSpy; 90 | beforeEach(function() { 91 | const ConnectedTest = connectBackboneToReact(mapModelsToProps)(TestComponent); 92 | forceUpdateSpy = sandbox.spy(ConnectedTest.prototype, 'forceUpdate'); 93 | 94 | wrapper = mount(); 95 | stub = wrapper.find(TestComponent); 96 | }); 97 | 98 | afterEach(function() { 99 | if (wrapper.exists()) wrapper.unmount(); 100 | }); 101 | 102 | it('passes mapped models and collections as properties to wrapped component', function() { 103 | assert.equal(stub.props().name, 'Harry'); 104 | assert.equal(stub.props().age, 25); 105 | assert.equal(stub.props().hungry, true); 106 | assert.equal(typeof stub.props().changeName, 'function'); 107 | assert.deepEqual(stub.props().users, [ { name: 'Harry', age: 25, hungry: true } ]); 108 | }); 109 | 110 | it('renders wrapped component', function() { 111 | assert.equal(wrapper.find('.name').text(), 'Harry'); 112 | assert.equal(wrapper.find('.age').text(), '25'); 113 | assert.equal(wrapper.find('.hungry').text(), 'hungry'); 114 | }); 115 | 116 | it('updates properties when props function changes models and collections ', function() { 117 | const newName = 'The Loud One'; 118 | stub.prop('changeName')(newName); 119 | wrapper.update(); 120 | assert.equal(userModel.get('name'), newName); 121 | assert.equal(wrapper.find(TestComponent).prop('name'), newName); 122 | 123 | assert.equal(forceUpdateSpy.callCount, 4); 124 | }); 125 | 126 | it('updates properties when model and collections change', function() { 127 | const newName = 'Banana'; 128 | userModel.set('name', newName); 129 | wrapper.update(); 130 | assert.equal(wrapper.find('.name').text(), 'Banana'); 131 | assert.equal(userModel.get('name'), newName); 132 | assert.equal(wrapper.find(TestComponent).prop('name'), newName); 133 | 134 | assert.equal(forceUpdateSpy.callCount, 4); 135 | }); 136 | 137 | it('creates listeners for every model', function() { 138 | assert(userOnSpy.calledOnce); 139 | assert.equal(userOnSpy.firstCall.args[0], ['all']); 140 | 141 | assert(collOnSpy.calledOnce); 142 | assert.equal(collOnSpy.firstCall.args[0], ['all']); 143 | }); 144 | 145 | it('removes listeners when unmounting', function() { 146 | wrapper.unmount(); 147 | 148 | assert(userOffSpy.calledOnce); 149 | assert.equal(userOffSpy.firstCall.args[0], ['all']); 150 | 151 | assert(collOffSpy.calledOnce); 152 | assert.equal(collOffSpy.firstCall.args[0], ['all']); 153 | }); 154 | 155 | it('does not pass through the models prop to the wrapped component', function() { 156 | assert.equal(stub.props().models, undefined); 157 | }); 158 | }); 159 | 160 | describe('when mounted with debounce set to true', function() { 161 | let forceUpdateSpy; 162 | beforeEach(function() { 163 | const ConnectedTest = connectBackboneToReact( 164 | mapModelsToProps, 165 | { debounce: true } 166 | )(TestComponent); 167 | forceUpdateSpy = sandbox.spy(ConnectedTest.prototype, 'forceUpdate'); 168 | 169 | wrapper = mount(); 170 | stub = wrapper.find(TestComponent); 171 | }); 172 | 173 | afterEach(function() { 174 | if (wrapper.exists()) wrapper.unmount(); 175 | }); 176 | 177 | it('updates properties when model and collections change', function(done) { 178 | const newName = 'Banana'; 179 | userModel.set('name', newName); 180 | 181 | setTimeout(() => { 182 | wrapper.update(); 183 | assert.equal(wrapper.find('.name').text(), 'Banana'); 184 | assert.equal(userModel.get('name'), newName); 185 | assert.equal(wrapper.find(TestComponent).prop('name'), newName); 186 | 187 | assert.equal(forceUpdateSpy.callCount, 1); 188 | 189 | done(); 190 | }, 0); 191 | }); 192 | 193 | it('does not throw when unmounted while debounce is running', function() { 194 | const newName = 'Banana'; 195 | userModel.set('name', newName); 196 | 197 | wrapper.unmount(); 198 | 199 | assert.equal(userModel.get('name'), newName); 200 | assert.equal(stub.props().name, 'Harry'); 201 | 202 | assert.equal(forceUpdateSpy.callCount, 0); 203 | }); 204 | }); 205 | 206 | describe('when mounted with an undefined model', function() { 207 | afterEach(function() { 208 | wrapper.unmount(); 209 | }); 210 | 211 | it('the default should mount and unmount the component successfully', function() { 212 | const ConnectedTest = connectBackboneToReact()(TestComponent); 213 | const eventListenerSpy = sandbox.spy(ConnectedTest.prototype, 'createEventListener'); 214 | wrapper = mount(); 215 | assert.equal(eventListenerSpy.callCount, 0); 216 | }); 217 | }); 218 | 219 | describe('when mounted with custom event names', function() { 220 | let forceUpdateSpy; 221 | beforeEach(function() { 222 | const ConnectedTest = connectBackboneToReact( 223 | mapModelsToProps, 224 | { 225 | events: { 226 | user: ['change:name'], 227 | coll: false, 228 | }, 229 | } 230 | )(TestComponent); 231 | forceUpdateSpy = sandbox.spy(ConnectedTest.prototype, 'forceUpdate'); 232 | 233 | wrapper = mount(); 234 | stub = wrapper.find(TestComponent); 235 | }); 236 | 237 | afterEach(function() { 238 | wrapper.unmount(); 239 | }); 240 | 241 | it('sets one event handler on the userModel', function() { 242 | assert(userOnSpy.calledOnce); 243 | assert.equal(userOnSpy.firstCall.args[0], ['change:name']); 244 | }); 245 | 246 | it('sets 0 event handlers on the userCollection', function() { 247 | assert.equal(collOnSpy.called, false); 248 | }); 249 | 250 | it('updates properties when model\'s name changes', function() { 251 | const newName = 'Banana'; 252 | userModel.set('name', newName); 253 | wrapper.update(); 254 | 255 | assert.equal(userModel.get('name'), newName); 256 | assert.equal(wrapper.find(TestComponent).prop('name'), newName); 257 | }); 258 | 259 | it('rerenders when tracked property changes', function() { 260 | const newName = 'Banana'; 261 | userModel.set('name', newName); 262 | assert.equal(forceUpdateSpy.callCount, 1); 263 | }); 264 | 265 | it('does not update properties when non tracked property changes', function() { 266 | const newAge = 99; 267 | userModel.set('age', newAge); 268 | 269 | assert.equal(userModel.get('age'), newAge); 270 | assert.equal(stub.props().age, 25); 271 | 272 | assert.equal(forceUpdateSpy.callCount, 0); 273 | }); 274 | 275 | it('does not rerender when non tracked property changes', function() { 276 | const newAge = 99; 277 | userModel.set('age', newAge); 278 | assert.equal(forceUpdateSpy.callCount, 0); 279 | }); 280 | }); 281 | 282 | describe('when custom event options disable event tracking', function() { 283 | beforeEach(function() { 284 | const ConnectedTest = connectBackboneToReact( // eslint-disable-line no-unused-vars 285 | mapModelsToProps, 286 | { 287 | events: { 288 | user: [], 289 | coll: false, 290 | }, 291 | } 292 | )(TestComponent); 293 | 294 | wrapper = mount(); 295 | }); 296 | 297 | afterEach(function() { 298 | wrapper.unmount(); 299 | }); 300 | 301 | it('sets 0 event handlers on the userModel', function() { 302 | assert.equal(userOnSpy.called, false); 303 | }); 304 | 305 | it('sets 0 event handlers on the userCollection', function() { 306 | assert.equal(collOnSpy.called, false); 307 | }); 308 | }); 309 | 310 | describe('when mounted with props given to connected component', function() { 311 | let connectedProps = { 312 | fruit: 'banana', 313 | peeled: true, 314 | age: 1, 315 | }; 316 | 317 | beforeEach(function() { 318 | // eslint-disable-next-line no-unused-vars 319 | const ConnectedTest = connectBackboneToReact(mapModelsToProps)(TestComponent); 320 | wrapper = mount(); 321 | stub = wrapper.find(TestComponent); 322 | }); 323 | 324 | afterEach(function() { 325 | wrapper.unmount(); 326 | }); 327 | 328 | it('passes connectedProps through', function() { 329 | assert.equal(stub.props().fruit, 'banana'); 330 | assert.equal(stub.props().peeled, true); 331 | }); 332 | 333 | it('overwrites mapModelsToProps', function() { 334 | assert.equal(stub.props().age, 1); 335 | }); 336 | }); 337 | 338 | describe('when only given modelsMap object', function() { 339 | let forceUpdateSpy; 340 | beforeEach(function() { 341 | const ConnectedTest = connectBackboneToReact()(TestComponent); 342 | forceUpdateSpy = sandbox.spy(ConnectedTest.prototype, 'forceUpdate'); 343 | 344 | wrapper = mount(); 345 | stub = wrapper.find(TestComponent); 346 | }); 347 | 348 | afterEach(function() { 349 | wrapper.unmount(); 350 | }); 351 | 352 | it('uses default mapModelsToProps function', function() { 353 | assert.equal(stub.props().user.name, 'Harry'); 354 | assert.equal(stub.props().user.age, 25); 355 | assert.equal(stub.props().user.hungry, true); 356 | }); 357 | 358 | it('creates default event listeners of "all" for every model', function() { 359 | assert(userOnSpy.calledOnce); 360 | assert.equal(userOnSpy.firstCall.args[0], ['all']); 361 | 362 | assert(collOnSpy.calledOnce); 363 | assert.equal(collOnSpy.firstCall.args[0], ['all']); 364 | }); 365 | 366 | it('re-renders props when model changes', function() { 367 | const newName = 'Banana'; 368 | userModel.set('name', newName); 369 | wrapper.update(); 370 | 371 | assert.equal(wrapper.find(TestComponent).getElement().props.user.name, 'Banana'); 372 | 373 | assert.equal(forceUpdateSpy.callCount, 4); 374 | }); 375 | }); 376 | 377 | describe('when given modelsMap and event options', function() { 378 | let forceUpdateSpy; 379 | beforeEach(function() { 380 | const ConnectedTest = connectBackboneToReact( 381 | null, 382 | { 383 | events: { 384 | user: ['change:name'], 385 | coll: false, 386 | }, 387 | } 388 | )(TestComponent); 389 | forceUpdateSpy = sandbox.spy(ConnectedTest.prototype, 'forceUpdate'); 390 | 391 | wrapper = mount(); 392 | stub = wrapper.find(TestComponent); 393 | }); 394 | 395 | afterEach(function() { 396 | wrapper.unmount(); 397 | }); 398 | 399 | it('uses default mapModelsToProps function', function() { 400 | assert.equal(stub.props().user.name, 'Harry'); 401 | assert.equal(stub.props().user.age, 25); 402 | assert.equal(stub.props().user.hungry, true); 403 | }); 404 | 405 | it('sets one event handler on the userModel', function() { 406 | assert(userOnSpy.calledOnce); 407 | assert.equal(userOnSpy.firstCall.args[0], ['change:name']); 408 | }); 409 | 410 | it('sets 0 event handlers on the userCollection', function() { 411 | assert.equal(collOnSpy.called, false); 412 | }); 413 | 414 | it('re-renders props when model changes', function() { 415 | const newName = 'Banana'; 416 | userModel.set('name', newName); 417 | wrapper.update(); 418 | 419 | assert.equal(wrapper.find(TestComponent).getElement().props.user.name, 'Banana'); 420 | 421 | assert.equal(forceUpdateSpy.callCount, 1); 422 | }); 423 | }); 424 | 425 | describe('when modelTypes are defined on the options object', function() { 426 | describe('and the model given is not an instance of required modelType', function() { 427 | let forceUpdateSpy; 428 | let errObj; 429 | 430 | beforeEach(function() { 431 | const ConnectedTest = connectBackboneToReact( 432 | null, 433 | { 434 | modelTypes: { 435 | user: UserModel, 436 | }, 437 | } 438 | )(TestComponent); 439 | forceUpdateSpy = sandbox.spy(ConnectedTest.prototype, 'forceUpdate'); 440 | 441 | settingsModel = new SettingsModel(); 442 | modelsMap = { 443 | user: settingsModel, 444 | }; 445 | 446 | try { 447 | wrapper = mount(); 448 | } catch (e) { 449 | errObj = e; 450 | } 451 | }); 452 | 453 | it('does not render', function() { 454 | assert.equal(forceUpdateSpy.callCount, 0); 455 | }); 456 | 457 | it('throws an error', function() { 458 | assert(errObj); 459 | assert.equal(errObj.message, '"user" model found on modelsMap does not match type required.'); 460 | }); 461 | }); 462 | 463 | describe('and the modelType required is a parent class', function() { 464 | let renderSpy; 465 | let errObj; 466 | 467 | beforeEach(function() { 468 | const ConnectedTest = connectBackboneToReact( 469 | null, 470 | { 471 | modelTypes: { 472 | user: Model, 473 | }, 474 | } 475 | )(TestComponent); 476 | renderSpy = sandbox.spy(ConnectedTest.prototype, 'render'); 477 | 478 | try { 479 | wrapper = mount(); 480 | } catch (e) { 481 | errObj = e; 482 | } 483 | }); 484 | 485 | it('renders', function() { 486 | assert.equal(renderSpy.callCount, 1); 487 | }); 488 | 489 | it('does not throw an error', function() { 490 | assert(errObj === undefined); 491 | }); 492 | }); 493 | }); 494 | 495 | describe('when using props in mapModelsToProps', function() { 496 | function mapWithProps({ coll }, { name }) { 497 | if (!name) return {}; 498 | const user = coll.findWhere({ name }); 499 | return { 500 | name: user.get('name'), 501 | age: user.get('age'), 502 | hungry: user.get('hungry'), 503 | }; 504 | } 505 | 506 | let a; 507 | let b; 508 | let forceUpdateSpy; 509 | beforeEach(function() { 510 | let ConnectedTest = connectBackboneToReact(mapWithProps)(TestComponent); 511 | forceUpdateSpy = sandbox.spy(ConnectedTest.prototype, 'forceUpdate'); 512 | a = new UserModel({ 513 | name: 'A', 514 | age: '10', 515 | hungry: false, 516 | }); 517 | b = new UserModel({ 518 | name: 'B', 519 | age: '20', 520 | hungry: false, 521 | }); 522 | 523 | const models = { 524 | coll: new UserCollection([a, b]), 525 | }; 526 | 527 | wrapper = mount(); 528 | stub = wrapper.find(TestComponent); 529 | }); 530 | 531 | afterEach(function() { 532 | wrapper.unmount(); 533 | }); 534 | 535 | it('retrieves the correct model based on props', function() { 536 | assert.equal(stub.find('.name').text(), a.get('name')); 537 | assert.equal(stub.find('.age').text(), a.get('age')); 538 | 539 | // Using props should not increase the number of times forceUpdate is called. 540 | assert.equal(forceUpdateSpy.calledOnce, false); 541 | }); 542 | 543 | it('update the models based on new props', function() { 544 | wrapper.setProps({ name: 'B'}); 545 | b.set('hungry', true); 546 | 547 | assert.equal(stub.find('.name').text(), b.get('name')); 548 | assert.equal(stub.find('.age').text(), b.get('age')); 549 | }); 550 | }); 551 | 552 | describe('when passed props change', function() { 553 | let newName; 554 | let newAge; 555 | let newUserModel; 556 | 557 | beforeEach(function() { 558 | const ConnectedTest = connectBackboneToReact(mapModelsToProps)(TestComponent); 559 | 560 | wrapper = mount(React.createElement(ConnectedTest, { models: modelsMap })); 561 | stub = wrapper.find(TestComponent); 562 | 563 | newName = 'Robert'; 564 | newAge = '30'; 565 | 566 | newUserModel = new UserModel({ 567 | name: newName, 568 | age: newAge, 569 | hungry: false, 570 | }); 571 | const newModelsMap = { 572 | user: newUserModel, 573 | coll: userCollection, 574 | }; 575 | 576 | wrapper.setProps({ models: newModelsMap }); 577 | }); 578 | 579 | afterEach(function() { 580 | wrapper.unmount(); 581 | }); 582 | 583 | it('renders the new props', function() { 584 | assert.equal(stub.find('.name').text(), newName); 585 | assert.equal(stub.find('.age').text(), newAge); 586 | assert.equal(stub.find('.hungry').text(), 'not hungry'); 587 | }); 588 | 589 | it('listen for updates', function() { 590 | newName = 'Bob'; 591 | newUserModel.set('name', newName); 592 | 593 | assert.equal(stub.find('.name').text(), newName); 594 | }); 595 | }); 596 | 597 | describe('when passed props change to include', function() { 598 | let ConnectedTest; 599 | let createListenerSpy; 600 | let removeListenerSpy; 601 | 602 | beforeEach(function() { 603 | ConnectedTest = connectBackboneToReact(mapModelsToProps)(TestComponent); 604 | wrapper = mount(); 605 | 606 | createListenerSpy = sandbox.spy(ConnectedTest.prototype, 'createEventListener'); 607 | removeListenerSpy = sandbox.spy(ConnectedTest.prototype, 'removeEventListener'); 608 | 609 | const decoratorUserModel = new UserModel({ 610 | name: 'Robert', 611 | age: '30', 612 | hungry: false, 613 | }); 614 | 615 | const initialModelsMap = { 616 | user: userModel, 617 | coll: userCollection, 618 | decorator: decoratorUserModel, 619 | }; 620 | 621 | wrapper.setProps({ models: initialModelsMap }); 622 | }); 623 | 624 | afterEach(function() { 625 | wrapper.unmount(); 626 | }); 627 | 628 | it('calls createEventListener once due to decoratorUserModel being added as a model', function() { 629 | assert.equal(createListenerSpy.callCount, 1); 630 | assert.equal(createListenerSpy.firstCall.args[0], 'decorator'); 631 | }); 632 | 633 | it('does not call removeEventListener', function() { 634 | assert.equal(removeListenerSpy.callCount, 0); 635 | }); 636 | 637 | describe('an undefined model', function() { 638 | beforeEach(function() { 639 | const newModelsMap = { 640 | user: userModel, 641 | coll: userCollection, 642 | decorator: undefined, 643 | }; 644 | 645 | wrapper.setProps({ models: newModelsMap }); 646 | }); 647 | 648 | it('does not call createEventListener again', function() { 649 | assert.equal(createListenerSpy.callCount, 1); 650 | }); 651 | 652 | it('calls removeEventListener once for decoratorUserModel', function() { 653 | assert.equal(removeListenerSpy.callCount, 1); 654 | assert.equal(removeListenerSpy.firstCall.args[0], 'decorator'); 655 | }); 656 | }); 657 | }); 658 | 659 | describe('when unmounted in an event listener and subscribed to "all" event', function() { 660 | // To add more color, "all" event handlers are triggered after individual event handlers. 661 | // That is to say, if you trigger "foo" the sequence of event handlers called is: 662 | // "foo" -> all event handlers (which can include additional triggers) -> "all" -> event handlers. 663 | // When you .off('all') within an event handler Backbone reassigns the "all" array of handlers 664 | // such that when you get to triggering the "all" event handlers that array has not been updated. 665 | // This is the line that reassigns that array: https://github.com/jashkenas/backbone/blob/bd50e2e4a4af5c09bc490185aab215794d42258b/backbone.js#L296 666 | // So that when you get here https://github.com/jashkenas/backbone/blob/bd50e2e4a4af5c09bc490185aab215794d42258b/backbone.js#L357 667 | // the "allEvents" value is stale. 668 | 669 | const arbitraryEvent = 'arbitraryEvent'; 670 | let forceUpdateSpy; 671 | 672 | beforeEach(function() { 673 | // eslint-disable-next-line no-unused-vars 674 | const ConnectedTest = connectBackboneToReact(mapModelsToProps)(TestComponent); 675 | forceUpdateSpy = sandbox.spy(ConnectedTest.prototype, 'forceUpdate'); 676 | 677 | wrapper = mount(); 678 | 679 | // Subscribe to an arbitrary event. 680 | userModel.on(arbitraryEvent, function() { 681 | // When called it unmounts an component. 682 | wrapper.unmount(); 683 | 684 | // But because we're subscribed to the "all" event it will still trigger that handler, 685 | // calling forceUpdate when it shouldn't. 686 | }); 687 | 688 | // Trigger the event. 689 | userModel.trigger(arbitraryEvent); 690 | }); 691 | 692 | it('does not call forceUpdate', function() { 693 | assert.equal(forceUpdateSpy.called, false); 694 | }); 695 | }); 696 | 697 | describe('when NOT configured to provide ref to the wrapped component', function() { 698 | beforeEach(function() { 699 | // eslint-disable-next-line no-unused-vars 700 | const ConnectedTest = connectBackboneToReact(mapModelsToProps)(TestComponent); 701 | 702 | wrapper = mount(); 703 | stub = wrapper.find(TestComponent); 704 | }); 705 | 706 | afterEach(function() { 707 | wrapper.unmount(); 708 | }); 709 | 710 | it('should throw an error when getWrappedInstance() is called', function() { 711 | assert.throws(function() { 712 | wrapper.instance().getWrappedInstance(); 713 | }); 714 | }); 715 | }); 716 | 717 | describe('when configured to provide ref to the wrapped component', function() { 718 | beforeEach(function() { 719 | // eslint-disable-next-line no-unused-vars 720 | const ConnectedTest = connectBackboneToReact( 721 | mapModelsToProps, 722 | { withRef: true } 723 | )(TestComponent); 724 | 725 | wrapper = mount(); 726 | stub = wrapper.find(TestComponent); 727 | }); 728 | 729 | afterEach(function() { 730 | wrapper.unmount(); 731 | }); 732 | 733 | it('should return the wrapped component via getWrappedInstance()', function() { 734 | assert.equal(wrapper.instance().getWrappedInstance(), stub.instance()); 735 | }); 736 | 737 | describe('and the returned wrapped component', function() { 738 | let randomName; 739 | 740 | beforeEach(function() { 741 | randomName = Math.random().toString(); 742 | }); 743 | 744 | it('should be able to update the actual component', function() { 745 | wrapper.instance().getWrappedInstance().props.changeName(randomName); 746 | assert.equal(stub.instance().props.name, randomName); 747 | }); 748 | 749 | it('should reflect the changes made to the actual component', function() { 750 | stub.instance().props.changeName(randomName); 751 | assert.equal(wrapper.instance().getWrappedInstance().props.name, randomName); 752 | }); 753 | 754 | it('should reflect the changes made to the data model', function() { 755 | userModel.set('name', randomName); 756 | assert.equal(wrapper.instance().getWrappedInstance().props.name, randomName); 757 | }); 758 | }); 759 | }); 760 | }); 761 | --------------------------------------------------------------------------------