-
-
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 3 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 |
---|---|---|
@@ -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 { | ||
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 CodeEvaluator 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 CodeEvaluator from '../CodeEvaluator'; | ||
|
||
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( | ||
<CodeEvaluator 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> | ||
<CodeEvaluator code="<span /><span />" evalInContext={evalInContext} onError={noop} /> | ||
</div> | ||
); | ||
|
||
expect(actual.html()).toMatchSnapshot(); | ||
}); | ||
|
||
it('should handle errors', () => { | ||
const onError = jest.fn(); | ||
|
||
shallow(<CodeEvaluator 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(<CodeEvaluator 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(<CodeEvaluator code={code} evalInContext={evalInContext} onError={noop} />); | ||
|
||
actual.find('button').simulate('click'); | ||
|
||
setTimeout(() => { | ||
try { | ||
expect(actual.html()).toMatchSnapshot(); | ||
done(); | ||
} catch (err) { | ||
done.fail(err); | ||
} | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
// Jest Snapshot v1, https://goo.gl/fbAQLP | ||
|
||
exports[`should render code 1`] = ` | ||
<Wrapper | ||
onError={[Function]} | ||
> | ||
<InitialStateComponent | ||
component={[Function]} | ||
initialState={Object {}} | ||
/> | ||
</Wrapper> | ||
`; | ||
|
||
exports[`should set initialState before the first render 1`] = ` | ||
|
||
<span> | ||
1 | ||
</span> | ||
|
||
`; | ||
|
||
exports[`should update state on setState 1`] = ` | ||
|
||
<button> | ||
2 | ||
</button> | ||
|
||
`; | ||
|
||
exports[`should wrap code in Fragment when it starts with < 1`] = ` | ||
|
||
<div> | ||
<span> | ||
</span> | ||
<span> | ||
</span> | ||
</div> | ||
|
||
`; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { default } from 'rsg-components/CodeEvaluator/CodeEvaluator'; |
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 CodeEvaluator from '../CodeEvaluator'; | ||
|
||
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 */ | ||
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. Do we still need this? 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. Yes, in the |
||
|
||
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> | ||
<CodeEvaluator | ||
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(); | ||
|
||
|
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 you have any new code here? If not I won't read the whole file ;-)
This is React specific, so we should reflect it in the name: something like
ReactExample
(not the best too but I have no better ideas ;-)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.
Oh, right... yeah, git isn't being your friend here, this is all moved from
Preview
.The only things I changed were:
PreviewComponent
toInitialStateComponent
compilerConfig
as a prop instead of reading it offcontext
as discussed above.I'm happy to rename it... I struggled to come up with a decent name to.