From 4806266eb2447faad1d5aaabc8a519511f5c959f Mon Sep 17 00:00:00 2001 From: Ovidiu Chereches Date: Mon, 13 Apr 2015 14:37:11 +0300 Subject: [PATCH 1/7] Add render method #4 --- entry.js | 3 +- src/render.js | 44 ++++++++++++++++++ tests/render-inject-state.js | 87 ++++++++++++++++++++++++++++++++++++ tests/render.js | 68 ++++++++++++++++++++++++++++ 4 files changed, 201 insertions(+), 1 deletion(-) create mode 100644 src/render.js create mode 100644 tests/render-inject-state.js create mode 100644 tests/render.js diff --git a/entry.js b/entry.js index afd1257..6227284 100644 --- a/entry.js +++ b/entry.js @@ -1,5 +1,6 @@ module.exports = { Mixin: require('./src/load-child-mixin.js'), Component: require('./src/load-child-component.js'), - serialize: require('./src/serialize.js').serialize + serialize: require('./src/serialize.js').serialize, + render: require('./src/render.js').render }; diff --git a/src/render.js b/src/render.js new file mode 100644 index 0000000..c1d3d55 --- /dev/null +++ b/src/render.js @@ -0,0 +1,44 @@ +var _ = require('lodash'), + React = require('react'); + +exports.render = function(options) { + /** + * Render a component and reproduce a state snapshot by recursively injecting + * the nested state into the component tree it generates. + * + * @param {Object} options + * @param {ReactClass} options.component + * @param {Object} options.snapshot + * @param {DOMElement} options.container + * + * @returns {ReactComponent} Reference to the rendered component + */ + var props = _.omit(options.snapshot, 'state'), + state = options.snapshot.state; + + var element = React.createElement(options.component, props), + component = React.render(element, options.container); + + if (!_.isEmpty(state)) { + injectState(component, state); + } + + return component; +}; + +var injectState = function(component, state) { + var rootState = _.omit(state, 'children'), + childrenStates = state.children; + + component.setState(rootState); + + if (_.isEmpty(childrenStates)) { + return; + } + + _.each(component.refs, function(child, ref) { + if (!_.isEmpty(childrenStates[ref])) { + injectState(child, childrenStates[ref]); + } + }); +}; diff --git a/tests/render-inject-state.js b/tests/render-inject-state.js new file mode 100644 index 0000000..1f624e3 --- /dev/null +++ b/tests/render-inject-state.js @@ -0,0 +1,87 @@ +var React = require('react/addons'), + renderIntoDocument = React.addons.TestUtils.renderIntoDocument, + render = require('../src/render.js').render; + +describe('Render and inject state', function() { + var component; + + class ChildComponent extends React.Component { + render() { + return React.DOM.span(); + } + } + + class ParentComponent extends React.Component { + render() { + return React.createElement(ChildComponent, {ref: 'child'}); + } + } + + class GrandparentComponent extends React.Component { + render() { + return React.createElement(ParentComponent, {ref: 'child'}); + } + } + + beforeEach(function() { + component = renderIntoDocument(React.createElement(GrandparentComponent)); + + sinon.spy(component, 'setState'); + sinon.spy(component.refs.child, 'setState'); + sinon.spy(component.refs.child.refs.child, 'setState'); + + sinon.stub(React, 'render').returns(component); + }); + + afterEach(function() { + React.render.restore(); + }); + + it('should set state on root component', function() { + render({ + component: GrandparentComponent, + snapshot: { + state: {foo: 'bar'} + } + }); + + var stateSet = component.setState.lastCall.args[0]; + expect(stateSet.foo).to.equal('bar'); + }); + + it('should set state on child component', function() { + render({ + component: GrandparentComponent, + snapshot: { + state: { + children: { + child: {foo: 'bar'} + } + } + } + }); + + var stateSet = component.refs.child.setState.lastCall.args[0]; + expect(stateSet.foo).to.equal('bar'); + }); + + it('should set state on grandchild component', function() { + render({ + component: GrandparentComponent, + snapshot: { + state: { + children: { + child: { + children: { + child: {foo: 'bar'} + } + } + } + } + } + }); + + var stateSet = component.refs.child.refs.child.setState.lastCall.args[0]; + expect(stateSet.foo).to.equal('bar'); + }); +}); diff --git a/tests/render.js b/tests/render.js new file mode 100644 index 0000000..60f2a7f --- /dev/null +++ b/tests/render.js @@ -0,0 +1,68 @@ +var React = require('react'), + render = require('../src/render.js').render; + +describe('Render', function() { + var domContainer; + + class Component extends React.Component { + render() { + return React.DOM.span(); + } + } + + beforeEach(function() { + sinon.spy(React, 'createElement'); + sinon.stub(React, 'render'); + + domContainer = document.createElement('div'); + }); + + afterEach(function() { + React.createElement.restore(); + React.render.restore(); + }); + + it('should create element for component', function() { + render({ + component: Component, + snapshot: {foo: 'bar'}, + container: domContainer + }); + + var args = React.createElement.lastCall.args; + expect(args[0]).to.equal(Component); + }); + + it('should create element with props', function() { + render({ + component: Component, + snapshot: {foo: 'bar'}, + container: domContainer + }); + + var args = React.createElement.lastCall.args; + expect(args[1].foo).to.equal('bar'); + }); + + it('should render created element', function() { + render({ + component: Component, + snapshot: {foo: 'bar'}, + container: domContainer + }); + + var args = React.render.lastCall.args; + expect(args[0]).to.equal(React.createElement.returnValues[0]); + }); + + it('should render in given container', function() { + render({ + component: Component, + snapshot: {foo: 'bar'}, + container: domContainer + }); + + var args = React.render.lastCall.args; + expect(args[1]).to.equal(domContainer); + }); +}); From 5af55e7ac2a5f0700e314dbb8fa4cbf67d1a0dbe Mon Sep 17 00:00:00 2001 From: Ovidiu Chereches Date: Mon, 13 Apr 2015 14:37:24 +0300 Subject: [PATCH 2/7] Add docstring to serialize method #4 --- src/serialize.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/serialize.js b/src/serialize.js index 71c12dd..a6d162c 100644 --- a/src/serialize.js +++ b/src/serialize.js @@ -4,6 +4,10 @@ exports.serialize = function(component) { /** * Generate a snapshot with the the props and state of a component combined, * including the state of all nested child components. + * + * @param {ReactComponent} component Rendered React component instance + * + * @returns {Object} Snapshot with component props and nested state */ var snapshot = _.clone(component.props), state = getComponentTreeState(component); From f1ff8e65f623933a741214181f2b18c01b342c31 Mon Sep 17 00:00:00 2001 From: Ovidiu Chereches Date: Mon, 13 Apr 2015 14:48:59 +0300 Subject: [PATCH 3/7] Fix typo #4 --- src/serialize.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/serialize.js b/src/serialize.js index a6d162c..7462703 100644 --- a/src/serialize.js +++ b/src/serialize.js @@ -2,7 +2,7 @@ var _ = require('lodash'); exports.serialize = function(component) { /** - * Generate a snapshot with the the props and state of a component combined, + * Generate a snapshot with the props and state of a component combined, * including the state of all nested child components. * * @param {ReactComponent} component Rendered React component instance From 81fec4ab71a82760e634ec465d5e6fa0604c2084 Mon Sep 17 00:00:00 2001 From: Ovidiu Chereches Date: Mon, 13 Apr 2015 14:57:03 +0300 Subject: [PATCH 4/7] Document serialize and render methods #4 --- README.md | 53 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 641deb8..e28acc8 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,54 @@ -# 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) +# React ComponentTree [![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) Serialize and reproduce the state of an entire tree of React components. + +A few examples where this can be useful: +- Using fixtures to load and test components in multiple supported states +- Extracting the app state when an error occurs in the page and reproducing +that exact state later on when debugging +- "Pausing" the app state and resuming it later (nice for games) + +## ComponentTree.serialize + +Generate a snapshot with the props and state of a component combined, including +the state of all nested child components. + +```js +myCompany.setProps({public: true}); +myCompany.setState({profitable: true}); +myCompany.refs.employee54.setState({bored: false}); + +var snapshot = ComponentTree.serialize(myCompany); +``` + +The snapshot looks like this: +```js +{ + public: true, + state: { + profitable: true + }, + children: { + employee54: { + bored: false + } + } +} +``` + +## ComponentTree.render + +Render a component and reproduce a state snapshot by recursively injecting the +nested state into the component tree it generates. + +```js +var myOtherCompany = ComponentTree.render({ + component: CompanyClass, + snapshot: snapshot, + container: document.getElementById('content') +}); + +console.log(myOtherCompany.props.public); // returns true +console.log(myOtherCompany.state.profitable); // returns true +console.log(myOtherCompany.refs.employee54.state.bored); // returns false +``` From 505e34c20ac4203e3ee7824c9a6132f2361a66f7 Mon Sep 17 00:00:00 2001 From: Ovidiu Chereches Date: Mon, 13 Apr 2015 15:40:28 +0300 Subject: [PATCH 5/7] injectState can be called on top of already-rendered component #4 --- entry.js | 3 ++- src/render.js | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/entry.js b/entry.js index 6227284..8242c0c 100644 --- a/entry.js +++ b/entry.js @@ -2,5 +2,6 @@ module.exports = { Mixin: require('./src/load-child-mixin.js'), Component: require('./src/load-child-component.js'), serialize: require('./src/serialize.js').serialize, - render: require('./src/render.js').render + render: require('./src/render.js').render, + injectState: require('./src/render.js').injectState }; diff --git a/src/render.js b/src/render.js index c1d3d55..ce57753 100644 --- a/src/render.js +++ b/src/render.js @@ -20,13 +20,13 @@ exports.render = function(options) { component = React.render(element, options.container); if (!_.isEmpty(state)) { - injectState(component, state); + exports.injectState(component, state); } return component; }; -var injectState = function(component, state) { +exports.injectState = function(component, state) { var rootState = _.omit(state, 'children'), childrenStates = state.children; @@ -38,7 +38,7 @@ var injectState = function(component, state) { _.each(component.refs, function(child, ref) { if (!_.isEmpty(childrenStates[ref])) { - injectState(child, childrenStates[ref]); + exports.injectState(child, childrenStates[ref]); } }); }; From ce94700ec22963dccee81d2ed4963d0df4f8681d Mon Sep 17 00:00:00 2001 From: Ovidiu Chereches Date: Tue, 14 Apr 2015 00:54:13 +0300 Subject: [PATCH 6/7] Fix and improve code snippets #4 --- README.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index e28acc8..2fe720f 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,8 @@ Generate a snapshot with the props and state of a component combined, including the state of all nested child components. ```js +var ComponentTree = require('react-component-tree'); + myCompany.setProps({public: true}); myCompany.setState({profitable: true}); myCompany.refs.employee54.setState({bored: false}); @@ -27,12 +29,12 @@ The snapshot looks like this: public: true, state: { profitable: true - }, - children: { - employee54: { - bored: false + children: { + employee54: { + bored: false + } } - } + }, } ``` From 95aa9bd73f789bd37a62939214419d5a65561b6d Mon Sep 17 00:00:00 2001 From: Ovidiu Chereches Date: Tue, 14 Apr 2015 00:55:30 +0300 Subject: [PATCH 7/7] Missing comma in js snippet #4 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2fe720f..458b1c3 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ The snapshot looks like this: { public: true, state: { - profitable: true + profitable: true, children: { employee54: { bored: false