├── .eslintignore ├── .gitignore ├── .npmignore ├── .travis.yml ├── CONTRIBUTING.md ├── src ├── load-child-mixin.js ├── entry.js ├── load-child-component.js ├── serialize.js ├── render.js └── load-child.js ├── .eslintrc ├── karma.conf.js ├── tests ├── unit │ ├── load-missing-child.js │ ├── load-child-mixin.js │ ├── load-child-component.js │ ├── render.js │ ├── serialize.js │ ├── render-inject-state.js │ └── load-child.js ├── bind-polyfill.js └── integration │ ├── render-stateless.js │ ├── render.js │ └── render-inject-state.js ├── LICENSE ├── package.json └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | tests/coverage 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | tests/coverage/ 3 | dist/ 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | tests/coverage/ 3 | src/ 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | after_success: 5 | - npm run coveralls 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | This repository follows the [Cosmos Contributing Guide.](https://github.com/skidding/cosmos/blob/master/CONTRIBUTING.md) 2 | -------------------------------------------------------------------------------- /src/load-child-mixin.js: -------------------------------------------------------------------------------- 1 | var loadChild = require('./load-child.js'); 2 | 3 | module.exports = { 4 | loadChild: function(childName, a, b, c, d, e, f) { 5 | return loadChild.loadChild(this, childName, a, b, c, d, e, f); 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /src/entry.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | Mixin: require('./load-child-mixin.js'), 3 | Component: require('./load-child-component.js'), 4 | loadChild: require('./load-child.js'), 5 | serialize: require('./serialize.js').serialize, 6 | render: require('./render.js').render, 7 | injectState: require('./render.js').injectState 8 | }; 9 | -------------------------------------------------------------------------------- /src/load-child-component.js: -------------------------------------------------------------------------------- 1 | var React = require('react'), 2 | loadChild = require('./load-child.js'); 3 | 4 | class LoadChildComponent extends React.Component { 5 | loadChild(childName, a, b, c, d, e, f) { 6 | return loadChild.loadChild(this, childName, a, b, c, d, e, f); 7 | } 8 | } 9 | 10 | module.exports = LoadChildComponent; 11 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "ecmaFeatures": { 3 | "jsx": true, 4 | "modules": true 5 | }, 6 | "env": { 7 | "browser": true, 8 | "node": true 9 | }, 10 | "parser": "babel-eslint", 11 | "rules": { 12 | "quotes": [2, "single"], 13 | "strict": [2, "never"], 14 | "react/jsx-uses-react": 2, 15 | "react/jsx-uses-vars": 2, 16 | "react/react-in-jsx-scope": 2 17 | }, 18 | "plugins": [ 19 | "react" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.set({ 3 | basePath: 'tests/', 4 | browsers: ['PhantomJS'], 5 | coverageReporter: { 6 | type: 'lcov', 7 | dir: 'coverage/' 8 | }, 9 | files: [ 10 | 'bind-polyfill.js', 11 | 'unit/*.js', 12 | 'integration/*.js' 13 | ], 14 | frameworks: ['mocha', 'chai', 'sinon-chai'], 15 | preprocessors: { 16 | '**/*.js': ['webpack'] 17 | }, 18 | reporters: ['mocha', 'coverage'], 19 | webpack: { 20 | module: { 21 | loaders: [{ 22 | test: /\.js$/, 23 | exclude: /node_modules/, 24 | loader: 'babel-loader' 25 | }], 26 | postLoaders: [{ 27 | test: /\.js$/, 28 | exclude: /(node_modules|tests)\//, 29 | loader: 'istanbul-instrumenter' 30 | }] 31 | } 32 | }, 33 | webpackMiddleware: { 34 | noInfo: true 35 | } 36 | }); 37 | }; 38 | -------------------------------------------------------------------------------- /tests/unit/load-missing-child.js: -------------------------------------------------------------------------------- 1 | var React = require('react'), 2 | loadChild = require('../../src/load-child.js').loadChild; 3 | 4 | describe('UNIT Load missing child', function() { 5 | var component; 6 | 7 | beforeEach(function() { 8 | component = { 9 | children: { 10 | missingChild: function() { 11 | return {}; 12 | } 13 | } 14 | }; 15 | 16 | sinon.stub(React, 'createElement', function() { 17 | throw new Error('Invalid component'); 18 | }); 19 | 20 | sinon.stub(console, 'error'); 21 | }); 22 | 23 | afterEach(function() { 24 | React.createElement.restore(); 25 | 26 | console.error.restore(); 27 | }); 28 | 29 | it('should handle exception', function() { 30 | expect(function whereAreYouSon() { 31 | loadChild(component, 'missingChild'); 32 | }).to.not.throw(); 33 | }); 34 | 35 | it('should log error', function() { 36 | loadChild(component, 'missingChild'); 37 | 38 | expect(console.error.lastCall.args[0]).to.be.an.instanceof(Error); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Ovidiu Cherecheș 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/serialize.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | 3 | exports.serialize = function(component) { 4 | /** 5 | * Generate a snapshot with the props and state of a component combined, 6 | * including the state of all nested child components. 7 | * 8 | * @param {ReactComponent} component Rendered React component instance 9 | * 10 | * @returns {Object} Snapshot with component props and nested state 11 | */ 12 | var snapshot = _.clone(component.props), 13 | state = getComponentTreeState(component); 14 | 15 | if (!_.isEmpty(state)) { 16 | snapshot.state = state; 17 | } 18 | 19 | return snapshot; 20 | }; 21 | 22 | var getComponentTreeState = function(component) { 23 | var state = component.state ? _.clone(component.state) : {}, 24 | childrenStates = {}, 25 | childState; 26 | 27 | _.each(component.refs, function(child, ref) { 28 | childState = getComponentTreeState(child); 29 | 30 | if (!_.isEmpty(childState)) { 31 | childrenStates[ref] = childState; 32 | } 33 | }); 34 | 35 | if (!_.isEmpty(childrenStates)) { 36 | state.children = childrenStates; 37 | } 38 | 39 | return state; 40 | }; 41 | -------------------------------------------------------------------------------- /tests/bind-polyfill.js: -------------------------------------------------------------------------------- 1 | /** 2 | * PhantomJS doesn't support Function.prototype.bind natively, so 3 | * polyfill it whenever this module is required. 4 | */ 5 | 6 | (function() { 7 | var Ap = Array.prototype; 8 | var slice = Ap.slice; 9 | var Fp = Function.prototype; 10 | 11 | if (!Fp.bind) { 12 | Fp.bind = function(context) { 13 | var func = this; 14 | var args = slice.call(arguments, 1); 15 | 16 | function bound() { 17 | var invokedAsConstructor = func.prototype && (this instanceof func); 18 | return func.apply( 19 | // Ignore the context parameter when invoking the bound function 20 | // as a constructor. Note that this includes not only constructor 21 | // invocations using the new keyword but also calls to base class 22 | // constructors such as BaseClass.call(this, ...) or super(...). 23 | !invokedAsConstructor && context || this, 24 | args.concat(slice.call(arguments)) 25 | ); 26 | } 27 | 28 | // The bound function must share the .prototype of the unbound 29 | // function so that any object created by one constructor will count 30 | // as an instance of both constructors. 31 | bound.prototype = func.prototype; 32 | 33 | return bound; 34 | }; 35 | } 36 | })(); 37 | -------------------------------------------------------------------------------- /tests/integration/render-stateless.js: -------------------------------------------------------------------------------- 1 | var React = require('react'), 2 | ReactDOM = require('react-dom'), 3 | render = require('../../src/render.js').render; 4 | 5 | describe('INTEGRATION Render stateless component', function() { 6 | var domContainer, 7 | children = [React.createElement('span', { 8 | key: '1', 9 | children: 'test child' 10 | })]; 11 | 12 | var StatelessComponent = (props) => 13 |
{props.children ? props.children : props.foo}
; 14 | 15 | beforeEach(function() { 16 | domContainer = document.createElement('div'); 17 | }); 18 | 19 | afterEach(function() { 20 | ReactDOM.unmountComponentAtNode(domContainer); 21 | }); 22 | 23 | it('should render component using correct props', function() { 24 | render({ 25 | component: StatelessComponent, 26 | snapshot: { 27 | foo: 'bar' 28 | }, 29 | container: domContainer 30 | }); 31 | 32 | expect(domContainer.innerText).to.equal('bar'); 33 | }); 34 | 35 | it('should receive children through props', function() { 36 | render({ 37 | component: StatelessComponent, 38 | snapshot: { 39 | children: children 40 | }, 41 | container: domContainer 42 | }); 43 | 44 | expect(domContainer.innerText).to.equal('test child'); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /tests/integration/render.js: -------------------------------------------------------------------------------- 1 | var React = require('react'), 2 | ReactDOM = require('react-dom'), 3 | render = require('../../src/render.js').render; 4 | 5 | describe('INTEGRATION Render', function() { 6 | var domContainer, 7 | children = [React.createElement('span', { 8 | key: '1', 9 | children: 'test child' 10 | })], 11 | component; 12 | 13 | class Component extends React.Component { 14 | render() { 15 | return React.DOM.span(); 16 | } 17 | } 18 | 19 | beforeEach(function() { 20 | domContainer = document.createElement('div'); 21 | 22 | component = render({ 23 | component: Component, 24 | snapshot: { 25 | foo: 'bar', 26 | children: children 27 | }, 28 | container: domContainer 29 | }); 30 | }); 31 | 32 | afterEach(function() { 33 | ReactDOM.unmountComponentAtNode(domContainer); 34 | }); 35 | 36 | it('should create component with correct props', function() { 37 | expect(component.props.foo).to.equal('bar'); 38 | }); 39 | 40 | it('should receive children through props', function() { 41 | expect(component.props.children).to.be.equal(children); 42 | }); 43 | 44 | it('should render in given container', function() { 45 | expect(ReactDOM.findDOMNode(component).parentNode).to.equal(domContainer); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /tests/unit/load-child-mixin.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | React = require('react'), 3 | TestUtils = require('react-addons-test-utils'), 4 | renderIntoDocument = TestUtils.renderIntoDocument, 5 | loadChild = require('../../src/load-child.js'), 6 | LoadChildMixin = require('../../src/load-child-mixin.js'); 7 | 8 | describe('UNIT Load child mixin', function() { 9 | var fakeReactElement = {}, 10 | myComponent; 11 | 12 | var MyComponent = React.createClass({ 13 | mixins: [LoadChildMixin], 14 | children: {}, 15 | 16 | render: function() { 17 | return React.DOM.span(); 18 | } 19 | }); 20 | 21 | beforeEach(function() { 22 | sinon.stub(loadChild, 'loadChild').returns(fakeReactElement); 23 | 24 | myComponent = renderIntoDocument(React.createElement(MyComponent, {})); 25 | }); 26 | 27 | afterEach(function() { 28 | loadChild.loadChild.restore(); 29 | }); 30 | 31 | it('should call loadChild lib with same args', function() { 32 | myComponent.loadChild('myChild', 5, 10, true); 33 | 34 | var args = loadChild.loadChild.lastCall.args; 35 | expect(args[0]).to.equal(myComponent); 36 | expect(args[1]).to.equal('myChild'); 37 | expect(args[2]).to.equal(5); 38 | expect(args[3]).to.equal(10); 39 | expect(args[4]).to.equal(true); 40 | }); 41 | 42 | it('should return what loadChild lib returned', function() { 43 | expect(myComponent.loadChild()).to.equal(fakeReactElement); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /src/render.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | React = require('react'), 3 | ReactDOM = require('react-dom'); 4 | 5 | exports.render = function(options) { 6 | /** 7 | * Render a component and reproduce a state snapshot by recursively injecting 8 | * the nested state into the component tree it generates. 9 | * 10 | * @param {Object} options 11 | * @param {ReactClass} options.component 12 | * @param {Object} options.snapshot 13 | * @param {DOMElement} options.container 14 | * 15 | * @returns {ReactComponent} Reference to the rendered component 16 | */ 17 | var props = _.omit(options.snapshot, 'state', 'children'), 18 | state = options.snapshot.state, 19 | children = options.snapshot.children; 20 | 21 | var element = React.createElement(options.component, props, children), 22 | component = ReactDOM.render(element, options.container); 23 | 24 | if (!_.isEmpty(state)) { 25 | exports.injectState(component, state); 26 | } 27 | 28 | return component; 29 | }; 30 | 31 | exports.injectState = function(component, state) { 32 | var rootState = _.omit(state, 'children'), 33 | childrenStates = state.children; 34 | 35 | component.setState(rootState, function() { 36 | if (_.isEmpty(childrenStates)) { 37 | return; 38 | } 39 | 40 | _.each(component.refs, function(child, ref) { 41 | if (!_.isEmpty(childrenStates[ref])) { 42 | exports.injectState(child, childrenStates[ref]); 43 | } 44 | }); 45 | }); 46 | }; 47 | -------------------------------------------------------------------------------- /tests/unit/load-child-component.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | React = require('react'), 3 | TestUtils = require('react-addons-test-utils'), 4 | renderIntoDocument = TestUtils.renderIntoDocument, 5 | loadChild = require('../../src/load-child.js'), 6 | LoadChildComponent = require('../../src/load-child-component.js'); 7 | 8 | describe('UNIT Load child component', function() { 9 | var fakeReactElement = {}, 10 | myComponent; 11 | 12 | class MyComponent extends LoadChildComponent { 13 | constructor(props) { 14 | super(props); 15 | this.children = {}; 16 | } 17 | render() { 18 | return React.DOM.span(); 19 | } 20 | } 21 | 22 | beforeEach(function() { 23 | sinon.stub(loadChild, 'loadChild').returns(fakeReactElement); 24 | 25 | myComponent = renderIntoDocument(React.createElement(MyComponent, {})); 26 | }); 27 | 28 | afterEach(function() { 29 | loadChild.loadChild.restore(); 30 | }); 31 | 32 | it('should call loadChild lib with same args', function() { 33 | myComponent.loadChild('myChild', 5, 10, true); 34 | 35 | var args = loadChild.loadChild.lastCall.args; 36 | expect(args[0]).to.equal(myComponent); 37 | expect(args[1]).to.equal('myChild'); 38 | expect(args[2]).to.equal(5); 39 | expect(args[3]).to.equal(10); 40 | expect(args[4]).to.equal(true); 41 | }); 42 | 43 | it('should return what loadChild lib returned', function() { 44 | expect(myComponent.loadChild()).to.equal(fakeReactElement); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-component-tree", 3 | "version": "0.4.0", 4 | "description": "Serialize and reproduce the state of an entire tree of React components", 5 | "main": "dist/entry.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/skidding/react-component-tree.git" 9 | }, 10 | "license": "MIT", 11 | "dependencies": { 12 | "lodash": "^3.6.0" 13 | }, 14 | "devDependencies": { 15 | "babel": "^5.8.23", 16 | "babel-core": "^5.0.12", 17 | "babel-eslint": "^4.1.5", 18 | "babel-loader": "^5.0.0", 19 | "chai": "^2.2.0", 20 | "coveralls": "^2.11.2", 21 | "eslint": "^1.9.0", 22 | "eslint-plugin-react": "^3.8.0", 23 | "istanbul": "^0.3.13", 24 | "istanbul-instrumenter-loader": "^0.1.2", 25 | "karma": "^0.13.9", 26 | "karma-chai": "^0.1.0", 27 | "karma-cli": "0.1.1", 28 | "karma-coverage": "^0.2.7", 29 | "karma-mocha": "^0.1.10", 30 | "karma-mocha-reporter": "^1.0.2", 31 | "karma-phantomjs-launcher": "^0.1.4", 32 | "karma-sinon-chai": "^0.3.0", 33 | "karma-webpack": "^1.7.0", 34 | "mocha": "^2.2.4", 35 | "react": "^15.1.0", 36 | "react-addons-test-utils": "^15.1.0", 37 | "react-dom": "^15.1.0", 38 | "sinon": "^1.14.1", 39 | "sinon-chai": "^2.7.0", 40 | "webpack": "^1.12.0" 41 | }, 42 | "peerDependencies": { 43 | "react": "^0.14.0 || ^15.0.0-0", 44 | "react-dom": "^0.14.0 || ^15.0.0-0" 45 | }, 46 | "scripts": { 47 | "pretest": "npm run lint", 48 | "lint": "eslint .", 49 | "test": "karma start --single-run", 50 | "coveralls": "cat tests/coverage/*/lcov.info | node_modules/coveralls/bin/coveralls.js", 51 | "compile": "babel -d dist/ src/", 52 | "prepublish": "rm -rf dist && npm run compile" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/unit/render.js: -------------------------------------------------------------------------------- 1 | var React = require('react'), 2 | ReactDOM = require('react-dom'), 3 | render = require('../../src/render.js').render; 4 | 5 | describe('UNIT Render', function() { 6 | var domContainer, 7 | children = [React.createElement('span', { 8 | key: '1', 9 | children: 'test child' 10 | })]; 11 | 12 | class Component extends React.Component { 13 | render() { 14 | return React.DOM.span(); 15 | } 16 | } 17 | 18 | beforeEach(function() { 19 | sinon.spy(React, 'createElement'); 20 | sinon.stub(ReactDOM, 'render'); 21 | 22 | domContainer = document.createElement('div'); 23 | 24 | render({ 25 | component: Component, 26 | snapshot: { 27 | foo: 'bar', 28 | children: children 29 | }, 30 | container: domContainer 31 | }); 32 | }); 33 | 34 | afterEach(function() { 35 | React.createElement.restore(); 36 | ReactDOM.render.restore(); 37 | }); 38 | 39 | it('should create element for component', function() { 40 | var args = React.createElement.lastCall.args; 41 | expect(args[0]).to.equal(Component); 42 | }); 43 | 44 | it('should create element with props', function() { 45 | var args = React.createElement.lastCall.args; 46 | expect(args[1].foo).to.equal('bar'); 47 | }); 48 | 49 | it('should omit children from props', function() { 50 | var args = React.createElement.lastCall.args; 51 | expect(args[1].children).to.be.undefined; 52 | }); 53 | 54 | it('should create element with children', function() { 55 | var args = React.createElement.lastCall.args; 56 | expect(args[2]).to.be.equal(children); 57 | }); 58 | 59 | it('should render created element', function() { 60 | var args = ReactDOM.render.lastCall.args; 61 | expect(args[0]).to.equal(React.createElement.returnValues[0]); 62 | }); 63 | 64 | it('should render in given container', function() { 65 | var args = ReactDOM.render.lastCall.args; 66 | expect(args[1]).to.equal(domContainer); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /tests/integration/render-inject-state.js: -------------------------------------------------------------------------------- 1 | var React = require('react'), 2 | ReactDOM = require('react-dom'), 3 | render = require('../../src/render.js').render; 4 | 5 | describe('INTEGRATION Render and inject state', function() { 6 | var domContainer, 7 | component; 8 | 9 | class ChildComponent extends React.Component { 10 | render() { 11 | return React.DOM.span(); 12 | } 13 | } 14 | 15 | class ParentComponent extends React.Component { 16 | render() { 17 | return React.createElement(ChildComponent, {ref: 'child'}); 18 | } 19 | } 20 | 21 | class GrandparentComponent extends React.Component { 22 | render() { 23 | return React.createElement(ParentComponent, {ref: 'child'}); 24 | } 25 | } 26 | 27 | beforeEach(function() { 28 | domContainer = document.createElement('div'); 29 | }); 30 | 31 | afterEach(function() { 32 | ReactDOM.unmountComponentAtNode(domContainer); 33 | }); 34 | 35 | it('should set state on root component', function() { 36 | component = render({ 37 | component: GrandparentComponent, 38 | snapshot: { 39 | state: {foo: 'bar'} 40 | }, 41 | container: domContainer 42 | }); 43 | 44 | expect(component.state.foo).to.equal('bar'); 45 | }); 46 | 47 | it('should set state on child component', function() { 48 | component = render({ 49 | component: GrandparentComponent, 50 | snapshot: { 51 | state: { 52 | children: { 53 | child: {foo: 'bar'} 54 | } 55 | } 56 | }, 57 | container: domContainer 58 | }); 59 | 60 | expect(component.refs.child.state.foo).to.equal('bar'); 61 | }); 62 | 63 | it('should set state on grandchild component', function() { 64 | component = render({ 65 | component: GrandparentComponent, 66 | snapshot: { 67 | state: { 68 | children: { 69 | child: { 70 | children: { 71 | child: {foo: 'bar'} 72 | } 73 | } 74 | } 75 | } 76 | }, 77 | container: domContainer 78 | }); 79 | 80 | expect(component.refs.child.refs.child.state.foo).to.equal('bar'); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /tests/unit/serialize.js: -------------------------------------------------------------------------------- 1 | var React = require('react'), 2 | TestUtils = require('react-addons-test-utils'), 3 | renderIntoDocument = TestUtils.renderIntoDocument, 4 | serialize = require('../../src/serialize.js').serialize; 5 | 6 | describe('UNIT Serialize', function() { 7 | class ChildComponent extends React.Component { 8 | render() { 9 | return React.DOM.span(); 10 | } 11 | } 12 | 13 | class ParentComponent extends React.Component { 14 | render() { 15 | return React.createElement(ChildComponent, {ref: 'child'}); 16 | } 17 | } 18 | 19 | class GrandparentComponent extends React.Component { 20 | render() { 21 | return React.createElement(ParentComponent, {ref: 'child'}); 22 | } 23 | } 24 | 25 | var render = function(Component, props) { 26 | return renderIntoDocument(React.createElement(Component, props)); 27 | } 28 | 29 | it('should extract component props', function() { 30 | var component = render(ChildComponent, {foo: 'bar'}); 31 | 32 | expect(serialize(component).foo).to.equal('bar'); 33 | }); 34 | 35 | it('should extract component state', function() { 36 | var component = render(ChildComponent); 37 | component.setState({foo: 'bar'}); 38 | 39 | expect(serialize(component).state.foo).to.equal('bar'); 40 | }); 41 | 42 | it('should not add .state key on empty state', function() { 43 | var component = render(ChildComponent, {foo: 'bar'}); 44 | 45 | expect(serialize(component).state).to.be.undefined; 46 | }); 47 | 48 | it('should extract child component state', function() { 49 | var component = render(ParentComponent); 50 | component.refs.child.setState({foo: 'bar'}); 51 | 52 | expect(serialize(component).state.children.child.foo).to.equal('bar'); 53 | }); 54 | 55 | it('should not add .children key on children with empty state', function() { 56 | var component = render(ParentComponent); 57 | component.setState({foo: 'bar'}); 58 | 59 | expect(serialize(component).state.children).to.be.undefined; 60 | }); 61 | 62 | it('should extract child state recursively', function() { 63 | var component = render(GrandparentComponent); 64 | component.refs.child.refs.child.setState({foo: 'bar'}); 65 | 66 | expect(serialize(component).state.children.child.children.child.foo) 67 | .to.equal('bar'); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > ## Deprecated 2 | > This package has been merged into the [React Cosmos](https://github.com/react-cosmos/react-cosmos) [monorepo](https://github.com/react-cosmos/react-cosmos/tree/master/packages). 3 | 4 | # React Component Tree [![Build Status](https://travis-ci.org/skidding/react-component-tree.svg?branch=master)](https://travis-ci.org/skidding/react-component-tree) [![Coverage Status](https://coveralls.io/repos/skidding/react-component-tree/badge.svg?branch=master)](https://coveralls.io/r/skidding/react-component-tree?branch=master) 5 | 6 | Serialize and reproduce the state of an entire tree of React components. 7 | 8 | A few examples where this can be useful: 9 | - Using fixtures to load and test components in multiple supported states 10 | - Extracting the app state when an error occurs in the page and reproducing 11 | that exact state later on when debugging 12 | - "Pausing" the app state and resuming it later (nice for games) 13 | 14 | React compatibility: 15 | - `react-component-tree@0.2` with `react@0.13` and below 16 | - `react-component-tree@0.4` with `react@0.14` and above 17 | 18 | ## ComponentTree.serialize 19 | 20 | Generate a snapshot with the props and state of a component combined, including 21 | the state of all nested child components. 22 | 23 | ```js 24 | var ComponentTree = require('react-component-tree'); 25 | 26 | myCompany.setProps({public: true}); 27 | myCompany.setState({profitable: true}); 28 | myCompany.refs.employee54.setState({bored: false}); 29 | 30 | var snapshot = ComponentTree.serialize(myCompany); 31 | ``` 32 | 33 | The snapshot looks like this: 34 | ```js 35 | { 36 | public: true, 37 | state: { 38 | profitable: true, 39 | children: { 40 | employee54: { 41 | bored: false 42 | } 43 | } 44 | }, 45 | } 46 | ``` 47 | 48 | ## ComponentTree.render 49 | 50 | Render a component and reproduce a state snapshot by recursively injecting the 51 | nested state into the component tree it generates. 52 | 53 | ```js 54 | var myOtherCompany = ComponentTree.render({ 55 | component: CompanyClass, 56 | snapshot: snapshot, 57 | container: document.getElementById('content') 58 | }); 59 | 60 | console.log(myOtherCompany.props.public); // returns true 61 | console.log(myOtherCompany.state.profitable); // returns true 62 | console.log(myOtherCompany.refs.employee54.state.bored); // returns false 63 | ``` 64 | -------------------------------------------------------------------------------- /tests/unit/render-inject-state.js: -------------------------------------------------------------------------------- 1 | var React = require('react'), 2 | ReactDOM = require('react-dom'), 3 | TestUtils = require('react-addons-test-utils'), 4 | renderIntoDocument = TestUtils.renderIntoDocument, 5 | render = require('../../src/render.js').render; 6 | 7 | describe('UNIT Render and inject state', function() { 8 | var component; 9 | 10 | class ChildComponent extends React.Component { 11 | render() { 12 | return React.DOM.span(); 13 | } 14 | } 15 | 16 | class ParentComponent extends React.Component { 17 | render() { 18 | return React.createElement(ChildComponent, {ref: 'child'}); 19 | } 20 | } 21 | 22 | class GrandparentComponent extends React.Component { 23 | render() { 24 | return React.createElement(ParentComponent, {ref: 'child'}); 25 | } 26 | } 27 | 28 | beforeEach(function() { 29 | component = renderIntoDocument(React.createElement(GrandparentComponent)); 30 | 31 | sinon.spy(component, 'setState'); 32 | sinon.spy(component.refs.child, 'setState'); 33 | sinon.spy(component.refs.child.refs.child, 'setState'); 34 | 35 | sinon.stub(ReactDOM, 'render').returns(component); 36 | }); 37 | 38 | afterEach(function() { 39 | ReactDOM.render.restore(); 40 | }); 41 | 42 | it('should set state on root component', function() { 43 | render({ 44 | component: GrandparentComponent, 45 | snapshot: { 46 | state: {foo: 'bar'} 47 | } 48 | }); 49 | 50 | var stateSet = component.setState.lastCall.args[0]; 51 | expect(stateSet.foo).to.equal('bar'); 52 | }); 53 | 54 | it('should set state on child component', function() { 55 | render({ 56 | component: GrandparentComponent, 57 | snapshot: { 58 | state: { 59 | children: { 60 | child: {foo: 'bar'} 61 | } 62 | } 63 | } 64 | }); 65 | 66 | var stateSet = component.refs.child.setState.lastCall.args[0]; 67 | expect(stateSet.foo).to.equal('bar'); 68 | }); 69 | 70 | it('should set state on grandchild component', function() { 71 | render({ 72 | component: GrandparentComponent, 73 | snapshot: { 74 | state: { 75 | children: { 76 | child: { 77 | children: { 78 | child: {foo: 'bar'} 79 | } 80 | } 81 | } 82 | } 83 | } 84 | }); 85 | 86 | var stateSet = component.refs.child.refs.child.setState.lastCall.args[0]; 87 | expect(stateSet.foo).to.equal('bar'); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /src/load-child.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | React = require('react'); 3 | 4 | exports.loadChild = function(component, childName, a, b, c, d, e, f) { 5 | /** 6 | * Create a React element for a specific child type. 7 | * 8 | * The component class and props of the child are returned together by a 9 | * corresponding function from the .children object. The functions are 10 | * mapped with the name of the child as the key. 11 | * 12 | * https://facebook.github.io/react/docs/top-level-api.html#react.createelement 13 | * 14 | * @param {Object} component Parent component 15 | * @param {Object} component.children Map of functions that generate child 16 | * component params (component+props) 17 | * @param {String} name Key that corresponds to the child component we want 18 | * to get the params for 19 | * @param {...*} [arguments] Optional extra arguments get passed to the 20 | * component .children function 21 | * 22 | * @returns {ReactElement} Created React element 23 | * 24 | * @example 25 | * loadChild({ 26 | * profileBadge: function(user) { 27 | * return { 28 | * component: require('./components/ProfileBadge.jsx'), 29 | * key: user.id, 30 | * name: user.name, 31 | * showAvatar: true 32 | * }; 33 | * } 34 | * }, 'profileBadge', {id: 3, name: 'John'}); 35 | * // will call 36 | * React.createElement(ProfileBadge, { 37 | * key: 3, 38 | * name: 'John', 39 | * showAvatar: true 40 | * }); 41 | */ 42 | var params = getChildParams.call( 43 | this, component, childName, a, b, c, d, e, f); 44 | 45 | // One child with bad params shouldn't block the entire app 46 | try { 47 | return React.createElement(params.component, 48 | _.omit(params, 'component', 'children'), 49 | params.children); 50 | } catch (e) { 51 | console.error(e); 52 | } 53 | }; 54 | 55 | var getChildParams = function(component, childName, a, b, c, d, e, f) { 56 | var params = component.children[childName].call(component, a, b, c, d, e, f); 57 | 58 | // Default the child ref to its key name if the child template doesn't return 59 | // a value 60 | if (!params.ref && isClassComponent(params.component)) { 61 | params.ref = childName; 62 | } 63 | 64 | return params; 65 | }; 66 | 67 | var isClassComponent = function(Component) { 68 | // Inspired from Recompose: http://bit.ly/1NCac7D 69 | return Component && 70 | Component.prototype && 71 | // Only evidence this is reliable: http://bit.ly/1MQPRyU 72 | typeof(Component.prototype.isReactComponent) === 'object'; 73 | }; 74 | -------------------------------------------------------------------------------- /tests/unit/load-child.js: -------------------------------------------------------------------------------- 1 | var React = require('react'), 2 | _ = require('lodash'), 3 | loadChild = require('../../src/load-child.js').loadChild; 4 | 5 | var ReactComponent = { 6 | prototype: { 7 | isReactComponent: {} 8 | } 9 | }; 10 | var StatelessComponent = { 11 | prototype: {} 12 | }; 13 | 14 | describe('UNIT Load child', function() { 15 | var FirstComponent = _.cloneDeep(ReactComponent), 16 | SecondComponent =_.cloneDeep(ReactComponent), 17 | ThirdComponent =_.cloneDeep(StatelessComponent), 18 | component, 19 | children = [React.createElement('span', { 20 | key: '1', 21 | children: 'test child' 22 | })]; 23 | 24 | beforeEach(function() { 25 | component = { 26 | children: { 27 | defaultRef: sinon.spy(function() { 28 | return { 29 | component: FirstComponent, 30 | alwaysTrue: true, 31 | children: children 32 | }; 33 | }), 34 | customRef: sinon.spy(function() { 35 | return { 36 | component: SecondComponent, 37 | ref: 'fooChild' 38 | }; 39 | }), 40 | omittedRef: function() { 41 | return { 42 | component: ThirdComponent 43 | }; 44 | } 45 | } 46 | }; 47 | 48 | sinon.stub(React, 'createElement'); 49 | }); 50 | 51 | afterEach(function() { 52 | React.createElement.restore(); 53 | }); 54 | 55 | it('should call .children function', function() { 56 | loadChild(component, 'defaultRef'); 57 | 58 | expect(component.children.defaultRef).to.have.been.called; 59 | }); 60 | 61 | it('should call .children function with component context', function() { 62 | loadChild(component, 'defaultRef'); 63 | 64 | expect(component.children.defaultRef).to.have.been.calledOn(component); 65 | }); 66 | 67 | it('should call .children function with extra args', function() { 68 | loadChild(component, 'customRef', 'first', 'second'); 69 | 70 | expect(component.children.customRef) 71 | .to.have.been.calledWith('first', 'second'); 72 | }); 73 | 74 | it('should create element using returned component class', function() { 75 | loadChild(component, 'defaultRef'); 76 | 77 | expect(React.createElement.lastCall.args[0]).to.equal(FirstComponent); 78 | }); 79 | 80 | it('should create element using returned props', function() { 81 | loadChild(component, 'defaultRef'); 82 | 83 | var props = React.createElement.lastCall.args[1]; 84 | expect(props.alwaysTrue).to.equal(true); 85 | }); 86 | 87 | it('should omit component param from props', function() { 88 | loadChild(component, 'defaultRef'); 89 | 90 | var props = React.createElement.lastCall.args[1]; 91 | expect(props.component).to.be.undefined; 92 | }); 93 | 94 | it('should omit children param from props', function() { 95 | loadChild(component, 'defaultRef'); 96 | 97 | var props = React.createElement.lastCall.args[1]; 98 | expect(props.children).to.be.undefined; 99 | }); 100 | 101 | it('should create element using returned children', function() { 102 | loadChild(component, 'defaultRef'); 103 | 104 | expect(React.createElement.lastCall.args[2]).to.equal(children); 105 | }); 106 | 107 | it('should use child name as ref if omitted', function() { 108 | loadChild(component, 'defaultRef'); 109 | 110 | var props = React.createElement.lastCall.args[1]; 111 | expect(props.ref).to.equal('defaultRef'); 112 | }); 113 | 114 | it('should use returned ref when present', function() { 115 | loadChild(component, 'customRef'); 116 | 117 | var props = React.createElement.lastCall.args[1]; 118 | expect(props.ref).to.equal('fooChild'); 119 | }); 120 | 121 | it('should omit ref for stateless components', function() { 122 | loadChild(component, 'omittedRef'); 123 | 124 | var props = React.createElement.lastCall.args[1]; 125 | expect(props.ref).to.be.undefined; 126 | }); 127 | }); 128 | --------------------------------------------------------------------------------