Jan van Brügge
[1, 2, 3, 4, 5]
.map(x => x * 2)
.forEach(x => console.log(x));
Promise.resolve([1, 2, 3, 4, 5]) //Or from API
.then(arr => arr
.map(x => x * 2)
.forEach(x => console.log(x))
);
Observable.from([1, 2, 3, 4, 5]) //Or from a websocket
.map(x => x * 2)
.subscribe(x => console.log(x));
Make an initial request to the server and write the data to the DOM. Update the data with a websocket.
const ws$ = websocket$.map(message => message.payload);
const http$ = response$.map(res => res.body)
.map(xs.fromArray)
.flatten();
xs.merge(ws$, http$)
.fold((data, x) => data.concat(x), [])
.compose(debounce(50)) //Batch DOM updates
.subscribe({ next: updateDOM });
If the user clicks x times within y milliseconds print the class of the element that was clicked.
click$
.compose(throttle(y))
.map(event => {
const numClick$ = click$.endWhen(xs.periodic(y).take(1))
.fold(n => n + 1, 1)
.last();
return numClick$
.filter(n => n >= x)
.mapTo(event.target.className);
}).flatten()
.addListener({ next: console.log });
Runnable example on webpackbin
Cycle is like the physicist’s dream of a unified theory of everything, but for JavaScript.
Nick Johnstone
A Cycle.js app is just a function taking sources as input and returning sinks as result.
import xs from 'xstream';
import { div, button, p } from '@cycle/dom';
function Counter(sources) {
const decrement$ = sources.DOM
.select('.decrement').events('click').mapTo(-1);
const increment$ = sources.DOM
.select('.increment').events('click').mapTo(+1);
const action$ = xs.merge(decrement$, increment$);
const count$ = action$.fold((x, y) => x + y, 0);
const vdom$ = count$.map(count =>
div([
button('.decrement', 'Decrement'),
button('.increment', 'Increment'),
p('Counter: ' + count)
])
);
return {
DOM: vdom$
};
}
import Greeter from './greeter';
import Counter from './counter';
function main(sources) {
const greeterSinks = Greeter(sources);
const counterSinks = Counter(sources);
const vdom$ = xs.combine(greeterSinks.DOM, counterSinks.DOM)
.map(children =>
Some children
{ children }
);
return {
DOM: vdom$
};
}
import Counter from './counter';
function main(sources) {
const counter1 = Counter(sources);
const counter2 = Counter(sources);
const vdom$ = xs.combine(counter1.DOM, counter2.DOM)
.map(children =>
Some children
{ children }
);
return {
DOM: vdom$
};
}
Both counters are activated if you press any button
Isolate them!
import Counter from './counter';
function main(sources) {
- const counter1 = Counter(sources);
- const counter2 = Counter(sources);
+ const counter1 = isolate(Counter, 'counterA')(sources);
+ const counter2 = isolate(Counter, 'counterB')(sources);
const vdom$ = xs.combine(counter1.DOM, counter2.DOM)
.map(children =>
Some children
{ children }
);
return {
DOM: vdom$
};
}
import Counter from './counter';
function main(sources) {
const vdom$ = xs.of();
const modal$ = sources.DOM.select('.btn').events('click')
.mapTo({
type: 'open',
component: Counter
});
return {
DOM: vdom$,
modal: modal$
};
}
const wrappedMain = modalify(main);
function logDriver(sink$) {
sink$.addListener({
next: console.log
});
//We do not return anything --> write-only driver
}
run(main, {
log: logDriver
});
function websocketDriver(/* no argument --> read-only */) {
let websocket = undefined;
return xs.create({
start: listener => {
websocket = new Websocket(myEndpoint);
websocket.onmessage = listener.next;
},
stop: () => {
websocket.close();
}
});
}
run(main, {
ws: websocketDriver
});
function reduxify(main, initialState, rootReducer) {
return function(sources) {
const actionProxy$ = xs.create();
const state$ = actionProxy$
.fold(rootReducer, initialState);
const sinks = main({ ...sources, state: state$ });
actionProxy$.imitate(sinks.state);
delete sinks.state;
return sinks;
}
}
A fractal state management tool for Cycle.js applications.
import xs from 'xstream';
function Counter(sources) {
const decrement$ = sources.DOM
.select('.decrement').events('click')
.mapTo(state => state - 1);
const increment$ = sources.DOM
.select('.increment').events('click')
.mapTo(state => state + 1);
const reducer$ = xs.merge(decrement$, increment$);
const vdom$ = sources.onion.state$.map(count =>
div([
button('.decrement', 'Decrement'),
button('.increment', 'Increment'),
p('Counter: ' + count)
])
);
return {
DOM: vdom$,
onion: reducer$
};
}
import Counter from './counter';
function main(sources) {
const counter1 = isolate(Counter, 'counterA')(sources);
const counter2 = isolate(Counter, 'counterB')(sources);
const vdom$ = xs.combine(counter1.DOM, counter2.DOM)
.map(children =>
Some children
{ children }
);
return {
DOM: vdom$,
onion: xs.merge(counter1.onion, counter2.onion),
log: sources.onion.state$ //what will be printed?
};
}
{
counterA: 5,
counterB: 3
}
Compare the parent state to the child state
{
counterA: 5,
counterB: 3
}
//childA
5
//childB
3
We can model this behavior
-- on the source (state$)
isolateSource :: ParentState -> ChildState
-- on the sink (reducer$)
isolateSinks :: (ParentState, ChildState) -> ParentState
We can model this behavior
-- on the source (state$)
get :: ParentState -> ChildState
-- on the sink (reducer$)
set :: (ParentState, ChildState) -> ParentState
We can model this behavior
const lens = {
get,
set
};
const halfLens = {
get: state => state.counterA / 2;
set: (state, n) => ({
...state,
counterA: n * 2
})
};
function main(sources) {
const counter1 = isolate(Counter, 'counterA')(sources);
const counter2 = isolate(Counter, {
onion: halfLens,
'*': 'counterB'
})(sources);
const vdom$ = xs.combine(counter1.DOM, counter2.DOM)
.map(view);
return {
DOM: vdom$,
onion: xs.merge(counter1.onion, counter2.onion),
};
}
interface TabbarState {
active: number;
tabs: TabState[];
}
interface TabState {
isActive: boolean; //Automaticly set by the lens
name: string;
}
const selectionLens = {
get: state => state.tabs.map(t => ({
...t,
isActive: t.id === state.active
})),
set: (state, tabs) => ({
...state,
active: tabs.reduce((acc, curr) => {
return curr.isActive && curr.id !== state.active ?
curr.id :
acc;
}, state.active),
tabs: tabs.map(omit('isActive'))
})
};
const TabbarComponent = makeCollection({
item: TabComponent,
itemKey: itemState => itemState.name,
itemScope: index => index,
collectSinks: instances => ({
DOM: instances.pickCombine('DOM').map(children =>
{ children }
),
onion: xs.merge(
xs.of(() => initialState),
instances.pickMerge('onion')
)
})
});
export default const isolatedTabbar = isolate(TabbarComponent, {
onion: selectionLens,
'*': null
});