├── .npmignore
├── .gitignore
├── example
├── src
│ ├── index.css
│ ├── bindings
│ │ ├── main
│ │ │ ├── index.js
│ │ │ ├── Off
│ │ │ │ └── index.js
│ │ │ └── On
│ │ │ │ └── index.js
│ │ └── index.js
│ ├── graph.json
│ ├── index.js
│ └── registerServiceWorker.js
├── README.md
├── .gitignore
├── package.json
└── public
│ └── index.html
├── test
├── ui.test.js
└── makeTest.js
├── src
└── RosmaroReact.js
├── package.json
├── LICENSE.txt
└── README.md
/.npmignore:
--------------------------------------------------------------------------------
1 | src
2 | test
3 | example
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | lib
2 | .DS_Store
3 | node_modules
--------------------------------------------------------------------------------
/example/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: sans-serif;
5 | }
6 |
--------------------------------------------------------------------------------
/example/src/bindings/main/index.js:
--------------------------------------------------------------------------------
1 | import {defaultHandler} from 'rosmaro-binding-utils';
2 |
3 | export default () => ({handler: defaultHandler});
--------------------------------------------------------------------------------
/example/README.md:
--------------------------------------------------------------------------------
1 | Test environment for Rosmaro-React.
2 |
3 | This project was bootstrapped with [Create React App](https://github.com/facebookincubator/create-react-app).
--------------------------------------------------------------------------------
/test/ui.test.js:
--------------------------------------------------------------------------------
1 | const {By, until} = require('selenium-webdriver');
2 | const makeTest = require('./makeTest');
3 |
4 | test('toggling on/off', makeTest(async ({click, getAttr}) => {
5 | await click('#off-button');
6 | await click('#on-button');
7 | await click('#off-button');
8 | }));
9 |
--------------------------------------------------------------------------------
/example/src/bindings/index.js:
--------------------------------------------------------------------------------
1 | import handler0 from './main/index.js';
2 | import handler1 from './main/Off/index.js';
3 | import handler2 from './main/On/index.js';
4 | export default opts => ({
5 | 'main': handler0(opts),
6 | 'main:Off': handler1(opts),
7 | 'main:On': handler2(opts)
8 | });
--------------------------------------------------------------------------------
/src/RosmaroReact.js:
--------------------------------------------------------------------------------
1 | import {connect} from 'react-redux'
2 |
3 | const makeRender = model => state => model({state, action: ({type: 'RENDER'})}).result.data;
4 |
5 | const mapStateToProps = selector => wholeState => selector(wholeState).state;
6 |
7 | export default ({selector, model}) =>
8 | connect(mapStateToProps(selector))(makeRender(model));
--------------------------------------------------------------------------------
/example/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
--------------------------------------------------------------------------------
/example/src/bindings/main/Off/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {connect} from 'react-redux';
3 |
4 | const View = connect()(({dispatch}) =>
5 |
9 | );
10 |
11 | export default ({makeHandler}) => ({
12 | handler: makeHandler({
13 |
14 | CLICK: () => ({arrow: 'clicked'}),
15 |
16 | RENDER: () => ,
17 |
18 | })
19 | });
--------------------------------------------------------------------------------
/example/src/bindings/main/On/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {connect} from 'react-redux';
3 |
4 | // This component is connected to the redux store,
5 | // so it can dispatch actions.
6 | const View = connect()(({dispatch}) =>
7 |
11 | );
12 |
13 | // The makeHandler function is passed to the makeBindings function
14 | // in the src/index.js file.
15 | export default ({makeHandler}) => ({
16 | handler: makeHandler({
17 |
18 | CLICK: () => ({arrow: 'clicked'}),
19 |
20 | RENDER: () => ,
21 |
22 | })
23 | });
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "react": "^16.2.0",
7 | "react-dom": "^16.2.0",
8 | "react-redux": "^5.0.7",
9 | "react-scripts": "1.1.4",
10 | "redux": "^4.0.0",
11 | "redux-saga": "^0.16.0",
12 | "rosmaro": "^0.8.1",
13 | "rosmaro-binding-utils": "0.0.4",
14 | "rosmaro-react": "file:../",
15 | "rosmaro-redux": "0.0.2",
16 | "lodash": ">=4.17.5"
17 | },
18 | "scripts": {
19 | "start": "react-scripts start",
20 | "build": "react-scripts build",
21 | "test": "react-scripts test --env=jsdom",
22 | "eject": "react-scripts eject"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/example/src/graph.json:
--------------------------------------------------------------------------------
1 | {
2 | "main": {
3 | "type": "graph",
4 | "nodes": {
5 | "On": "On",
6 | "Off": "Off"
7 | },
8 | "arrows": {
9 | "On": {
10 | "clicked": {
11 | "target": "Off",
12 | "entryPoint": "start"
13 | }
14 | },
15 | "Off": {
16 | "clicked": {
17 | "target": "On",
18 | "entryPoint": "start"
19 | }
20 | }
21 | },
22 | "entryPoints": {
23 | "start": {
24 | "target": "Off",
25 | "entryPoint": "start"
26 | }
27 | }
28 | },
29 | "On": {
30 | "type": "leaf"
31 | },
32 | "Off": {
33 | "type": "leaf"
34 | }
35 | }
--------------------------------------------------------------------------------
/test/makeTest.js:
--------------------------------------------------------------------------------
1 | // Related to the webdriver
2 | const {Builder, promise, By, until} = require('selenium-webdriver');
3 | require('chromedriver');
4 |
5 | // Settings
6 | const settings = {
7 | appRootUrl: 'http://localhost:3000'
8 | };
9 |
10 | // Global Config
11 | jest.setTimeout(10 * 1000);
12 | promise.USE_PROMISE_MANAGER = false;
13 |
14 | const helpers = (driver, settings) => {
15 | const waitForPresent = async (cssSelector) => await driver.wait(until.elementLocated(
16 | By.css(cssSelector)
17 | ));
18 |
19 | const getAttr = async (cssSelector, attr) => {
20 | const elem = await waitForPresent(cssSelector);
21 | return elem.getAttribute(attr);
22 | };
23 |
24 | const click = async (cssSelector) => {
25 | const clickable = await waitForPresent(cssSelector);
26 | await clickable.click();
27 | };
28 |
29 | return {
30 | click,
31 | getAttr
32 | };
33 | };
34 |
35 | module.exports = testCase => async() => {
36 | const driver = await new Builder().forBrowser('chrome').build();
37 | await driver.get(settings.appRootUrl);
38 | await testCase(helpers(driver, settings));
39 | driver.quit();
40 | };
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rosmaro-react",
3 | "license": "MIT",
4 | "description": "Visual automata-based programming for React",
5 | "keywords": [
6 | "react",
7 | "automata-based programming",
8 | "finite state machine"
9 | ],
10 | "version": "0.0.7",
11 | "repository": {
12 | "type": "git",
13 | "url": "https://github.com/lukaszmakuch/rosmaro-react.git"
14 | },
15 | "author": "Łukasz Makuch (http://lukaszmakuch.pl)",
16 | "devDependencies": {
17 | "babel-cli": "^6.26.0",
18 | "babel-preset-env": "^1.7.0",
19 | "chromedriver": "^2.41.0",
20 | "jest": "^23.6.0",
21 | "lodash": ">=4.17.5",
22 | "selenium-webdriver": "^4.0.0-alpha.1"
23 | },
24 | "peerDependencies": {
25 | "rosmaro": "0.8.x",
26 | "rosmaro-redux": "*",
27 | "redux-saga": "*",
28 | "redux": "*",
29 | "react": ">=15.0.0"
30 | },
31 | "main": "./lib/RosmaroReact.js",
32 | "scripts": {
33 | "build": "./node_modules/.bin/babel ./src --out-dir ./lib --presets env",
34 | "test": "./node_modules/.bin/jest ./test",
35 | "prepublish": "npm run build"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2018 Łukasz Makuch
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
13 | all 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
21 | THE SOFTWARE.
--------------------------------------------------------------------------------
/example/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import graph from './graph.json';
5 | import rosmaro from 'rosmaro';
6 | import makeBindings from './bindings';
7 | import {Provider} from 'react-redux'
8 | import {typeHandler, partialReturns, defaultHandler} from 'rosmaro-binding-utils';
9 | import rosmaroComponent from 'rosmaro-react';
10 | import {makeReducer, effectDispatcher} from 'rosmaro-redux';
11 | import createSagaMiddleware from 'redux-saga';
12 | import {createStore, applyMiddleware} from 'redux';
13 | import registerServiceWorker from './registerServiceWorker';
14 |
15 | const makeHandler = opts => partialReturns(typeHandler({defaultHandler})(opts));
16 | const bindings = makeBindings({makeHandler});
17 | const model = rosmaro({graph, bindings});
18 |
19 | const saga = function* () {};
20 |
21 | const rootReducer = makeReducer(model);
22 |
23 | const sagaMiddleware = createSagaMiddleware();
24 | const store = createStore(
25 | rootReducer,
26 | applyMiddleware(effectDispatcher, sagaMiddleware)
27 | );
28 | sagaMiddleware.run(saga);
29 |
30 | const App = rosmaroComponent({
31 | model,
32 | selector: state => state
33 | });
34 |
35 | ReactDOM.render(
36 |
37 |
38 | ,
39 | document.getElementById('root')
40 | );
41 | registerServiceWorker();
42 |
--------------------------------------------------------------------------------
/example/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
16 | Rosmaro React App
17 |
18 |
19 |
22 |
23 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/example/src/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | // In production, we register a service worker to serve assets from local cache.
2 |
3 | // This lets the app load faster on subsequent visits in production, and gives
4 | // it offline capabilities. However, it also means that developers (and users)
5 | // will only see deployed updates on the "N+1" visit to a page, since previously
6 | // cached resources are updated in the background.
7 |
8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
9 | // This link also includes instructions on opting out of this behavior.
10 |
11 | const isLocalhost = Boolean(
12 | window.location.hostname === 'localhost' ||
13 | // [::1] is the IPv6 localhost address.
14 | window.location.hostname === '[::1]' ||
15 | // 127.0.0.1/8 is considered localhost for IPv4.
16 | window.location.hostname.match(
17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
18 | )
19 | );
20 |
21 | export default function register() {
22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
23 | // The URL constructor is available in all browsers that support SW.
24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
25 | if (publicUrl.origin !== window.location.origin) {
26 | // Our service worker won't work if PUBLIC_URL is on a different origin
27 | // from what our page is served on. This might happen if a CDN is used to
28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
29 | return;
30 | }
31 |
32 | window.addEventListener('load', () => {
33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
34 |
35 | if (isLocalhost) {
36 | // This is running on localhost. Lets check if a service worker still exists or not.
37 | checkValidServiceWorker(swUrl);
38 | } else {
39 | // Is not local host. Just register service worker
40 | registerValidSW(swUrl);
41 | }
42 | });
43 | }
44 | }
45 |
46 | function registerValidSW(swUrl) {
47 | navigator.serviceWorker
48 | .register(swUrl)
49 | .then(registration => {
50 | registration.onupdatefound = () => {
51 | const installingWorker = registration.installing;
52 | installingWorker.onstatechange = () => {
53 | if (installingWorker.state === 'installed') {
54 | if (navigator.serviceWorker.controller) {
55 | // At this point, the old content will have been purged and
56 | // the fresh content will have been added to the cache.
57 | // It's the perfect time to display a "New content is
58 | // available; please refresh." message in your web app.
59 | console.log('New content is available; please refresh.');
60 | } else {
61 | // At this point, everything has been precached.
62 | // It's the perfect time to display a
63 | // "Content is cached for offline use." message.
64 | console.log('Content is cached for offline use.');
65 | }
66 | }
67 | };
68 | };
69 | })
70 | .catch(error => {
71 | console.error('Error during service worker registration:', error);
72 | });
73 | }
74 |
75 | function checkValidServiceWorker(swUrl) {
76 | // Check if the service worker can be found. If it can't reload the page.
77 | fetch(swUrl)
78 | .then(response => {
79 | // Ensure service worker exists, and that we really are getting a JS file.
80 | if (
81 | response.status === 404 ||
82 | response.headers.get('content-type').indexOf('javascript') === -1
83 | ) {
84 | // No service worker found. Probably a different app. Reload the page.
85 | navigator.serviceWorker.ready.then(registration => {
86 | registration.unregister().then(() => {
87 | window.location.reload();
88 | });
89 | });
90 | } else {
91 | // Service worker found. Proceed as normal.
92 | registerValidSW(swUrl);
93 | }
94 | })
95 | .catch(() => {
96 | console.log(
97 | 'No internet connection found. App is running in offline mode.'
98 | );
99 | });
100 | }
101 |
102 | export function unregister() {
103 | if ('serviceWorker' in navigator) {
104 | navigator.serviceWorker.ready.then(registration => {
105 | registration.unregister();
106 | });
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Rosmaro for React
2 |
3 | Leverage the power of [visual automata-based programming](https://rosmaro.js.org) to build [React](https://reactjs.org) components!
4 |
5 | This package lets you build a _React component_ which [Rosmaro](https://rosmaro.js.org) state lives in [Redux](https://redux.js.org) and effects are managed in a declarative way with [Redux-saga](https://redux-saga.js.org).
6 |
7 | ---
8 |
9 | # Using Rosmaro-React
10 |
11 | ## Installing dependencies
12 | ```
13 | $ npm install --save rosmaro rosmaro-redux redux-saga redux rosmaro-react
14 | $ mkdir -p src/bindings && touch src/graph.json
15 | ```
16 |
17 | ## The index.js file
18 | ```javascript
19 | // IMPORTING DEPENDENCIES:
20 |
21 | // This is the component factory function exported by this package.
22 | import rosmaroComponent from 'rosmaro-react';
23 |
24 | // The content of this file should be generated using the Rosmaro visual editor (https://rosmaro.js.org/editor/). For more information on drawing graphs please visit https://rosmaro.js.org/doc/#graphs
25 | import graph from './graph.json';
26 |
27 | // It's convenient to generate the bindings/index.js file using https://github.com/lukaszmakuch/rosmaro-tools
28 | import makeBindings from './bindings';
29 |
30 | // We need Rosmaro and React.
31 | import rosmaro from 'rosmaro';
32 | import React from 'react';
33 | import ReactDOM from 'react-dom';
34 |
35 | // Redux itself.
36 | import {createStore, applyMiddleware} from 'redux';
37 |
38 | // This will gives us a Rosmaro driven reducer and integrate it with Redux Saga.
39 | import {makeReducer, effectDispatcher} from 'rosmaro-redux';
40 |
41 | // We need to provide the Redux state.
42 | import {Provider} from 'react-redux';
43 |
44 | // Side-effects are handled by Redux Saga.
45 | import createSagaMiddleware from 'redux-saga';
46 |
47 | // For more information on Redux Saga please refer to the official documentation at https://redux-saga.js.org
48 | import rootSaga from './sagas';
49 |
50 | // Writing handlers is easier with https://github.com/lukaszmakuch/rosmaro-binding-utils
51 | import {typeHandler, partialReturns, defaultHandler} from 'rosmaro-binding-utils';
52 |
53 | // SETTING THINGS UP:
54 |
55 | // Makes writing handlers convenient.
56 | const makeHandler = opts => partialReturns(typeHandler({defaultHandler})(opts));
57 |
58 | // Rosmaro bindings are built with the handler factory defined above.
59 | const bindings = makeBindings({makeHandler});
60 |
61 | // This is the Rosmaro model.
62 | const model = rosmaro({graph, bindings});
63 |
64 | // The reducer is based on the Rosmaro model.
65 | const rootReducer = makeReducer(model);
66 |
67 | // For more information on connecting Rosmaro and Redux please check https://github.com/lukaszmakuch/rosmaro-redux
68 | const sagaMiddleware = createSagaMiddleware();
69 | const store = createStore(
70 | rootReducer,
71 | applyMiddleware(effectDispatcher, sagaMiddleware)
72 | );
73 | sagaMiddleware.run(rootSaga);
74 |
75 | // This component is driven by the `model` Rosmaro model.
76 | // Its state lives in the provided Redux store.
77 | // The selector function defines the exact location of the Rosmaro model state.
78 | const App = rosmaroComponent({
79 | model,
80 | selector: state => state
81 | });
82 |
83 | // Rendering the component.
84 | ReactDOM.render(
85 |
86 |
87 | ,
88 | document.getElementById('root')
89 | );
90 | ```
91 |
92 | ## Bindings
93 |
94 | The most important thing is that every handler must react to the `RENDER` action. It is supposed to return a _React component_.
95 |
96 | How the bindings are built doesn't really matter as long as they are correctly interpreted by Rosmaro. However, it's highly recommended to use [rosmaro-binding-utils](https://github.com/lukaszmakuch/rosmaro-binding-utils) and [rosmaro-tools](https://github.com/lukaszmakuch/rosmaro-tools) most of the time, as they make most of the things a lot easier and provide a directory structure.
97 |
98 | Below is an example of a simple On/Off component.
99 |
100 | We're going to focus on the `src/bindings` directory we created at the very beginning.
101 |
102 | Let's give it the following structure:
103 | ```
104 | $ tree -U
105 | .
106 | └── main
107 | ├── index.js
108 | ├── On
109 | │ └── index.js
110 | └── Off
111 | └── index.js
112 | ```
113 | The `src/bindings/main/index.js` file contains the `main` node binding. We don't need anything fancy here, so it may use the default handler provided by [rosmaro-binding-utils](https://github.com/lukaszmakuch/rosmaro-binding-utils):
114 | ```javascript
115 | // src/bindings/main/index.js
116 | import {defaultHandler} from 'rosmaro-binding-utils';
117 |
118 | export default () => ({handler: defaultHandler});
119 | ```
120 | The `src/bindings/main/On/index.js` file is the `main:On` node binding. It's a simple button which when clicked makes the node follow the `clicked` arrow.
121 | ```javascript
122 | // src/bindings/main/On/index.js
123 | import React from 'react';
124 | import {connect} from 'react-redux';
125 |
126 | // This component is connected to the redux store,
127 | // so it can dispatch actions.
128 | const View = connect()(({dispatch}) =>
129 |
133 | );
134 |
135 | // The makeHandler function is passed to the makeBindings function
136 | // in the src/index.js file.
137 | export default ({makeHandler}) => ({
138 | handler: makeHandler({
139 |
140 | CLICK: () => ({arrow: 'clicked'}),
141 |
142 | RENDER: () => ,
143 |
144 | })
145 | });
146 | ```
147 | The `src/bindings/main/Off/index.js` file is a very similar button:
148 | ```javascript
149 | // src/bindings/main/Off/index.js
150 | import React from 'react';
151 | import {connect} from 'react-redux';
152 |
153 | const View = connect()(({dispatch}) =>
154 |
158 | );
159 |
160 | export default ({makeHandler}) => ({
161 | handler: makeHandler({
162 |
163 | CLICK: () => ({arrow: 'clicked'}),
164 |
165 | RENDER: () => ,
166 |
167 | })
168 | });
169 | ```
170 | Now the `src/bindings/index.js` file, which is imported in the `src/index.js` file may be generated:
171 | ```
172 | $ cd src/bindings
173 | $ npx rosmaro-tools bindings:build .
174 | Generated index.js!
175 | ```
176 |
177 | The whole code of this example can be found in the `example` directory.
178 |
179 | ---
180 | Links:
181 | - [React documentation](http://reactjs.org) - how to build the UI layer
182 | - [Rosmaro documentation](https://rosmaro.js.org/doc) - how to model changes of behavior
183 | - [Redux documentation](https://redux.js.org) - about the object the Rosmaro state lives in
184 | - [Redux-Saga documentation](https://redux-saga.js.org) - managing side effects in a declarative way
--------------------------------------------------------------------------------