├── 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 |
(
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 | });
--------------------------------------------------------------------------------