Last active
July 27, 2018 12:54
-
-
Save aronallen/f327f6571b4af425542393dcf7cbc867 to your computer and use it in GitHub Desktop.
cycle-spoke
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
/* | |
Below is a simplified state management library for Cycle JS. | |
It is named after a spoke on a bicycle wheel, because a spoke at the bottom of the wheel will be at the top after a semi-revoltion. | |
cycle-spoke aims to be approachable, fun, fractal, and easy to use for developers who are familiar with reducers. | |
Any cycle component can be spoked, spoke creates a local circular reference, and you can have as many spoked components as you like. | |
It applies any reducers emitted from the components spoke sink to the internal state, and provides a source of the derived state. | |
A spoked component has no initial state, so you must emit a reducer that sets the initial state on load. | |
A spoked component may receive reducers from its parent. A spoked component returns it's state as a sink. | |
A spokeable component has the reverse contract of a spoked component. | |
An optional debug function may be provided to spoke() to inspect the active reducer, last, and next state | |
*/ | |
import { proxy } from 'most-proxy'; | |
import { Stream, just, scan, skip, multicast, empty, merge, combineArray } from 'most'; | |
import { DOMSource } from '@cycle/dom/most-typings'; | |
import { VNode } from 'snabbdom/vnode'; | |
import { h } from 'snabbdom/h'; | |
export type Sources<T> = { | |
spoke: Stream<T>; | |
} | |
export type Sinks<T> = { | |
spoke: Stream<Reducer<T>> | |
} | |
export type Reducer<T extends {}> = (s: Partial<T>) => T; | |
export type SpokedSources<T> = { | |
spoke?: Stream<Reducer<T>>; | |
} | |
export type SpokedSinks<T> = { | |
spoke: Stream<T> | |
} | |
export type HubbedSinks = { | |
DOM: Stream<Array<VNode>>; | |
} | |
export type SpokableComponent<T extends {}> = (sources: Sources<T>) => Sinks<T>; | |
export type HubbedComponent<T extends {}> = (sources: Sources<T>) => Sinks<T> & HubbedSinks; | |
export type SpokedComponent<T extends {}> = (sources: SpokedSources<T>) => SpokedSinks<T>; | |
export type Debugger<T extends {}> = (reducer: Reducer<T>, last: Partial<T>, next: T) => void; | |
export function spoke<T extends {}>(Component: SpokableComponent<T>, debug: Debugger<T> = () => {}): SpokedComponent<T> { | |
return (sources) => { | |
const spoke_proxy = proxy(); | |
const spoke_external = sources.spoke || empty(); | |
const spoke_merged = merge( | |
spoke_proxy.stream, | |
spoke_external | |
) as Stream<Reducer<T>>; | |
const spoke_scan = scan( | |
(last, reducer) => { | |
const next = reducer(last) as T; | |
debug(reducer, last, next); | |
return next; | |
}, | |
{} as Partial<T>, | |
spoke_merged | |
) | |
const spoke_skip = skip(1, spoke_scan) as Stream<T>; | |
const spoke = multicast(spoke_skip); | |
const sinks = Component({ | |
...sources, | |
spoke | |
}); | |
spoke_proxy.attach(sinks.spoke); | |
return { | |
...sinks, | |
spoke | |
}; | |
} | |
} | |
export type ISources = { | |
DOM: DOMSource; | |
spoke: Stream<{ count: number }> | |
} | |
export type ISinks = { | |
DOM: Stream<VNode>; | |
spoke: Stream<Reducer<{ count: number }>>; | |
} | |
export const Component = (sources: ISources): ISinks => { | |
return { | |
DOM: sources.spoke | |
.map( | |
(state) => h('span', String(state.count)) | |
), | |
spoke: merge( | |
// initial state | |
just(() => ({ count: 1 })), | |
// future state | |
sources.DOM | |
.select('span') | |
.events('click') | |
.take(1) | |
.constant( | |
(state: {count: number}) => ({ count: state.count + 1 }) | |
) | |
) | |
}; | |
}; | |
const SpokedComponent = spoke(Component); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment