-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Split new CodeEvaluator component out from Preview #976
Changes from 5 commits
6220654
389c554
0d7c46d
26ea351
b349fb4
6083342
358dffb
f35d1c9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 = ( | ||
<Wrapper onError={this.handleError}> | ||
<PreviewComponent component={exampleComponent} initialState={initialState} /> | ||
</Wrapper> | ||
<ReactExample | ||
code={code} | ||
evalInContext={this.props.evalInContext} | ||
onError={this.handleError} | ||
compilerConfig={this.context.config.compilerConfig} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't CodeEvaluator read config from context itself, similar to what you did for the Editor? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think it inherits context when it is rendered via There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, true. |
||
/> | ||
); | ||
|
||
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(); | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 InitialStateComponent extends Component { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Something like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Gotcha, I'll change it. |
||
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 = ( | ||
<Wrapper onError={this.props.onError}> | ||
<InitialStateComponent component={exampleComponent} initialState={initialState} /> | ||
</Wrapper> | ||
); | ||
return wrappedComponent; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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( | ||
<ReactExample code={'<button>OK</button>'} evalInContext={evalInContext} onError={noop} /> | ||
); | ||
|
||
expect(actual).toMatchSnapshot(); | ||
}); | ||
|
||
it('should wrap code in Fragment when it starts with <', () => { | ||
const actual = mount( | ||
<div> | ||
<ReactExample code="<span /><span />" evalInContext={evalInContext} onError={noop} /> | ||
</div> | ||
); | ||
|
||
expect(actual.html()).toMatchSnapshot(); | ||
}); | ||
|
||
it('should handle errors', () => { | ||
const onError = jest.fn(); | ||
|
||
shallow(<ReactExample code={'<invalid code'} evalInContext={evalInContext} onError={onError} />); | ||
|
||
expect(onError).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
it('should set initialState before the first render', () => { | ||
const code = ` | ||
initialState = {count:1}; | ||
<span>{state.count}</span> | ||
`; | ||
const actual = mount(<ReactExample code={code} evalInContext={evalInContext} onError={noop} />); | ||
expect(actual.html()).toMatchSnapshot(); | ||
}); | ||
|
||
it('should update state on setState', done => { | ||
const code = ` | ||
initialState = {count:1}; | ||
setTimeout(() => state.count === 1 && setState({count:2})); | ||
<button>{state.count}</button> | ||
`; | ||
const actual = mount(<ReactExample code={code} evalInContext={evalInContext} onError={noop} />); | ||
|
||
actual.find('button').simulate('click'); | ||
|
||
setTimeout(() => { | ||
try { | ||
expect(actual.html()).toMatchSnapshot(); | ||
done(); | ||
} catch (err) { | ||
done.fail(err); | ||
} | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we still need this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, in the
handleError
function.