Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 47 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,48 @@
# react-component-playground [![Build Status](https://travis-ci.org/skidding/react-component-playground.svg?branch=master)](https://travis-ci.org/skidding/react-component-playground) [![Coverage Status](https://coveralls.io/repos/skidding/react-component-playground/badge.svg?branch=master)](https://coveralls.io/r/skidding/react-component-playground?branch=master)
# React Component Playground
[![Build Status](https://travis-ci.org/skidding/react-component-playground.svg?branch=master)](https://travis-ci.org/skidding/react-component-playground) [![Coverage Status](https://coveralls.io/repos/skidding/react-component-playground/badge.svg?branch=master)](https://coveralls.io/r/skidding/react-component-playground?branch=master)

Isolated loader for React components.
ComponentPlayground provides a minimal frame for loading and testing React
components in isolation.

Working with ComponentPlayground improves the component design because it
surfaces any implicit dependencies. It also forces you to define sane inputs
for every component, making them more predictable and easier to debug down
the road.

Features include:

- Rendering full-screen components or with the navigation pane on the side.
- Injecting predefined state into components via [ComponentTree](https://github.com/skidding/react-component-tree)
- Real-time editing of props and state with instant feedback

Before diving deeper, you need to understand what a component _fixture_ looks
like. It's the same thing as a snapshot in the ComponentTree utility. Read more
on the project [README](https://github.com/skidding/react-component-tree#componenttreeserialize).

`components` is by far the most important of the ComponentPlayground [props](https://github.com/skidding/react-component-playground/blob/master/src/components/component-playground.jsx#L19-L26).
This is an example:

```js
{
ComponentOne: {
class: require('./components/ComponentOne.jsx'),
fixtures: {
normal: {
fooProp: 'bar'
},
paused: {
fooProp: 'bar',
state: {
paused: true
}
}
}
},
ComponentTwo: {
class: require('./components/ComponentTwo.jsx'),
fixtures: {
//...
}
}
};
```
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-component-playground",
"version": "0.1.1",
"version": "0.2.0",
"description": "Isolated loader for React components",
"main": "build/bundle.js",
"repository": {
Expand Down
110 changes: 63 additions & 47 deletions src/components/component-playground.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ module.exports = React.createClass({
mixins: [ComponentTree.Mixin],

propTypes: {
fixtures: React.PropTypes.object.isRequired,
components: React.PropTypes.object.isRequired,
selectedComponent: React.PropTypes.string,
selectedFixture: React.PropTypes.string,
fixtureEditor: React.PropTypes.bool,
Expand All @@ -39,35 +39,36 @@ module.exports = React.createClass({
return props.selectedComponent && props.selectedFixture;
},

getSelectedFixtureContents: function(props) {
if (!this.isFixtureSelected(props)) {
return {};
}

var fixture = props.fixtures[props.selectedComponent]
[props.selectedFixture];
getSelectedComponentClass: function(props) {
return props.components[props.selectedComponent].class;
},

return _.merge({
component: props.selectedComponent
}, fixture);
getSelectedFixtureContents: function(props) {
return props.components[props.selectedComponent]
.fixtures[props.selectedFixture];
},

getSelectedFixtureUserInput: function(props) {
if (!this.isFixtureSelected(props)) {
return '{}';
}

return JSON.stringify(this.getSelectedFixtureContents(props), null, 2);
},

getFixtureState: function(props, expandedComponents) {
return {
expandedComponents: this.getExpandedComponents(props,
expandedComponents),
fixtureContents: this.getSelectedFixtureContents(props),
fixtureUserInput: this.getSelectedFixtureUserInput(props),
var state = {
expandedComponents:
this.getExpandedComponents(props, expandedComponents),
fixtureContents: {},
fixtureUserInput: '{}',
isFixtureUserInputValid: true
};

if (this.isFixtureSelected(props)) {
_.assign(state, {
fixtureContents: this.getSelectedFixtureContents(props),
fixtureUserInput: this.getSelectedFixtureUserInput(props)
});
}

return state;
}
},

Expand All @@ -84,44 +85,42 @@ module.exports = React.createClass({

children: {
preview: function() {
var props = {
var params = {
component: this.constructor.getSelectedComponentClass(this.props),
// Child should re-render whenever fixture changes
key: JSON.stringify(this.state.fixtureContents)
};

return _.merge(props, this.state.fixtureContents);
return _.merge(params, _.omit(this.state.fixtureContents, 'state'));
}
},

render: function() {
var isFixtureSelected = this.constructor.isFixtureSelected(this.props);

var classes = classNames({
'component-playground': true,
'full-screen': this.props.fullScreen
});

var homeUrlProps = {
fixtureEditor: this.props.fixtureEditor
};

return (
<div className={classes}>
<div className="header">
{this._renderButtons()}
{isFixtureSelected ? this._renderButtons() : null}
<h1>
<a ref="homeLink"
href={stringifyParams(homeUrlProps)}
href={stringifyParams({})}
className="home-link"
onClick={this.props.router.routeLink}>
<span className="react">React</span> Component Playground
</a>
{_.isEmpty(this.state.fixtureContents) ? this._renderCosmosPlug()
: null}
{!isFixtureSelected ? this._renderCosmosPlug() : null}
</h1>
</div>
<div className="fixtures">
{this._renderFixtures()}
</div>
{this._renderContentFrame()}
{isFixtureSelected ? this._renderContentFrame() : null}
</div>
);
},
Expand All @@ -135,7 +134,7 @@ module.exports = React.createClass({

_renderFixtures: function() {
return <ul className="components">
{_.map(this.props.fixtures, function(fixtures, componentName) {
{_.map(this.props.components, function(component, componentName) {

var classes = classNames({
'component': true,
Expand All @@ -151,7 +150,7 @@ module.exports = React.createClass({
{componentName}
</a>
</p>
{this._renderComponentFixtures(componentName, fixtures)}
{this._renderComponentFixtures(componentName, component.fixtures)}
</li>;

}.bind(this))}
Expand Down Expand Up @@ -186,8 +185,7 @@ module.exports = React.createClass({
_renderContentFrame: function() {
return <div className="content-frame">
<div ref="previewContainer" className={this._getPreviewClasses()}>
{!_.isEmpty(this.state.fixtureContents) ? this.loadChild('preview')
: null}
{this.loadChild('preview')}
</div>
{this.props.fixtureEditor ? this._renderFixtureEditor() : null}
</div>
Expand All @@ -209,11 +207,9 @@ module.exports = React.createClass({
},

_renderButtons: function() {
var isFixtureSelected = this.constructor.isFixtureSelected(this.props);

return <ul className="buttons">
{this._renderFixtureEditorButton()}
{isFixtureSelected ? this._renderFullScreenButton() : null}
{this._renderFullScreenButton()}
</ul>;
},

Expand All @@ -224,16 +220,11 @@ module.exports = React.createClass({
});

var fixtureEditorUrlProps = {
fixtureEditor: !this.props.fixtureEditor
fixtureEditor: !this.props.fixtureEditor,
selectedComponent: this.props.selectedComponent,
selectedFixture: this.props.selectedFixture
};

if (this.constructor.isFixtureSelected(this.props)) {
_.extend(fixtureEditorUrlProps, {
selectedComponent: this.props.selectedComponent,
selectedFixture: this.props.selectedFixture
});
}

return <li className={classes}>
<a href={stringifyParams(fixtureEditorUrlProps)}
ref="fixtureEditorButton"
Expand All @@ -255,6 +246,12 @@ module.exports = React.createClass({
</li>;
},

componentDidMount: function() {
if (this.refs.preview) {
this._injectPreviewChildState();
}
},

componentWillReceiveProps: function(nextProps) {
if (nextProps.selectedComponent !== this.props.selectedComponent ||
nextProps.selectedFixture !== this.props.selectedFixture) {
Expand All @@ -263,6 +260,17 @@ module.exports = React.createClass({
}
},

componentDidUpdate: function(prevProps, prevState) {
if (this.refs.preview && (
// Avoid deep comparing the fixture contents when component and/or
// fixture changed, because it's more expensive
this.props.selectedComponent !== prevProps.selectedComponent ||
this.props.selectedFixture !== prevProps.selectedFixture ||
!_.isEqual(this.state.fixtureContents, prevState.fixtureContents))) {
this._injectPreviewChildState();
}
},

onComponentClick: function(componentName, event) {
event.preventDefault();

Expand All @@ -286,7 +294,7 @@ module.exports = React.createClass({

try {
var fixtureContents =
this.constructor.getSelectedFixtureContents(this.props);
_.cloneDeep(this.constructor.getSelectedFixtureContents(this.props));

if (userInput) {
_.merge(fixtureContents, JSON.parse(userInput));
Expand Down Expand Up @@ -324,5 +332,13 @@ module.exports = React.createClass({
fixtureName === this.props.selectedFixture;

return classNames(classes);
},

_injectPreviewChildState: function() {
var state = this.state.fixtureContents.state;

if (!_.isEmpty(state)) {
ComponentTree.injectState(this.refs.preview, _.cloneDeep(state));
}
}
});
Loading