Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
9fefd3b
commit d7ec44f
Showing
14 changed files
with
4,352 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
{ | ||
"presets": ["env", "react"], | ||
|
||
"plugins": [ | ||
"transform-class-properties", | ||
"transform-object-rest-spread" | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
{ | ||
"extends": "airbnb", | ||
|
||
"parser": "babel-eslint", | ||
|
||
"plugins": ["prettier"], | ||
|
||
"env": { | ||
"browser": true, | ||
"jest": true | ||
}, | ||
|
||
"rules": { | ||
"arrow-parens": ["error", "as-needed", { "requireForBlockBody": false }], | ||
"no-use-before-define": "off", | ||
"import/no-extraneous-dependencies": ["error", { "devDependencies": true }], | ||
"prettier/prettier": ["error", { | ||
"semi": false, | ||
"singleQuote": true, | ||
"trailingComma": "es5" | ||
}], | ||
"react/jsx-filename-extension": ["error", { "extensions": [".js"] }], | ||
"react/prop-types": ["error", { skipUndeclared: true }], | ||
"semi": ["error", "never"], | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
node_modules |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
language: node_js | ||
|
||
node_js: | ||
- "lts/*" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,19 @@ | ||
# react-automata | ||
[![Build Status](https://travis-ci.org/MicheleBertoli/react-automata.svg?branch=master)](https://travis-ci.org/MicheleBertoli/react-automata) | ||
[![tested with jest](https://img.shields.io/badge/tested_with-jest-99424f.svg)](https://github.com/facebook/jest) | ||
[![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier) | ||
|
||
> This is a work in progress | ||
# React Automata | ||
|
||
What if your components' state was predictable? | ||
|
||
Goals: | ||
- Declaratively define states | ||
- Automagically generate tests | ||
|
||
# Inspiration | ||
|
||
[Infinitely Better UIs with Finite Automata](https://www.youtube.com/watch?v=VU1NKX6Qkxc) by [David](https://twitter.com/DavidKPiano) | ||
|
||
[Rambling thoughts on React and Finite State Machines](https://www.youtube.com/watch?v=MkdV2-U16tc) by [Ryan](https://twitter.com/ryanflorence) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
{ | ||
"name": "react-automata", | ||
"version": "0.1.0", | ||
"main": "index.js", | ||
"author": "Michele Bertoli", | ||
"license": "MIT", | ||
"scripts": { | ||
"test": "jest", | ||
"precommit": "lint-staged" | ||
}, | ||
"devDependencies": { | ||
"babel-core": "^6.26.0", | ||
"babel-eslint": "^8.0.1", | ||
"babel-jest": "^21.2.0", | ||
"babel-plugin-transform-class-properties": "^6.24.1", | ||
"babel-plugin-transform-object-rest-spread": "^6.26.0", | ||
"babel-preset-env": "^1.6.1", | ||
"babel-preset-react": "^6.24.1", | ||
"eslint": "^4.9.0", | ||
"eslint-config-airbnb": "^16.1.0", | ||
"eslint-plugin-import": "^2.7.0", | ||
"eslint-plugin-jsx-a11y": "^6.0.2", | ||
"eslint-plugin-prettier": "^2.3.1", | ||
"eslint-plugin-react": "^7.4.0", | ||
"husky": "^0.14.3", | ||
"jest": "^21.2.1", | ||
"lint-staged": "^4.3.0", | ||
"prettier": "^1.7.4", | ||
"prop-types": "^15.6.0", | ||
"react": "^16.0.0", | ||
"react-dom": "^16.0.0", | ||
"react-test-renderer": "^16.0.0", | ||
"xstate": "^1.2.1" | ||
}, | ||
"lint-staged": { | ||
"*.{js}": [ | ||
"eslint --fix", | ||
"git add" | ||
] | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import React from 'react' | ||
import PropTypes from 'prop-types' | ||
|
||
class State extends React.Component { | ||
render() { | ||
return this.context.machineState === this.props.name | ||
? this.props.children | ||
: null | ||
} | ||
} | ||
|
||
State.defaultProps = { | ||
children: null, | ||
} | ||
|
||
State.propTypes = { | ||
name: PropTypes.string.isRequired, | ||
children: PropTypes.node, | ||
} | ||
|
||
State.contextTypes = { | ||
machineState: PropTypes.string, | ||
} | ||
|
||
export default State |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export { default as State } from './State' | ||
export { default as testStateMachine } from './testStateMachine' | ||
export { default as withStateMachine } from './withStateMachine' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import React from 'react' | ||
import PropTypes from 'prop-types' | ||
import TestRenderer from 'react-test-renderer' | ||
|
||
const createContext = (context, Component) => { | ||
class Context extends React.Component { | ||
getChildContext() { | ||
return { ...context } | ||
} | ||
|
||
render() { | ||
return <Component /> | ||
} | ||
} | ||
|
||
Context.childContextTypes = { | ||
machineState: PropTypes.string, | ||
} | ||
|
||
return Context | ||
} | ||
|
||
const injectState = (renderer, Component, fixtures) => { | ||
if (fixtures) { | ||
const { instance } = renderer.root.findByType(Component) | ||
instance.setState(fixtures) | ||
} | ||
} | ||
|
||
const moveToNextState = (config, Component, machineState) => { | ||
const { on: actions } = config.machine.states[machineState] | ||
if (actions) { | ||
Object.values(actions).forEach(state => { | ||
toMatchSnapshot(config, Component, state) | ||
}) | ||
} | ||
} | ||
|
||
const toMatchSnapshot = (config, Component, machineState) => { | ||
const Context = createContext({ machineState }, Component) | ||
const renderer = TestRenderer.create(<Context />) | ||
injectState(renderer, Component, config.fixtures[machineState]) | ||
expect(renderer.toJSON()).toMatchSnapshot(machineState) | ||
moveToNextState(config, Component, machineState) | ||
} | ||
|
||
const testStateMachine = (config, Component) => { | ||
toMatchSnapshot(config, Component, config.machine.initial) | ||
} | ||
|
||
export default testStateMachine |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import React from 'react' | ||
import PropTypes from 'prop-types' | ||
import { Machine } from 'xstate' | ||
|
||
const withStateMachine = config => Component => { | ||
class StateMachine extends React.Component { | ||
state = { | ||
machineState: this.machine.getInitialState(), | ||
} | ||
|
||
getChildContext() { | ||
return { ...this.state } | ||
} | ||
|
||
machine = Machine(config) | ||
|
||
handleTransition = action => { | ||
this.setState(prevState => ({ | ||
machineState: this.machine.transition(prevState.machineState, action) | ||
.value, | ||
})) | ||
} | ||
|
||
render() { | ||
return <Component {...this.props} transition={this.handleTransition} /> | ||
} | ||
} | ||
|
||
StateMachine.childContextTypes = { | ||
machineState: PropTypes.string, | ||
} | ||
|
||
return StateMachine | ||
} | ||
|
||
export default withStateMachine |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
// Jest Snapshot v1, https://goo.gl/fbAQLP | ||
|
||
exports[`error 1`] = ` | ||
<div> | ||
<h1> | ||
State Machine | ||
</h1> | ||
Oh, snap! | ||
</div> | ||
`; | ||
|
||
exports[`fetching 1`] = ` | ||
<div> | ||
<h1> | ||
State Machine | ||
</h1> | ||
Loading... | ||
</div> | ||
`; | ||
|
||
exports[`idle 1`] = ` | ||
<div> | ||
<h1> | ||
State Machine | ||
</h1> | ||
<button | ||
onClick={[Function]} | ||
> | ||
Fetch | ||
</button> | ||
</div> | ||
`; | ||
|
||
exports[`success 1`] = ` | ||
<div> | ||
<h1> | ||
State Machine | ||
</h1> | ||
<ul> | ||
<li> | ||
GIST1 | ||
</li> | ||
<li> | ||
GIST2 | ||
</li> | ||
</ul> | ||
</div> | ||
`; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import React from 'react' | ||
import { State } from '../src' | ||
|
||
class App extends React.Component { | ||
state = { gists: [] } | ||
|
||
handleClick = () => { | ||
this.props.transition('FETCH') | ||
|
||
fetch('https://api.github.com/users/gaearon/gists') | ||
.then(response => response.json()) | ||
.then(gists => { | ||
this.setState({ gists }) | ||
this.props.transition('SUCCESS') | ||
}) | ||
.catch(() => this.props.transition('ERROR')) | ||
} | ||
|
||
render() { | ||
return ( | ||
<div> | ||
<h1>State Machine</h1> | ||
<State name="idle"> | ||
<button onClick={this.handleClick}>Fetch</button> | ||
</State> | ||
<State name="fetching">Loading...</State> | ||
<State name="success"> | ||
<ul> | ||
{this.state.gists.map(gist => ( | ||
<li key={gist.id}>{gist.description}</li> | ||
))} | ||
</ul> | ||
</State> | ||
<State name="error">Oh, snap!</State> | ||
</div> | ||
) | ||
} | ||
} | ||
|
||
export default App |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import { testStateMachine } from '../src' | ||
import App from './app' | ||
|
||
const machine = { | ||
initial: 'idle', | ||
states: { | ||
idle: { | ||
on: { | ||
FETCH: 'fetching', | ||
}, | ||
}, | ||
fetching: { | ||
on: { | ||
SUCCESS: 'success', | ||
ERROR: 'error', | ||
}, | ||
}, | ||
success: {}, | ||
error: {}, | ||
}, | ||
} | ||
|
||
const fixtures = { | ||
success: { | ||
gists: [ | ||
{ | ||
id: 'ID1', | ||
description: 'GIST1', | ||
}, | ||
{ | ||
id: 'ID2', | ||
description: 'GIST2', | ||
}, | ||
], | ||
}, | ||
} | ||
|
||
test('it works', () => { | ||
testStateMachine({ machine, fixtures }, App) | ||
}) |
Oops, something went wrong.