├── .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 [](https://travis-ci.org/skidding/react-component-tree) [](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 |
--------------------------------------------------------------------------------