diff --git a/src/rsg-components/Preview/Preview.js b/src/rsg-components/Preview/Preview.js
index 87209da19..7ad4e078c 100644
--- a/src/rsg-components/Preview/Preview.js
+++ b/src/rsg-components/Preview/Preview.js
@@ -1,34 +1,13 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import ReactDOM from 'react-dom';
-import { transform } from 'buble';
import PlaygroundError from 'rsg-components/PlaygroundError';
-import Wrapper from 'rsg-components/Wrapper';
-import splitExampleCode from '../../utils/splitExampleCode';
-/* eslint-disable no-invalid-this, react/no-multi-comp */
+import ReactExample from '../ReactExample';
-const Fragment = React.Fragment ? React.Fragment : 'div';
-const FragmentTag = React.Fragment ? 'React.Fragment' : 'div';
-
-const compileCode = (code, config) => transform(code, config).code;
-const wrapCodeInFragment = code => `<${FragmentTag}>${code}${FragmentTag}>;`;
-
-// Wrap everything in a React component to leverage the state management
-// of this component
-class PreviewComponent extends Component {
- static propTypes = {
- component: PropTypes.func.isRequired,
- initialState: PropTypes.object.isRequired,
- };
-
- state = this.props.initialState;
- setStateBinded = this.setState.bind(this);
+/* eslint-disable no-invalid-this */
- render() {
- return this.props.component(this.state, this.setStateBinded);
- }
-}
+const Fragment = React.Fragment ? React.Fragment : 'div';
export default class Preview extends Component {
static propTypes = {
@@ -68,29 +47,6 @@ export default class Preview extends Component {
this.unmountPreview();
}
- // Eval the code to extract the value of the initial state
- getExampleInitialState(compiledCode) {
- if (compiledCode.indexOf('initialState') === -1) {
- return {};
- }
-
- return this.props.evalInContext(`
- var state = {}, initialState = {};
- try {
- ${compiledCode};
- } catch (err) {}
- return initialState;
- `)();
- }
-
- // Run example code and return the last top-level expression
- getExampleComponent(compiledCode) {
- return this.props.evalInContext(`
- var initialState = {};
- ${compiledCode}
- `);
- }
-
unmountPreview() {
if (this.mountNode) {
ReactDOM.unmountComponentAtNode(this.mountNode);
@@ -107,18 +63,13 @@ export default class Preview extends Component {
return;
}
- const compiledCode = this.compileCode(code);
- if (!compiledCode) {
- return;
- }
-
- const { head, example } = splitExampleCode(compiledCode);
- const initialState = this.getExampleInitialState(head);
- const exampleComponent = this.getExampleComponent(example);
const wrappedComponent = (
-
-
-
+
);
window.requestAnimationFrame(() => {
@@ -131,16 +82,6 @@ export default class Preview extends Component {
});
}
- compileCode(code) {
- try {
- const wrappedCode = code.trim().match(/^) ? wrapCodeInFragment(code) : code;
- return compileCode(wrappedCode, this.context.config.compilerConfig);
- } catch (err) {
- this.handleError(err);
- }
- return false;
- }
-
handleError = err => {
this.unmountPreview();
diff --git a/src/rsg-components/Preview/Preview.spec.js b/src/rsg-components/Preview/Preview.spec.js
index d7b540d36..b49da2d4f 100644
--- a/src/rsg-components/Preview/Preview.spec.js
+++ b/src/rsg-components/Preview/Preview.spec.js
@@ -10,6 +10,7 @@ const evalInContext = a =>
require
);
const code = '';
+const newCode = '';
const options = {
context: {
config: {
@@ -58,6 +59,8 @@ it('should wrap code in Fragment when it starts with <', () => {
});
it('should render component renderer', () => {
+ console.error = jest.fn();
+
const actual = shallow(, {
...options,
disableLifecycleMethods: true,
@@ -66,6 +69,28 @@ it('should render component renderer', () => {
expect(actual).toMatchSnapshot();
});
+it('should update', () => {
+ const actual = mount(, options);
+
+ actual.setProps({ code: newCode });
+
+ expect(actual.html()).toMatchSnapshot();
+});
+
+it('should handle no code', () => {
+ const actual = mount(, options);
+
+ expect(actual.html()).toMatchSnapshot();
+});
+
+it('should handle errors', () => {
+ console.error = jest.fn();
+ const actual = shallow(, options);
+
+ expect(actual).toMatchSnapshot();
+ expect(console.error).toHaveBeenCalledTimes(1);
+});
+
it('should not clear console on initial mount', () => {
console.clear = jest.fn();
mount(, options);
@@ -79,35 +104,3 @@ it('should clear console on second mount', () => {
});
expect(console.clear).toHaveBeenCalledTimes(1);
});
-
-it('should set initialState before the first render', () => {
- const code = `
-initialState = {count:1};
-{state.count}
- `;
- const actual = mount(, options);
- expect(actual.html()).toMatchSnapshot();
-});
-
-it('should update state on setState', done => {
- const code = `
-initialState = {count:1};
-setTimeout(() => state.count === 1 && setState({count:2}));
-
- `;
- const actual = mount(, options);
-
- actual
- .instance()
- .mountNode.querySelector('button')
- .click();
-
- setTimeout(() => {
- try {
- expect(actual.html()).toMatchSnapshot();
- done();
- } catch (err) {
- done.fail(err);
- }
- });
-});
diff --git a/src/rsg-components/Preview/__snapshots__/Preview.spec.js.snap b/src/rsg-components/Preview/__snapshots__/Preview.spec.js.snap
index c41d547be..aeae44b19 100644
--- a/src/rsg-components/Preview/__snapshots__/Preview.spec.js.snap
+++ b/src/rsg-components/Preview/__snapshots__/Preview.spec.js.snap
@@ -1,26 +1,32 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`should render component renderer 1`] = `
+exports[`should handle errors 1`] = `
+
`;
-exports[`should set initialState before the first render 1`] = `
+exports[`should handle no code 1`] = `
-
- 1
-
`;
-exports[`should update state on setState 1`] = `
+exports[`should render component renderer 1`] = `
+
+
+
+`;
+
+exports[`should update 1`] = `
diff --git a/src/rsg-components/ReactExample/ReactExample.js b/src/rsg-components/ReactExample/ReactExample.js
new file mode 100644
index 000000000..529b6f0dd
--- /dev/null
+++ b/src/rsg-components/ReactExample/ReactExample.js
@@ -0,0 +1,94 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import { transform } from 'buble';
+import Wrapper from 'rsg-components/Wrapper';
+import splitExampleCode from '../../utils/splitExampleCode';
+
+/* eslint-disable no-invalid-this, react/no-multi-comp */
+
+const FragmentTag = React.Fragment ? 'React.Fragment' : 'div';
+
+const compileCode = (code, config) => transform(code, config).code;
+const wrapCodeInFragment = code => `<${FragmentTag}>${code}${FragmentTag}>;`;
+
+// Wrap everything in a React component to leverage the state management
+// of this component
+class StateHolder extends Component {
+ static propTypes = {
+ component: PropTypes.func.isRequired,
+ initialState: PropTypes.object.isRequired,
+ };
+
+ state = this.props.initialState;
+ setStateBinded = this.setState.bind(this);
+
+ render() {
+ return this.props.component(this.state, this.setStateBinded);
+ }
+}
+
+export default class ReactExample extends Component {
+ static propTypes = {
+ code: PropTypes.string.isRequired,
+ evalInContext: PropTypes.func.isRequired,
+ onError: PropTypes.func.isRequired,
+ compilerConfig: PropTypes.object,
+ };
+ static contextTypes = {};
+
+ shouldComponentUpdate(nextProps) {
+ return this.props.code !== nextProps.code;
+ }
+
+ // Eval the code to extract the value of the initial state
+ getExampleInitialState(compiledCode) {
+ if (compiledCode.indexOf('initialState') === -1) {
+ return {};
+ }
+
+ return this.props.evalInContext(`
+ var state = {}, initialState = {};
+ try {
+ ${compiledCode};
+ } catch (err) {}
+ return initialState;
+ `)();
+ }
+
+ // Run example code and return the last top-level expression
+ getExampleComponent(compiledCode) {
+ return this.props.evalInContext(`
+ var initialState = {};
+ ${compiledCode}
+ `);
+ }
+
+ compileCode(code) {
+ try {
+ const wrappedCode = code.trim().match(/^) ? wrapCodeInFragment(code) : code;
+ return compileCode(wrappedCode, this.props.compilerConfig);
+ } catch (err) {
+ if (this.props.onError) {
+ this.props.onError(err);
+ }
+ }
+ return false;
+ }
+
+ render() {
+ const compiledCode = this.compileCode(this.props.code);
+ if (!compiledCode) {
+ return null;
+ }
+
+ const { head, example } = splitExampleCode(compiledCode);
+ const initialState = this.getExampleInitialState(head);
+ const exampleComponent = this.getExampleComponent(example);
+ const wrappedComponent = (
+
+
+
+ );
+ return wrappedComponent;
+ }
+}
diff --git a/src/rsg-components/ReactExample/ReactExample.spec.js b/src/rsg-components/ReactExample/ReactExample.spec.js
new file mode 100644
index 000000000..ddc8a05cf
--- /dev/null
+++ b/src/rsg-components/ReactExample/ReactExample.spec.js
@@ -0,0 +1,65 @@
+import React from 'react';
+import noop from 'lodash/noop';
+import ReactExample from '../ReactExample';
+
+const evalInContext = a =>
+ // eslint-disable-next-line no-new-func
+ new Function('require', 'state', 'setState', 'const React = require("react");' + a).bind(
+ null,
+ require
+ );
+
+it('should render code', () => {
+ const actual = shallow(
+ OK'} evalInContext={evalInContext} onError={noop} />
+ );
+
+ expect(actual).toMatchSnapshot();
+});
+
+it('should wrap code in Fragment when it starts with <', () => {
+ const actual = mount(
+
+
+
+ );
+
+ expect(actual.html()).toMatchSnapshot();
+});
+
+it('should handle errors', () => {
+ const onError = jest.fn();
+
+ shallow();
+
+ expect(onError).toHaveBeenCalledTimes(1);
+});
+
+it('should set initialState before the first render', () => {
+ const code = `
+initialState = {count:1};
+{state.count}
+ `;
+ const actual = mount();
+ expect(actual.html()).toMatchSnapshot();
+});
+
+it('should update state on setState', done => {
+ const code = `
+initialState = {count:1};
+setTimeout(() => state.count === 1 && setState({count:2}));
+
+ `;
+ const actual = mount();
+
+ actual.find('button').simulate('click');
+
+ setTimeout(() => {
+ try {
+ expect(actual.html()).toMatchSnapshot();
+ done();
+ } catch (err) {
+ done.fail(err);
+ }
+ });
+});
diff --git a/src/rsg-components/ReactExample/__snapshots__/ReactExample.spec.js.snap b/src/rsg-components/ReactExample/__snapshots__/ReactExample.spec.js.snap
new file mode 100644
index 000000000..2b8b201cf
--- /dev/null
+++ b/src/rsg-components/ReactExample/__snapshots__/ReactExample.spec.js.snap
@@ -0,0 +1,39 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render code 1`] = `
+
+
+
+`;
+
+exports[`should set initialState before the first render 1`] = `
+
+
+ 1
+
+
+`;
+
+exports[`should update state on setState 1`] = `
+
+
+
+`;
+
+exports[`should wrap code in Fragment when it starts with < 1`] = `
+
+
+
+
+
+
+
+
+`;
diff --git a/src/rsg-components/ReactExample/index.js b/src/rsg-components/ReactExample/index.js
new file mode 100644
index 000000000..5179f7fd5
--- /dev/null
+++ b/src/rsg-components/ReactExample/index.js
@@ -0,0 +1 @@
+export { default } from 'rsg-components/ReactExample/ReactExample';