Skip to content

Instantly share code, notes, and snippets.

@aronallen
Last active July 27, 2018 12:54
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save aronallen/f327f6571b4af425542393dcf7cbc867 to your computer and use it in GitHub Desktop.
Save aronallen/f327f6571b4af425542393dcf7cbc867 to your computer and use it in GitHub Desktop.
cycle-spoke
/*
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