strikejs-react
Version:
A state management framework for ReactJS applications.
230 lines (210 loc) • 6.34 kB
text/typescript
import {createPool,Pool} from './Pool';
import {Reducer} from './Reducer';
import {IMiddleware} from './Middleware';
import {IManagedState,createManagedState} from './ManagedState';
import {StatefulComponent} from './StatefulComponent';
import * as React from 'react';
import {Action} from './Action';
/**
* Returns value at a given key with in an object literal.
*
* @export
* @param {object} object the object to use
* @param {string} path the path to return its value
* @param {string} p path separator, defaults to '.'
* @returns {any} the value at the given key
*/
export function getDataAt<T>(object: any, path: string, p: string): T {
let o: any = object,
key: string,
temp: any,
pathSep: string = p ? p : '.',
list: string[] = path.split(pathSep);
while ((key = list.shift()) && (temp = o[key]) && (o = temp));
return temp;
}
export interface DispatchFn {
<V,R>(fn:(dispatch:DispatchFn,getState:StateGetter,extra:V)=>R,extra:V):R;
<V>(fn:(dispatch:DispatchFn,getState:StateGetter)=>V):V;
<V>(action:Action,cb:()=>void):void;
(action:Action):Promise<void>;
}
export type StateGetter = <T>(key:string)=>T;
/**
* Represents store configuration options
*/
export interface StoreCfg {
/**
* @type {boolean}
* @description whether the store to track changes.
*/
trackChanges?:boolean;
/**
* @type {Array<Middleware>}
* @description an array of middlewares to add to the store.
*/
middlewares?:IMiddleware[];
}
/**
* A state container store
*/
export interface IStore {
/**
* Connects a component to the store.
* @param {StatefulComponent<T>} el the component to connect to the store.
* @returns {IStore} the store instance.
*/
connect<T,V>(el:StatefulComponent<T,V>):this;
/**
* Disconnects a component from the store.
* @param {StatefulComponent<T>} el the component to disconnect.
* @returns {IStore} the store instance.
*/
disconnect<T,V>(el:StatefulComponent<T,V>):this;
/**
* Set the state of a specific component within the state.
* @param {string} key the component's key.
* @param {T} val the updated state.
* @returns {IStore} the store instance;
*/
setStateAt<T>(key:string,val:T):this;
/**
* Get the state of a specific component.
* @param {string} key the component's key.
* @returns {T} the state of the component.
*/
getStateAt<T>(key:string):T;
/**
* Dispatches an action within the store.
* @param {Action} action the action to dispatch.
* @returns {Promise<void>} if promises are supported by the browser.
* The promise is resolved when the view is updated. Otherwise, returns `void`.
* @throws {Error} if no action is provided
*/
dispatch:DispatchFn;
}
/**
* Creates a state container instance with the provided configurations
* @param {StoreCfg} cfg the store configurations.
* @returns {IStore}
*/
export function createStore(cfg:StoreCfg):IStore{
let components:Dictionary<StatefulComponent<any,any>> = {};
let actions:Dictionary<Action> = {};
let backlog:Action[] = [];
let middlewares:IMiddleware[] = cfg.middlewares || [];
let trackChanges = typeof cfg.trackChanges === "undefined"?false:cfg.trackChanges;
let o:IStore = null;
let state = {};
let pool = createPool(createManagedState);
function connect<T,V>(el:StatefulComponent<T,V>):IStore{
var key = el.getStateKey();
components[key] = el;
state[key] = el.state || {};
return o;
}
function disconnect<T,V>(el:StatefulComponent<T,V>):IStore{
delete components[el.getStateKey()];
delete state[el.getStateKey()];
return o;
}
function getStateAt<T>(key:string){
return getDataAt<T>(state,key,'.');//state[key];
}
function setStateAt<T>(key:string,val:T):IStore{
if (components[key]){
state[key] = val;
components[key].setState(state[key]);
return o;
}
throw new Error(`Component with key ${key} could not be found`);
}
function applyMiddleware(action:Action,done:(action:Action|null|undefined)=>void):void{
let idx = 0;
let m:IMiddleware = null;
function next(action?:Action){
if (!action || idx >= middlewares.length){
return done(action);
}
m = middlewares[idx];
idx++;
return m(action,o,next);
}
next(action);
}
function doExecute(key,action){
let managedState = pool.get();
let component = components[key];
managedState.setState(state[key]);
let rd = component.getReducer();
if (rd){
rd(managedState,action);
if (managedState.hasChanges()){
action.__executed = true;
var changes = managedState.changes();
component.setState(()=>changes,()=>{
action.__done && typeof action.__done === "function" && action.__done();
});
}
}
pool.put(managedState);
}
function execute(action){
if (action){
if (trackChanges){
backlog.push(action);
}
for(var key in components){
doExecute(key,action);
}
if (!action.__executed){
action.__done();
}
}
}
function onAction(action:Action,cb?:()=>void){
action.__done = cb ;
action.__executed = false;
if (!cb){
if (typeof Promise !== "undefined"){
return new Promise((res)=>{
action.__done = res;
let act = applyMiddleware(action,(finalAction)=>{
finalAction && execute(finalAction);
});
});
}
}
let act = applyMiddleware(action,(finalAction)=>{
finalAction && execute(finalAction);
});
}
function onActionFail(err:Error){
console.log(err,err.message,err.stack);
}
const dispatch:DispatchFn = function(...args:any[]){
if (args.length === 0){
throw new Error(`No action provided`);
}else if (args.length === 1){
if (typeof args[0] === "function"){
return args[0](dispatch,getStateAt);
}else if (typeof args[0] === "object"){
return onAction(args[0]);
}
}else if (args.length === 2){
if (typeof args[0] === "function") {
return args[0](dispatch,getStateAt,args[1]);
}else if (typeof args[0] === "object"){
return onAction(args[0],args[1]);
}
}
}
o = {
connect,
disconnect,
getStateAt,
setStateAt,
dispatch
};
return o;
}