diff --git a/fixtures/component-playground/default.js b/fixtures/component-playground/default.js index 47f8332..009fd11 100644 --- a/fixtures/component-playground/default.js +++ b/fixtures/component-playground/default.js @@ -1,7 +1,21 @@ +var React = require('react'); + +class FirstComponent extends React.Component { + render() { + return React.DOM.div(); + } +} + +class SecondComponent extends React.Component { + render() { + return React.DOM.div(); + } +} + module.exports = { components: { FirstComponent: { - class: 'FirstComponent', + class: FirstComponent, fixtures: { 'default': { myProp: false, @@ -9,6 +23,11 @@ module.exports = { foo: 'bar', shouldBeCloned: {} }, + children: [ + React.createElement('span', { + children: 'test child' + }) + ], state: { somethingHappened: false } @@ -17,7 +36,7 @@ module.exports = { } }, SecondComponent: { - class: 'SecondComponent', + class: SecondComponent, fixtures: { 'index': { myProp: true, diff --git a/fixtures/component-playground/selected-fixture.js b/fixtures/component-playground/selected-fixture.js index acefe47..38cbc6e 100644 --- a/fixtures/component-playground/selected-fixture.js +++ b/fixtures/component-playground/selected-fixture.js @@ -1,4 +1,5 @@ -var _ = require('lodash'), +var React = require('react'), + _ = require('lodash'), defaultFixture = require('./default.js'); module.exports = _.merge({}, defaultFixture, { @@ -17,6 +18,13 @@ module.exports = _.merge({}, defaultFixture, { somethingHappened: false } }, + fixtureUnserializableProps: { + children: [ + React.createElement('span', { + children: 'test child' + }) + ] + }, fixtureChange: 10 } }); diff --git a/package.json b/package.json index 02074fa..fb4e09d 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "dependencies": { "classnames": "^1.2.0", "lodash": "^3.6.0", - "react-component-tree": "^0.2.2", + "react-component-tree": "^0.2.3", "react-querystring-router": "^0.2.0" }, "devDependencies": { diff --git a/src/components/component-playground.jsx b/src/components/component-playground.jsx index 5820487..48f0ad9 100644 --- a/src/components/component-playground.jsx +++ b/src/components/component-playground.jsx @@ -5,7 +5,8 @@ var _ = require('lodash'), classNames = require('classnames'), ComponentTree = require('react-component-tree'), stringifyParams = require('react-querystring-router').uri.stringifyParams, - parseLocation = require('react-querystring-router').uri.parseLocation; + parseLocation = require('react-querystring-router').uri.parseLocation, + isSerializable = require('../lib/is-serializable.js').isSerializable; module.exports = React.createClass({ /** @@ -62,15 +63,31 @@ module.exports = React.createClass({ expandedComponents: this.getExpandedComponents(props, expandedComponents), fixtureContents: {}, + fixtureUnserializableProps: {}, fixtureUserInput: '{}', isFixtureUserInputValid: true }; if (this.isFixtureSelected(props)) { - var fixtureContents = this.getSelectedFixtureContents(props); + var originalFixtureContents = this.getSelectedFixtureContents(props), + fixtureContents = {}, + fixtureUnserializableProps = {}; + + // Unserializable props are stored separately from serializable ones + // because the serializable props can be overriden by the user using + // the editor, while the unserializable props are always attached + // behind the scenes + _.forEach(originalFixtureContents, function(value, key) { + if (isSerializable(value)) { + fixtureContents[key] = value; + } else { + fixtureUnserializableProps[key] = value; + } + }); _.assign(state, { fixtureContents: fixtureContents, + fixtureUnserializableProps: fixtureUnserializableProps, fixtureUserInput: this.getStringifiedFixtureContents(fixtureContents) }); } @@ -104,6 +121,9 @@ module.exports = React.createClass({ key: this._getPreviewComponentKey() }; + // Shallow apply unserializable props + _.assign(params, this.state.fixtureUnserializableProps); + return _.merge(params, _.omit(this.state.fixtureContents, 'state')); } }, @@ -357,10 +377,14 @@ module.exports = React.createClass({ var snapshot = ComponentTree.serialize(this.refs.preview); + // Continue to ignore unserializable props + var serializableSnapshot = + _.omit(snapshot, _.keys(this.state.fixtureUnserializableProps)); + this.setState({ - fixtureContents: snapshot, + fixtureContents: serializableSnapshot, fixtureUserInput: - this.constructor.getStringifiedFixtureContents(snapshot), + this.constructor.getStringifiedFixtureContents(serializableSnapshot), isFixtureUserInputValid: true }); }, @@ -370,15 +394,7 @@ module.exports = React.createClass({ newState = {fixtureUserInput: userInput}; try { - var originalFixtureContents = - this.constructor.getSelectedFixtureContents(this.props); - - // We only want to extend Function props because they can't be serialized - // and they are not part of the editor's contents - var fixtureContents = - _.pick(originalFixtureContents, function(value, key) { - return _.isFunction(value); - }); + var fixtureContents = {}; if (userInput) { _.merge(fixtureContents, JSON.parse(userInput)); diff --git a/src/lib/is-serializable.js b/src/lib/is-serializable.js new file mode 100644 index 0000000..ec33427 --- /dev/null +++ b/src/lib/is-serializable.js @@ -0,0 +1,24 @@ +var _ = require('lodash'); + +exports.isSerializable = function(obj) { + if (_.isUndefined(obj) || + _.isNull(obj) || + _.isBoolean(obj) || + _.isNumber(obj) || + _.isString(obj)) { + return true; + } + + if (!_.isPlainObject(obj) && + !_.isArray(obj)) { + return false; + } + + for (var key in obj) { + if (!exports.isSerializable(obj[key])) { + return false; + } + } + + return true; +}; diff --git a/tests/components/component-playground/selected-fixture-and-editor/events/handlers.js b/tests/components/component-playground/selected-fixture-and-editor/events/handlers.js index a536816..137228e 100644 --- a/tests/components/component-playground/selected-fixture-and-editor/events/handlers.js +++ b/tests/components/component-playground/selected-fixture-and-editor/events/handlers.js @@ -10,47 +10,54 @@ describe(`ComponentPlayground (${FIXTURE}) Events Handlers`, function() { container, fixture; - var childSnapshot = {}, - stringifiedChildSnapshot = '{}', - stateSet; - beforeEach(function() { - sinon.stub(ComponentTree, 'serialize').returns(childSnapshot); - sinon.stub(JSON, 'stringify').returns(stringifiedChildSnapshot); - ({fixture, container, component, $component} = render(originalFixture)); - - sinon.spy(component, 'setState'); - - component.onFixtureUpdate(); - stateSet = component.setState.lastCall.args[0]; - }); - - afterEach(function() { - ComponentTree.serialize.restore(); - JSON.stringify.restore(); - - component.setState.restore(); - }); - - it('should mark user input state as valid', function() { - expect(stateSet.isFixtureUserInputValid).to.equal(true); - }); - - it('should serialize preview child', function() { - expect(ComponentTree.serialize) - .to.have.been.calledWith(component.refs.preview); - }); - - it('should update child snapshot state', function() { - expect(stateSet.fixtureContents).to.equal(childSnapshot); - }); - - it('should stringify preview child snapshot', function() { - expect(JSON.stringify).to.have.been.calledWith(childSnapshot); }); - it('should update stringified child snapshot state', function() { - expect(stateSet.fixtureUserInput).to.equal(stringifiedChildSnapshot); + define('on fixture update', function() { + var stateSet; + + beforeEach(function() { + sinon.stub(ComponentTree, 'serialize').returns(_.merge({}, + fixture.state.fixtureContents, + fixture.state.fixtureUnserializableProps)); + sinon.spy(component, 'setState'); + + component.onFixtureUpdate(); + + stateSet = component.setState.lastCall.args[0]; + }); + + afterEach(function() { + ComponentTree.serialize.restore(); + component.setState.restore(); + }); + + it('should mark user input state as valid', function() { + expect(stateSet.isFixtureUserInputValid).to.equal(true); + }); + + it('should serialize preview child', function() { + expect(ComponentTree.serialize) + .to.have.been.calledWith(component.refs.preview); + }); + + it('should put serializable child state in fixture contents', function() { + for (var key in fixture.state.fixtureContents) { + expect(stateSet.fixtureContents[key]) + .to.deep.equal(fixture.state.fixtureContents[key]); + } + }); + + it('should ignore unserializable child state', function() { + for (var key in fixture.state.fixtureUnserializableProps) { + expect(stateSet.fixtureContents[key]).to.be.undefined; + } + }); + + it('should update stringified child snapshot state', function() { + expect(stateSet.fixtureUserInput) + .to.equal(JSON.stringify(fixture.state.fixtureContents, null, 2)); + }); }); }); diff --git a/tests/components/component-playground/selected-fixture/events/dom.js b/tests/components/component-playground/selected-fixture/events/dom.js index 643d34d..a97c2e4 100644 --- a/tests/components/component-playground/selected-fixture/events/dom.js +++ b/tests/components/component-playground/selected-fixture/events/dom.js @@ -79,7 +79,7 @@ describe(`ComponentPlayground (${FIXTURE}) Events DOM`, function() { expect(stateSet.expandedComponents.length).to.equal(1); expect(stateSet.expandedComponents[0]).to.equal('FirstComponent'); - expect(stateSet.fixtureContents).to.equal(fixtureContents); + expect(stateSet.fixtureContents).to.deep.equal(fixtureContents); expect(stateSet.fixtureUserInput).to.equal( JSON.stringify(fixtureContents, null, 2)); expect(stateSet.isFixtureUserInputValid).to.equal(true); diff --git a/tests/components/component-playground/selected-fixture/render/children.js b/tests/components/component-playground/selected-fixture/render/children.js index 833d2bb..eb29530 100644 --- a/tests/components/component-playground/selected-fixture/render/children.js +++ b/tests/components/component-playground/selected-fixture/render/children.js @@ -31,6 +31,7 @@ describe(`ComponentPlayground (${FIXTURE}) Render Children`, function() { it('should send fixture contents to preview child', function() { var fixtureContents = fixture.state.fixtureContents; + for (var key in fixtureContents) { if (key !== 'state') { expect(childParams[key]).to.deep.equal(fixtureContents[key]); @@ -38,6 +39,15 @@ describe(`ComponentPlayground (${FIXTURE}) Render Children`, function() { } }); + it('should send unserializable props to preview child', function() { + var fixtureUnserializableContents = + fixture.state.fixtureUnserializableContents; + + for (var key in fixtureUnserializableContents) { + expect(childParams[key]).to.equal(fixtureUnserializableContents[key]); + } + }); + it('should not send state as prop to preview child', function() { expect(childParams.state).to.be.undefined; }); diff --git a/tests/components/component-playground/selected-fixture/transitions/mount.js b/tests/components/component-playground/selected-fixture/transitions/mount.js index 0f74fe0..2a8bdcd 100644 --- a/tests/components/component-playground/selected-fixture/transitions/mount.js +++ b/tests/components/component-playground/selected-fixture/transitions/mount.js @@ -31,10 +31,15 @@ describe(`ComponentPlayground (${FIXTURE}) Transitions Mount`, function() { expect(expandedComponents[0]).to.equal('FirstComponent'); }); - it('should populate state with fixture contents', function() { + it('should populate state with serializable fixture contents', function() { expect(component.state.fixtureContents.myProp).to.equal(false); }); + it('should populate state with unserializable fixture props', function() { + expect(component.state.fixtureUnserializableProps.children).to.equal( + fixture.children); + }); + it('should populate stringified fixture contents as user input', function() { expect(component.state.fixtureUserInput).to.equal( JSON.stringify(component.state.fixtureContents, null, 2)); diff --git a/tests/components/component-playground/selected-fixture/transitions/props.js b/tests/components/component-playground/selected-fixture/transitions/props.js index 1ff4157..1db1bf7 100644 --- a/tests/components/component-playground/selected-fixture/transitions/props.js +++ b/tests/components/component-playground/selected-fixture/transitions/props.js @@ -42,6 +42,10 @@ describe(`ComponentPlayground (${FIXTURE}) Transitions Props`, function() { expect(stateSet.fixtureContents.myProp).to.equal(true); }); + it('should reset unserializable fixture props', function() { + expect(stateSet.fixtureUnserializableProps).to.deep.equal({}); + }); + it('should replace fixture user input', function() { expect(JSON.parse(stateSet.fixtureUserInput).myProp).to.equal(true); });