Skip to content

Instantly share code, notes, and snippets.

@ZeekoZhu
Last active September 17, 2019 21:15
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ZeekoZhu/c10b30815b711db909926c172789dfd2 to your computer and use it in GitHub Desktop.
Save ZeekoZhu/c10b30815b711db909926c172789dfd2 to your computer and use it in GitHub Desktop.
An Elm LIKE state management approach based on RxJS
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);
}
}
// 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