├── .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 --------------------------------------------------------------------------------