Last active
September 17, 2019 21:15
-
-
Save ZeekoZhu/c10b30815b711db909926c172789dfd2 to your computer and use it in GitHub Desktop.
An Elm LIKE state management approach based on RxJS
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'rxjs/add/operator/scan'; | |
import 'rxjs/add/operator/do'; | |
import { BehaviorSubject } from 'rxjs/BehaviorSubject'; | |
import { Subject } from 'rxjs/Subject'; | |
type UpdateResult<TState, TMsgType> = TState | [TState, TMsgType[]]; | |
type Pattern<TMsg, TState, TMsgType> = | |
[new (...args: any[]) => TMsg, (acc: TState, msg: TMsg, $msg: Subject<TMsgType>) => UpdateResult<TState, TMsgType>]; | |
export class ElmArch<TState, TMsgType> { | |
private readonly $msg = new Subject<TMsgType>(); | |
/** | |
* Pattern matching syntax | |
* @template TMsg | |
* @param {new (...args: any[]) => TMsg} type constructor of Msg | |
* @param {(acc: TState, msg: TMsg, $msg: Subject<TMsgType>) => UpdateResult<TState, TMsgType>} reducer method to compute new state | |
* @returns {Pattern<TMsg, TState, TMsgType>} | |
* @memberof ElmArch | |
*/ | |
caseOf<TMsg>( | |
type: new (...args: any[]) => TMsg, | |
reducer: (acc: TState, msg: TMsg, $msg: Subject<TMsgType>) => UpdateResult<TState, TMsgType>) | |
: Pattern<TMsg, TState, TMsgType> { | |
return [type, reducer]; | |
} | |
/** | |
* Generate a result of a new state with a sets of msgs, these msgs will be published after new state is published | |
* @param {TState} newState | |
* @param {...TMsgType[]} msgs | |
* @returns {UpdateResult<TState, TMsgType>} | |
* @memberof ElmArch | |
*/ | |
nextWithCmds(newState: TState, ...msgs: TMsgType[]): UpdateResult<TState, TMsgType> { | |
if (arguments.length === 1) { | |
return newState; | |
} else { | |
return [newState, msgs]; | |
} | |
} | |
matchWith<TMsg>($msg: Subject<TMsgType>, patterns: Pattern<TMsg, TState, TMsgType>[]) { | |
return (acc: UpdateResult<TState, TMsgType>, msg: TMsg) => { | |
const state = acc instanceof Array ? acc[0] : acc; | |
for (const it of patterns) { | |
if (msg instanceof it[0]) { | |
return it[1](state, msg, $msg); | |
} | |
} | |
throw new Error('Invalid Message Type'); | |
}; | |
} | |
begin(initState: TState, patterns: Pattern<any, TState, TMsgType>[]) { | |
const $res = new BehaviorSubject<TState>(initState); | |
this.$msg.do(m => console.log('%cMessage', 'color:blue', m)) | |
.scan(this.matchWith(this.$msg, patterns), initState) | |
.do((s: UpdateResult<TState, TMsgType>) => { | |
if (s instanceof Array) { | |
console.log('%cState %O %cCmds %O', 'color:green', s[0], 'color:darkcyan', s[1]); | |
} else { | |
console.log('%cState', 'color:green', s); | |
} | |
}) | |
.subscribe((s: UpdateResult<TState, TMsgType>) => { | |
if (s instanceof Array) { | |
const [state, msgs] = s; | |
$res.next(state); | |
msgs.forEach(m => this.$msg.next(m)); | |
} else { | |
$res.next(s); | |
} | |
}); | |
return $res; | |
} | |
send(msg: TMsgType) { | |
this.$msg.next(msg); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Model | |
export interface IHabbitPresetsState { | |
presets: IHabbitPreset[]; | |
isLoading: boolean; | |
isOperating: boolean; | |
} | |
// Update | |
export class Get { | |
} | |
export class Receive { | |
constructor(public payload: IHabbitPreset[]) { } | |
} | |
export class Add { | |
constructor(public payload: IHabbitPreset) { | |
} | |
} | |
export class AddResp { | |
constructor(public payload: IHabbitPreset) { | |
} | |
} | |
export class Delete { | |
constructor(public payload: number) { | |
} | |
} | |
export class DeleteResp { | |
constructor(public payload: number) { } | |
} | |
export type HabbitPresetsMsg = | |
Get | Receive | |
| Add | AddResp | |
| Delete | DeleteResp; | |
const arch = new ElmArch<IHabbitPresetsState, HabbitPresetsMsg>(); | |
const update = () => { | |
const caseOf = arch.caseOf; | |
return [ | |
caseOf(Get, (state, msg, $msg) => { | |
http.get<IHabbitPreset[]>('/api/HabbitPresets') | |
.subscribe(data => $msg.next(new Receive(data))); | |
return { ...state, isLoading: true }; | |
}), | |
caseOf(Receive, (s, m) => { | |
return { ...s, isLoading: false, presets: m.payload }; | |
}), | |
caseOf(Add, (state, msg, $msg) => { | |
http.post<IHabbitPreset>('/api/HabbitPresets', msg.payload) | |
.subscribe(data => $msg.next(new AddResp(data))); | |
return { ...state, isOperating: true }; | |
}), | |
caseOf(AddResp, (state, msg) => { | |
return { | |
...state, | |
isOperating: false, | |
presets: [...state.presets, msg.payload] | |
}; | |
}), | |
caseOf(Delete, (state, msg, $msg) => { | |
http.delete<IHabbitPreset>(`/api/HabbitPresets/${msg.payload}`) | |
.subscribe(data => $msg.next(new DeleteResp(data.id))); | |
return { | |
...state, isOperating: true | |
}; | |
}), | |
caseOf(DeleteResp, (s, m, $m) => { | |
return { | |
...s, presets: s.presets.filter(x => x.id !== m.payload), | |
isOperating: false | |
}; | |
}) | |
]; | |
} | |
// set initial state | |
const initState = { presets: [], isLoading: false, isOperating: false }; | |
// now you get a observable state of your app | |
const state$ = arch.begin(initState, update()); | |
// schedule a message to change state | |
arch.send(new Get()); | |
// Use jQuery or something else to render your View |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment