├── register.js ├── .prettierrc ├── poc.gif ├── .babelrc ├── .storybook ├── addons.js └── config.js ├── src ├── cyto │ ├── layout.js │ ├── style.js │ └── index.js ├── index.js ├── register.js └── graphBuilder │ └── statechart.js ├── dist ├── cyto │ ├── layout.js │ ├── style.js │ └── index.js ├── graphBuilder │ ├── utils.js │ └── statechart.js ├── index.js └── register.js ├── stories ├── helper │ ├── TrafficLight.js │ └── StateProvider.js └── index.stories.js ├── README.md ├── LICENSE ├── .gitignore └── package.json /register.js: -------------------------------------------------------------------------------- 1 | require('./dist/register'); 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /poc.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/do-wa/xstate-addon/HEAD/poc.gif -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react"], 3 | "plugins": ["transform-object-rest-spread"] 4 | } 5 | -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-actions/register'; 2 | import '@storybook/addon-links/register'; 3 | import '../src/register'; 4 | -------------------------------------------------------------------------------- /src/cyto/layout.js: -------------------------------------------------------------------------------- 1 | const layout = { 2 | name: 'cose-bilkent', 3 | randomize: true, 4 | idealEdgeLength: 80, 5 | animate: false, 6 | fit: true 7 | }; 8 | export default layout; 9 | -------------------------------------------------------------------------------- /dist/cyto/layout.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | var layout = { 7 | name: 'cose-bilkent', 8 | randomize: true, 9 | idealEdgeLength: 80, 10 | animate: false, 11 | fit: true 12 | }; 13 | exports.default = layout; -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure } from '@storybook/react'; 2 | 3 | // automatically import all files ending in *.stories.js 4 | const req = require.context('../stories', true, /.stories.js$/); 5 | function loadStories() { 6 | req.keys().forEach((filename) => req(filename)); 7 | } 8 | 9 | configure(loadStories, module); 10 | -------------------------------------------------------------------------------- /stories/helper/TrafficLight.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const TrafficLight = ({ light }) => ( 4 |
11 | {['red', 'yellow', 'green'].map((c, i) => { 12 | let val = typeof light === 'string' ? light : Object.keys(light)[0]; 13 | return ( 14 |
21 | ); 22 | })} 23 |
24 | ); 25 | 26 | export default TrafficLight; 27 | -------------------------------------------------------------------------------- /dist/graphBuilder/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 8 | 9 | var createNode = exports.createNode = function createNode(data) { 10 | return { 11 | group: 'nodes', 12 | data: _extends({}, data) 13 | }; 14 | }; 15 | var createEdge = exports.createEdge = function createEdge(data) { 16 | return { 17 | group: 'edges', 18 | data: _extends({}, data) 19 | }; 20 | }; 21 | var createId = exports.createId = function createId(parent, id) { 22 | return '' + (parent ? parent + '.' : '') + id; 23 | }; -------------------------------------------------------------------------------- /stories/helper/StateProvider.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import addons from '@storybook/addons'; 3 | 4 | export default class StateProvider extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { 8 | machine: props.machine, 9 | currentState: props.machine.initial 10 | }; 11 | this.onEvent = this.onEvent.bind(this); 12 | } 13 | shouldComponentUpdate(nextProp, nextState) { 14 | return nextState.currentState !== this.state.currentState; 15 | } 16 | onEvent(next) { 17 | this.setState({ 18 | machine: this.state.machine, 19 | currentState: this.state.machine.transition(this.state.currentState, next) 20 | .value 21 | }); 22 | } 23 | render() { 24 | const { render } = this.props; 25 | const { machine, currentState } = this.state; 26 | const onEvent = this.onEvent; 27 | return render({ machine, currentState, onEvent }); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WARNING 2 | 3 | **this project is not actively maintained** 4 | 5 | **based on xstate v3.2 and storybook v3.4** 6 | 7 | # xstate-addon 8 | A storybook addon project for xstate (https://github.com/davidkpiano/xstate) 9 | 10 | The project is in an early stage. 11 | Everyone who is interested in bringing this project forward is welcome to contribute ideas, docs and code. 12 | 13 | ## install 14 | 15 | 16 | ```javascript 17 | npm i storybook-addon-xstate -D 18 | ``` 19 | 20 | ### register 21 | 22 | addons.js 23 | ```javascript 24 | import 'storybook-addon-xstate/register'; 25 | ``` 26 | 27 | ### story 28 | 29 | ```javascript 30 | import { WithXStateGraph } from 'storybook-addon-xstate'; 31 | 32 | 37 | 38 | 39 | ``` 40 | 41 | 42 | ## basic idea 43 | ![Basic Idea](https://github.com/do-wa/xstate-addon/blob/master/poc.gif) 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 do-wa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | -------------------------------------------------------------------------------- /src/cyto/style.js: -------------------------------------------------------------------------------- 1 | const style = [ 2 | { 3 | selector: 'node', 4 | style: { 5 | padding: '20px', 6 | 'text-valign': ele => (ele.data('hasChildren') ? 'top' : 'center'), 7 | 'text-halign': 'center', 8 | 'background-color': 'white', 9 | 'border-color': ele => { 10 | return ele.data('selected') 11 | ? 'red' 12 | : ele.data('isInitial') ? 'white' : 'black'; 13 | }, 14 | 'border-width': 1, 15 | shape: ele => (ele.data('isInitial') ? 'ellipse' : 'roundrectangle'), 16 | label: 'data(key)' 17 | } 18 | }, 19 | { 20 | selector: 'edge', 21 | style: { 22 | width: 1, 23 | 'line-color': 'black', 24 | 'text-background-opacity': 1, 25 | 'text-background-color': '#ffffff', 26 | 'target-arrow-color': 'black', 27 | 'target-arrow-shape': 'triangle', 28 | 'source-arrow-color': 'black', 29 | 'source-arrow-shape': ele => (ele.data('isInitial') ? 'circle' : 'none'), 30 | 'curve-style': ele => 31 | ele.data('isInitial') ? 'unbundled-bezier' : 'bezier', 32 | 33 | label: 'data(key)' 34 | } 35 | } 36 | ]; 37 | 38 | export default style; 39 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import addons from '@storybook/addons'; 3 | import debounce from 'lodash.debounce'; 4 | 5 | export class WithXStateGraph extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | const channel = addons.getChannel(); 9 | 10 | this.onTransition = this.onTransition.bind(this); 11 | this.resizeEmitter = debounce(evt => { 12 | if (evt.key === 'panelSizes') { 13 | channel.emit('xstate/resize'); 14 | } 15 | }, 100); 16 | 17 | channel.on('xstate/transition', this.onTransition); 18 | window.addEventListener('storage', this.resizeEmitter, false); 19 | } 20 | onTransition(nextState) { 21 | this.props.onTransition(nextState); 22 | } 23 | componentWillUnmount() { 24 | const channel = addons.getChannel(); 25 | window.removeEventListener('storage', this.resizeEmitter, false); 26 | channel.removeListener('xstate/transition', this.onTransition); 27 | } 28 | render() { 29 | const { children, machine, currentState } = this.props; 30 | const channel = addons.getChannel(); 31 | channel.emit('xstate/buildGraph', { machine, currentState }); 32 | return children; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /dist/cyto/style.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | var style = [{ 7 | selector: 'node', 8 | style: { 9 | padding: '20px', 10 | 'text-valign': function textValign(ele) { 11 | return ele.data('hasChildren') ? 'top' : 'center'; 12 | }, 13 | 'text-halign': 'center', 14 | 'background-color': 'white', 15 | 'border-color': function borderColor(ele) { 16 | return ele.data('selected') ? 'red' : ele.data('isInitial') ? 'white' : 'black'; 17 | }, 18 | 'border-width': 1, 19 | shape: function shape(ele) { 20 | return ele.data('isInitial') ? 'ellipse' : 'roundrectangle'; 21 | }, 22 | label: 'data(key)' 23 | } 24 | }, { 25 | selector: 'edge', 26 | style: { 27 | width: 1, 28 | 'line-color': 'black', 29 | 'text-background-opacity': 1, 30 | 'text-background-color': '#ffffff', 31 | 'target-arrow-color': 'black', 32 | 'target-arrow-shape': 'triangle', 33 | 'source-arrow-color': 'black', 34 | 'source-arrow-shape': function sourceArrowShape(ele) { 35 | return ele.data('isInitial') ? 'circle' : 'none'; 36 | }, 37 | 'curve-style': function curveStyle(ele) { 38 | return ele.data('isInitial') ? 'unbundled-bezier' : 'bezier'; 39 | }, 40 | 41 | label: 'data(key)' 42 | } 43 | }]; 44 | 45 | exports.default = style; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "storybook-addon-xstate", 3 | "version": "0.0.1", 4 | "description": "", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "babel -d ./dist ./src", 9 | "storybook": "start-storybook -p 6006", 10 | "build-storybook": "build-storybook" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/do-wa/xstate-addon.git" 15 | }, 16 | "keywords": [ 17 | "storybook", 18 | "addon", 19 | "xstate" 20 | ], 21 | "author": "Dominik Walnsch", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/do-wa/xstate-addon/issues" 25 | }, 26 | "homepage": "https://github.com/do-wa/xstate-addon#readme", 27 | "devDependencies": { 28 | "babel-cli": "^6.26.0", 29 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 30 | "babel-preset-es2015": "^6.24.1", 31 | "babel-preset-react": "^6.24.1", 32 | "react": "^16.2.0", 33 | "react-dom": "^16.2.0", 34 | "xstate": "^3.2.1", 35 | "@storybook/react": "^3.4.4", 36 | "@storybook/addon-actions": "^3.4.4", 37 | "@storybook/addon-links": "^3.4.4", 38 | "@storybook/addons": "^3.4.4", 39 | "babel-core": "^6.26.3" 40 | }, 41 | "peerDependencies": { 42 | "@storybook/addons": "^3.3.0", 43 | "react": "*" 44 | }, 45 | "dependencies": { 46 | "cytoscape": "^3.2.8", 47 | "cytoscape-cose-bilkent": "^4.0.0", 48 | "lodash.debounce": "^4.0.8", 49 | "ramda": "^0.25.0" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/cyto/index.js: -------------------------------------------------------------------------------- 1 | import c from 'cytoscape'; 2 | import coseBilkent from 'cytoscape-cose-bilkent'; 3 | 4 | import layout from './layout'; 5 | import style from './style'; 6 | 7 | c.use(coseBilkent); 8 | let cy; 9 | export const render = (domElement, graph, onEventClicked) => { 10 | if (cy) { 11 | cy.remove(); 12 | } 13 | cy = c({ 14 | container: domElement, 15 | layout, 16 | elements: graph, 17 | style 18 | }); 19 | cy.on('tap', evt => { 20 | const target = evt.target; 21 | if (target.group && target.group() === 'edges') { 22 | onEventClicked(target.data('key')); 23 | } 24 | }); 25 | 26 | const resetSelected = () => { 27 | const curEles = cy.filter((ele, i) => ele.data('selected')); 28 | curEles.forEach(element => { 29 | element.data('selected', false); 30 | element.style('border-color', 'black'); 31 | }); 32 | }; 33 | const setAsSelected = id => { 34 | const nextEl = cy.getElementById(id); 35 | nextEl.style('border-color', 'red'); 36 | nextEl.data('selected', true); 37 | }; 38 | return { 39 | setState(next) { 40 | resetSelected(); 41 | if (typeof next === 'string') { 42 | setAsSelected(next); 43 | } else { 44 | const nextEles = Object.keys(next); 45 | nextEles.forEach(key => { 46 | setAsSelected(key); 47 | setAsSelected(next[key]); 48 | }); 49 | } 50 | }, 51 | resize() { 52 | cy.resize(); 53 | cy.fit(); 54 | }, 55 | remove() { 56 | cy.remove(); 57 | } 58 | }; 59 | }; 60 | -------------------------------------------------------------------------------- /stories/index.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Machine } from 'xstate'; 3 | 4 | import { storiesOf } from '@storybook/react'; 5 | import { action } from '@storybook/addon-actions'; 6 | import { linkTo } from '@storybook/addon-links'; 7 | import { WithXStateGraph } from '../src/index'; 8 | 9 | import { Button, Welcome } from '@storybook/react/demo'; 10 | import TrafficLight from './helper/TrafficLight'; 11 | import StateProvider from './helper/StateProvider'; 12 | 13 | const lightMachine = Machine({ 14 | id: 'light', 15 | initial: 'green', 16 | states: { 17 | green: { 18 | on: { 19 | TIMER: 'yellow' 20 | } 21 | }, 22 | yellow: { 23 | on: { 24 | TIMER: 'red' 25 | } 26 | }, 27 | red: { 28 | on: { 29 | TIMER: 'green' 30 | }, 31 | initial: 'walk', 32 | states: { 33 | walk: { 34 | on: { 35 | PED_TIMER: 'wait' 36 | } 37 | }, 38 | wait: { 39 | on: { 40 | PED_TIMER: 'stop' 41 | } 42 | }, 43 | stop: {} 44 | } 45 | } 46 | } 47 | }); 48 | 49 | storiesOf('Welcome', module).add('to Storybook', () => ( 50 | 51 | )); 52 | 53 | storiesOf('LightMachine', module).add('Example', () => { 54 | return ( 55 | ( 58 |
59 | {JSON.stringify(currentState)} 60 | 65 | 66 | 67 |
68 | )} 69 | /> 70 | ); 71 | }); 72 | -------------------------------------------------------------------------------- /src/register.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import addons from '@storybook/addons'; 3 | import style from './cyto/style'; 4 | 5 | import { render } from './cyto'; 6 | import { build } from './graphBuilder/statechart'; 7 | 8 | const styles = { 9 | cy: { 10 | height: '100%', 11 | width: '100%' 12 | } 13 | }; 14 | 15 | class XStateGraph extends React.Component { 16 | constructor(...args) { 17 | super(...args); 18 | this.buildGraph = this.buildGraph.bind(this); 19 | this.resizeGraph = this.resizeGraph.bind(this); 20 | this.curMachine = ''; 21 | } 22 | 23 | resizeGraph() { 24 | if (this.graph) this.graph.resize(); 25 | } 26 | buildGraph({ machine, currentState }) { 27 | if (machine && currentState) { 28 | if (this.curMachine !== machine.id) { 29 | this.curMachine = machine.id; 30 | this.graph = render(this.cNode, build(machine, currentState), event => { 31 | const { channel } = this.props; 32 | channel.emit('xstate/transition', event); 33 | }); 34 | } 35 | this.graph.setState(currentState); 36 | } 37 | } 38 | componentDidUpdate() { 39 | this.resizeGraph(); 40 | } 41 | componentDidMount() { 42 | const { channel, api } = this.props; 43 | channel.on('xstate/buildGraph', this.buildGraph); 44 | channel.on('xstate/resize', this.resizeGraph); 45 | } 46 | 47 | componentWillUnmount() { 48 | this.unmounted = true; 49 | const { channel, api } = this.props; 50 | channel.removeListener('xstate/buildGraph', this.buildGraph); 51 | channel.removeListener('xstate/resize', this.resizeGraph); 52 | this.graph.remove(); 53 | } 54 | 55 | render() { 56 | return
(this.cNode = el)} />; 57 | } 58 | } 59 | 60 | addons.register('xstate/machine', api => { 61 | addons.addPanel('xstate/machine/graph', { 62 | title: 'xstate', 63 | render: () => 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /src/graphBuilder/statechart.js: -------------------------------------------------------------------------------- 1 | import { getNodes } from 'xstate/lib/graph'; 2 | import { getActionType } from 'xstate/lib/utils'; 3 | const getEdges = node => { 4 | const edges = []; 5 | if (node.states) { 6 | Object.keys(node.states).forEach(stateKey => { 7 | edges.push(...getEdges(node.states[stateKey])); 8 | }); 9 | } 10 | 11 | Object.keys(node.on || {}).forEach(event => { 12 | edges.push(...getEventNodes(node, event)); 13 | }); 14 | return edges; 15 | }; 16 | 17 | const getEventNodes = (node, event) => { 18 | const transitions = node.on[event] || []; 19 | 20 | return transitions.map(transition => { 21 | return { 22 | group: 'edges', 23 | data: { 24 | id: `${node.key}${event}`, 25 | source: node.key, 26 | target: transition.target, 27 | key: event, 28 | 29 | actions: transition.actions 30 | ? getActionType(transition.actions.map(getActionType)) 31 | : [] 32 | } 33 | }; 34 | }); 35 | }; 36 | 37 | const createInitialNodes = (node, parent = '') => { 38 | let newNodes = []; 39 | if (node.initial) { 40 | newNodes.push({ 41 | group: 'nodes', 42 | data: { 43 | id: `${node.key}.initial`, 44 | parent, 45 | isInitial: true 46 | } 47 | }); 48 | newNodes.push({ 49 | group: 'edges', 50 | data: { 51 | isInitial: true, 52 | id: `${node.key}.initial.edge`, 53 | source: `${node.key}.initial`, 54 | target: `${node.initial}`, 55 | parent 56 | } 57 | }); 58 | } 59 | return newNodes; 60 | }; 61 | 62 | export const build = (machine, currentState) => { 63 | const nodes = getNodes(machine); 64 | const graphNodes = nodes.reduce((acc, node, idx) => { 65 | const { path, key } = node; 66 | let parent = path.length > 1 ? path[path.length - 2] : ''; 67 | acc.push(...createInitialNodes(node, node.key)); 68 | acc.push(...getEdges(node)); 69 | acc.push({ 70 | group: 'nodes', 71 | data: { 72 | path, 73 | id: key, 74 | key, 75 | parent, 76 | hasChildren: Object.keys(node.states).length 77 | } 78 | }); 79 | return acc; 80 | }, []); 81 | graphNodes.push(...createInitialNodes({ initial: machine.initial, key: '' })); 82 | return graphNodes; 83 | }; 84 | -------------------------------------------------------------------------------- /dist/cyto/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.render = undefined; 7 | 8 | var _cytoscape = require('cytoscape'); 9 | 10 | var _cytoscape2 = _interopRequireDefault(_cytoscape); 11 | 12 | var _cytoscapeCoseBilkent = require('cytoscape-cose-bilkent'); 13 | 14 | var _cytoscapeCoseBilkent2 = _interopRequireDefault(_cytoscapeCoseBilkent); 15 | 16 | var _layout = require('./layout'); 17 | 18 | var _layout2 = _interopRequireDefault(_layout); 19 | 20 | var _style = require('./style'); 21 | 22 | var _style2 = _interopRequireDefault(_style); 23 | 24 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 25 | 26 | _cytoscape2.default.use(_cytoscapeCoseBilkent2.default); 27 | var cy = void 0; 28 | var render = exports.render = function render(domElement, graph, onEventClicked) { 29 | if (cy) { 30 | cy.remove(); 31 | } 32 | cy = (0, _cytoscape2.default)({ 33 | container: domElement, 34 | layout: _layout2.default, 35 | elements: graph, 36 | style: _style2.default 37 | }); 38 | cy.on('tap', function (evt) { 39 | var target = evt.target; 40 | if (target.group && target.group() === 'edges') { 41 | onEventClicked(target.data('key')); 42 | } 43 | }); 44 | 45 | var resetSelected = function resetSelected() { 46 | var curEles = cy.filter(function (ele, i) { 47 | return ele.data('selected'); 48 | }); 49 | curEles.forEach(function (element) { 50 | element.data('selected', false); 51 | element.style('border-color', 'black'); 52 | }); 53 | }; 54 | var setAsSelected = function setAsSelected(id) { 55 | var nextEl = cy.getElementById(id); 56 | nextEl.style('border-color', 'red'); 57 | nextEl.data('selected', true); 58 | }; 59 | return { 60 | setState: function setState(next) { 61 | resetSelected(); 62 | if (typeof next === 'string') { 63 | setAsSelected(next); 64 | } else { 65 | var nextEles = Object.keys(next); 66 | nextEles.forEach(function (key) { 67 | setAsSelected(key); 68 | setAsSelected(next[key]); 69 | }); 70 | } 71 | }, 72 | resize: function resize() { 73 | cy.resize(); 74 | cy.fit(); 75 | }, 76 | remove: function remove() { 77 | cy.remove(); 78 | } 79 | }; 80 | }; -------------------------------------------------------------------------------- /dist/graphBuilder/statechart.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.build = undefined; 7 | 8 | var _graph = require('xstate/lib/graph'); 9 | 10 | var _utils = require('xstate/lib/utils'); 11 | 12 | function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } 13 | 14 | var getEdges = function getEdges(node) { 15 | var edges = []; 16 | if (node.states) { 17 | Object.keys(node.states).forEach(function (stateKey) { 18 | edges.push.apply(edges, _toConsumableArray(getEdges(node.states[stateKey]))); 19 | }); 20 | } 21 | 22 | Object.keys(node.on || {}).forEach(function (event) { 23 | edges.push.apply(edges, _toConsumableArray(getEventNodes(node, event))); 24 | }); 25 | return edges; 26 | }; 27 | 28 | var getEventNodes = function getEventNodes(node, event) { 29 | var transitions = node.on[event] || []; 30 | 31 | return transitions.map(function (transition) { 32 | return { 33 | group: 'edges', 34 | data: { 35 | id: '' + node.key + event, 36 | source: node.key, 37 | target: transition.target, 38 | key: event, 39 | 40 | actions: transition.actions ? (0, _utils.getActionType)(transition.actions.map(_utils.getActionType)) : [] 41 | } 42 | }; 43 | }); 44 | }; 45 | 46 | var createInitialNodes = function createInitialNodes(node) { 47 | var parent = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ''; 48 | 49 | var newNodes = []; 50 | if (node.initial) { 51 | newNodes.push({ 52 | group: 'nodes', 53 | data: { 54 | id: node.key + '.initial', 55 | parent: parent, 56 | isInitial: true 57 | } 58 | }); 59 | newNodes.push({ 60 | group: 'edges', 61 | data: { 62 | isInitial: true, 63 | id: node.key + '.initial.edge', 64 | source: node.key + '.initial', 65 | target: '' + node.initial, 66 | parent: parent 67 | } 68 | }); 69 | } 70 | return newNodes; 71 | }; 72 | 73 | var build = exports.build = function build(machine, currentState) { 74 | var nodes = (0, _graph.getNodes)(machine); 75 | var graphNodes = nodes.reduce(function (acc, node, idx) { 76 | var path = node.path, 77 | key = node.key; 78 | 79 | var parent = path.length > 1 ? path[path.length - 2] : ''; 80 | acc.push.apply(acc, _toConsumableArray(createInitialNodes(node, node.key))); 81 | acc.push.apply(acc, _toConsumableArray(getEdges(node))); 82 | acc.push({ 83 | group: 'nodes', 84 | data: { 85 | path: path, 86 | id: key, 87 | key: key, 88 | parent: parent, 89 | hasChildren: Object.keys(node.states).length 90 | } 91 | }); 92 | return acc; 93 | }, []); 94 | graphNodes.push.apply(graphNodes, _toConsumableArray(createInitialNodes({ initial: machine.initial, key: '' }))); 95 | return graphNodes; 96 | }; -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.WithXStateGraph = undefined; 7 | 8 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 9 | 10 | var _react = require('react'); 11 | 12 | var _react2 = _interopRequireDefault(_react); 13 | 14 | var _addons = require('@storybook/addons'); 15 | 16 | var _addons2 = _interopRequireDefault(_addons); 17 | 18 | var _lodash = require('lodash.debounce'); 19 | 20 | var _lodash2 = _interopRequireDefault(_lodash); 21 | 22 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 23 | 24 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 25 | 26 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 27 | 28 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 29 | 30 | var WithXStateGraph = exports.WithXStateGraph = function (_React$Component) { 31 | _inherits(WithXStateGraph, _React$Component); 32 | 33 | function WithXStateGraph(props) { 34 | _classCallCheck(this, WithXStateGraph); 35 | 36 | var _this = _possibleConstructorReturn(this, (WithXStateGraph.__proto__ || Object.getPrototypeOf(WithXStateGraph)).call(this, props)); 37 | 38 | var channel = _addons2.default.getChannel(); 39 | 40 | _this.onTransition = _this.onTransition.bind(_this); 41 | _this.resizeEmitter = (0, _lodash2.default)(function (evt) { 42 | if (evt.key === 'panelSizes') { 43 | channel.emit('xstate/resize'); 44 | } 45 | }, 100); 46 | 47 | channel.on('xstate/transition', _this.onTransition); 48 | window.addEventListener('storage', _this.resizeEmitter, false); 49 | return _this; 50 | } 51 | 52 | _createClass(WithXStateGraph, [{ 53 | key: 'onTransition', 54 | value: function onTransition(nextState) { 55 | this.props.onTransition(nextState); 56 | } 57 | }, { 58 | key: 'componentWillUnmount', 59 | value: function componentWillUnmount() { 60 | var channel = _addons2.default.getChannel(); 61 | window.removeEventListener('storage', this.resizeEmitter, false); 62 | channel.removeListener('xstate/transition', this.onTransition); 63 | } 64 | }, { 65 | key: 'render', 66 | value: function render() { 67 | var _props = this.props, 68 | children = _props.children, 69 | machine = _props.machine, 70 | currentState = _props.currentState; 71 | 72 | var channel = _addons2.default.getChannel(); 73 | channel.emit('xstate/buildGraph', { machine: machine, currentState: currentState }); 74 | return children; 75 | } 76 | }]); 77 | 78 | return WithXStateGraph; 79 | }(_react2.default.Component); -------------------------------------------------------------------------------- /dist/register.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 4 | 5 | var _react = require('react'); 6 | 7 | var _react2 = _interopRequireDefault(_react); 8 | 9 | var _addons = require('@storybook/addons'); 10 | 11 | var _addons2 = _interopRequireDefault(_addons); 12 | 13 | var _style = require('./cyto/style'); 14 | 15 | var _style2 = _interopRequireDefault(_style); 16 | 17 | var _cyto = require('./cyto'); 18 | 19 | var _statechart = require('./graphBuilder/statechart'); 20 | 21 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 22 | 23 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 24 | 25 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 26 | 27 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 28 | 29 | var styles = { 30 | cy: { 31 | height: '100%', 32 | width: '100%' 33 | } 34 | }; 35 | 36 | var XStateGraph = function (_React$Component) { 37 | _inherits(XStateGraph, _React$Component); 38 | 39 | function XStateGraph() { 40 | var _ref; 41 | 42 | _classCallCheck(this, XStateGraph); 43 | 44 | for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { 45 | args[_key] = arguments[_key]; 46 | } 47 | 48 | var _this = _possibleConstructorReturn(this, (_ref = XStateGraph.__proto__ || Object.getPrototypeOf(XStateGraph)).call.apply(_ref, [this].concat(args))); 49 | 50 | _this.buildGraph = _this.buildGraph.bind(_this); 51 | _this.resizeGraph = _this.resizeGraph.bind(_this); 52 | _this.curMachine = ''; 53 | return _this; 54 | } 55 | 56 | _createClass(XStateGraph, [{ 57 | key: 'resizeGraph', 58 | value: function resizeGraph() { 59 | if (this.graph) this.graph.resize(); 60 | } 61 | }, { 62 | key: 'buildGraph', 63 | value: function buildGraph(_ref2) { 64 | var _this2 = this; 65 | 66 | var machine = _ref2.machine, 67 | currentState = _ref2.currentState; 68 | 69 | if (machine && currentState) { 70 | if (this.curMachine !== machine.id) { 71 | this.curMachine = machine.id; 72 | this.graph = (0, _cyto.render)(this.cNode, (0, _statechart.build)(machine, currentState), function (event) { 73 | var channel = _this2.props.channel; 74 | 75 | channel.emit('xstate/transition', event); 76 | }); 77 | } 78 | this.graph.setState(currentState); 79 | } 80 | } 81 | }, { 82 | key: 'componentDidUpdate', 83 | value: function componentDidUpdate() { 84 | this.resizeGraph(); 85 | } 86 | }, { 87 | key: 'componentDidMount', 88 | value: function componentDidMount() { 89 | var _props = this.props, 90 | channel = _props.channel, 91 | api = _props.api; 92 | 93 | channel.on('xstate/buildGraph', this.buildGraph); 94 | channel.on('xstate/resize', this.resizeGraph); 95 | } 96 | }, { 97 | key: 'componentWillUnmount', 98 | value: function componentWillUnmount() { 99 | this.unmounted = true; 100 | var _props2 = this.props, 101 | channel = _props2.channel, 102 | api = _props2.api; 103 | 104 | channel.removeListener('xstate/buildGraph', this.buildGraph); 105 | channel.removeListener('xstate/resize', this.resizeGraph); 106 | this.graph.remove(); 107 | } 108 | }, { 109 | key: 'render', 110 | value: function render() { 111 | var _this3 = this; 112 | 113 | return _react2.default.createElement('div', { style: styles.cy, id: 'cy', ref: function ref(el) { 114 | return _this3.cNode = el; 115 | } }); 116 | } 117 | }]); 118 | 119 | return XStateGraph; 120 | }(_react2.default.Component); 121 | 122 | _addons2.default.register('xstate/machine', function (api) { 123 | _addons2.default.addPanel('xstate/machine/graph', { 124 | title: 'xstate', 125 | render: function render() { 126 | return _react2.default.createElement(XStateGraph, { channel: _addons2.default.getChannel(), api: api }); 127 | } 128 | }); 129 | }); --------------------------------------------------------------------------------