An open API service indexing awesome lists of open source software.

https://github.com/isocroft/hecter

A small (experimental & toy) library that utilizes the idea of state-charts and state machines to manage web app behavior enabling predictable & responsive UIs by executing state transition graphs
https://github.com/isocroft/hecter

app-behaviour behaviour-flow-management non-transient-domain-state predictable-uis scheduled-transitions state-charts state-machines transient-ui-state

Last synced: 3 months ago
JSON representation

A small (experimental & toy) library that utilizes the idea of state-charts and state machines to manage web app behavior enabling predictable & responsive UIs by executing state transition graphs

Awesome Lists containing this project

README

          

# hecter
A small (experimental & toy) library that utilizes the idea of state-charts and state machines to manage web app behavior enabling predictable & responsive UIs by executing state transition graphs.

## APIs

> Getters / Triggers:

let machine = new HecterMachine(transitionGraph, initialState);

- machine.get( void );
- machine.scheduleNext(eventName, actionData);
- machine.transit(eventName, actionData);
- machine.next( void );
- machine.nextAll( void );
- machine.disconnect( void );

> Hooks:

- machine.setRerenderHook( Function< state, hasError > );
- machine.setActionHandlerHook( Function< machine, action> );

## Usage

> **Hecter** can be used with any data flow management tool (e.g. Redux / MobX / Radixx )

- **asyncActions.js**

```js

/** asyncActions.js */

import axios from 'axios'

const networkRequest = (machine, action, params) => {

let source = action.data.source;
let type = String(action.type);

return (dispatch, getState) => {

const storeDispatch = function(_action){
setTimeout(() => {
dispatch(_action);
},0);
};

return axios(params).then(function(data){
source = null;
machine.scheduleNext("$AJAX_SUCCESS_RESP", null);
return storeDispatch({
type:type,
data:data
});
}).catch(function(thrownError){
source = null;
machine.scheduleNext("$AJAX_ERROR_RESP", thrownError);
return storeDispatch({
type:"",
data:null
});
});
}
};

const delay = (machine, action, promise) => (dispatch, getState) => {

//....
};

export { networkRequest, delay }
```

- **hooks.js**

```js

/** hooks.js */

import { networkRequest, delay } from './asyncActions.js'

const rerenderHookFactory = (component) => (state, hasError) => {
component._hasError = hasError;
return component.setState((prevState) => {
return Object.assign({}, prevState, {
global:state
})
});
};

const actionHandlerHookFactory = (storeorActionDispatcher) => (machine, action) => {
switch(action.type){
case "MAKE_ENTRY":
action = networkRequest(machine, action, {
method:"GET",
url:"https://jsonplaceholder.typicode.com/"+action.data.text,
cancelToken:action.data.source.token
});
break;
}

return storeorActionDispatcher.dispatch(action);
};

export { actionHandlerHookFactory, rerenderHookFactory }
```

- **machine.js**

```js

/** machine.js */

const stateGraph = {
idle:{
$SEARCH_BUTTON_CLICK:{
guard:function(stateGraph, currentState, actionData){
return actionData.text.length > 0
? 'searching'
: {current:null, error:new Error("No text entered in form...")};
},
action:function(actionData = null){
return {type:"MAKE_ENTRY",data:actionData};
},
parallel:function(stateGraph, currentState){
return "form.loading"
}
}
},
searching:{
$AJAX_SUCCESS_RESP:{
guard:function(stateGraph, currentState, actionData){
return 'idle';
},
action:null,
parallel:function(stateGraph, currentState){
return "form.ready"
}
},
$AJAX_ERROR_RESP:{
guard:function(stateGraph, currentState, actionData){
return actionData instanceof Error
? {current:'idle', error:actionData}
: {current:null, error:new Error("Invalid transition...")};
},
action:null,
parallel:function(stateGraph, currentState){
return "form.ready";
}
},
$CANCEL_BUTTON_CLICK:{
guard:function(stateGraph, currentState, actionData){
return 'canceled';
},
action:null,
parallel:function(stateGraph, currentState){
return "form.ready"
}
}
},
canceled:{
$BUTTON_CLICK:{
guard:function(stateGraph, currentState, actionData){
return 'searching'
},
action:null,
parallel:function(stateGraph, currentState){
return "form.loading"
}
}
}
};

const machine = new HecterMachine(stateGraph, {
current:'idle',
parallel:'form.ready',
error:null
});

export default machine
```

- **store.js**

```js

/** store.js */

import { createStore, applyMiddleware } from 'redux'
import machine from './machine.js'

const thunkMiddleware = ({ dispatch, getState }) => next => action => {

if (typeof action === 'function') {
return action(dispatch, getState);
}

return next(action);
};

const loggerMiddleware = ({ getState }) => next => action => {

console.log("PREV APP DATA STATE: ", getState());
next(action);
console.log("NEXT APP DATA STATE: ", getState());

};

const store = createStore(
function(state, action){
switch(action.type){
case "MAKE_ENTRY":
return Object.assign({}, state, {
items:action.data
});
break;
default:
return state
}
},
{items:[]},
applyMiddleware(thunkMiddleware, loggerMiddleware)
);

store.subscribe(machine.nextAll.bind(machine));

export default store

```
- **FormUnitComponent.js**

```js

/** FormUnitComponent.js */

import machine from './machine.js'
import axios from 'axios'

const source = null

const searchButtonClick = event => {

const CancelToken = axios.CancelToken;
const source = CancelToken.source();
source = source

machine.transit('$SEARCH_BUTTON_CLICK', {text:event.target.form.elements['query'].value, source});
}

const cancelButtonClick = event => {

if(source !== null)
source.cancel();

machine.transit('$CANCEL_BUTTON_CLICK', null);
}

const renderInput = (data, behavior) => (
behavior.parallel == "form.loading"
?
:
)

const renderSearchButton = (data, behavior) => (
behavior.parallel == "form.loading"
? Searching...
: Search
)

const renderCancelButton = (data, behavior) => (
behavior.current === 'idle'
? Cancel
: (behavior.current === 'canceled'
? Canceling...
: Cancel)
)

const renderList = (data, behavior) => (
data.length
?


    data.map(item =>
  • item

  • );

:

No search data yet!


)

const renderLoadingMessage = (data, behavior) => {
let message = `Loading Search Results...`;

return (


{message}


);
}

const renderErrorMessage = (data, behavior) => {
let message = `Error Loading Search Results: ${behavior.error}`;

return (


{message}


);
}

const renderResult = (data, behavior) => (
behavior.parallel === 'form.loading'
? renderLoadingMessage(data, behavior)
: behavior.error !== null ? renderErrorMessage(data, behavior) : renderList(data, behavior)
)

const FormUnit = props =>


e.preventDefault() }>
{renderInput(null, props.behavior)}
{renderSearchButton(null, props.behavior)}
{renderCancelButton(null, props.behavior)}

{renderResult(props.items, props.behavior)}


export default FormUnit

```

- **App.js**

```js

/** App.js */

import React, { Component } from 'react'
import { actionHandlerHookFactory, rerenderHookFactory } from './hooks.js'
import FormUnit from './FormUnitComponent.js'

class App extends Component {

constructor(props){

super(props)

this.state = {
global:props.machine.get(), // any global state which every part/component of our app should be tracking continually
values:{}, // any values e.g. form values that need to be tracked
counts:{}, // any counter/counters that need to be tracked
statuses:{} // any status/statuses that need to be tracked
};

props.machine.setRerenderHook(rerenderHookFactory(this));
props.machine.setActionHandlerHook(actionHandlerHookFactory(
props.store
));
}

componentDidUnmount(){

this.props.machine.disconnect();
}

render(){
let data = this.props.store.getState();


}
}

export default App

```

- **main.js**

```js

/** main.js */

import ReactDOM from 'react-dom'
import App from './App.js'
import store from './store.js'
import machine from './machine.js'

ReactDOM.render(, document.getElementById("root"))

```
## License

MIT