Cycle.js
Conceptions
const userActions = human(computerMutations);
const computerMutations = computer(userActions);
Intent => Model => View
function computer(sources) {
// intent
const change$ = sources.select('input').events('input').map(e => e.target.val);
// model
const width$ = change$.startWith(20);
const state$ = change$.combine(width$, height$);
// view
return { DOM: state$.map(() => div('hello')) };
};
Streams
see => Stream in Rx.js.md
Model-View-Intent
Goals: separate main()
into several individual functional parts.
intent()
functionPurpose: interpret DOM events as user’s intended actionsInput: DOM sourceOutput: Action Streamsmodel()
functionPurpose: manage stateInput: Action StreamsOutput: State Streamview()
functionPurpose: visually represent state from the ModelInput: State StreamOutput: Stream of Virtual DOM nodes as the DOM Driver sink
function intent(domSource) {
return {
changeWeight$: domSource.select('.weight').events('input')
.map(ev => ev.target.value),
changeHeight$: domSource.select('.height').events('input')
.map(ev => ev.target.value)
};
}
function model(actions) {
const weight$ = actions.changeWeight$.startWith(70);
const height$ = actions.changeHeight$.startWith(170);
return xs.combine(weight$, height$)
.map(([weight, height]) => {
return {weight, height, bmi: bmi(weight, height)};
});
}
function view(state$) {
return state$.map(({weight, height, bmi}) =>
div([
renderWeightSlider(weight),
renderHeightSlider(height),
h2('BMI is ' + bmi)
])
);
}
function main(sources) {
return {DOM: view(model(intent(sources.DOM)))};
}
Components
Stream as props for components.
function LabeledSlider(sources) {
const domSource = sources.DOM;
const props$ = sources.props;
// ...
return sinks;
}
function main(sources) {
const props$ = xs.of({
label: 'Radius', unit: '', min: 10, value: 30, max: 100
});
const childSources = {DOM: sources.DOM, props: props$};
const labeledSlider = LabeledSlider(childSources);
const childVDom$ = labeledSlider.DOM;
const childValue$ = labeledSlider.value;
// ...
}
Drivers
A driver example:
function WSDriver(/* no sinks */) {
return xs.create({
start: listener => {
this.connection = new WebSocket('ws://localhost:4000');
connection.onerror = (err) => {
listener.error(err)
}
connection.onmessage = (msg) => {
listener.next(msg)
}
},
stop: () => {
this.connection.close();
},
});
}
DOM Driver
- connect virtual DOM with
cycle.js
.
export interface DOMDriverOptions {
modules: Array;
transposition: boolean;
}
export function makeDOMDriver(container: String | DOMElement, options: DOMDriverOptions);
- Virtual DOM Elements
export function tagName(tagOrSelector, optionalData, optionalChildrenOrText);
DemoCode
function main(sources) {
const weightProps$ = xs.of({
label: 'Weight', unit: 'kg', min: 40, value: 70, max: 150
});
const heightProps$ = xs.of({
label: 'Height', unit: 'cm', min: 140, value: 170, max: 210
});
const weightSources = {DOM: sources.DOM, props: weightProps$};
const heightSources = {DOM: sources.DOM, props: heightProps$};
const weightSlider = isolate(LabeledSlider)(weightSources);
const heightSlider = isolate(LabeledSlider)(heightSources);
const weightVDom$ = weightSlider.DOM;
const weightValue$ = weightSlider.value;
const heightVDom$ = heightSlider.DOM;
const heightValue$ = heightSlider.value;
const bmi$ = xs.combine(weightValue$, heightValue$)
.map(([weight, height]) => {
const heightMeters = height * 0.01;
const bmi = Math.round(weight / (heightMeters * heightMeters));
return bmi;
})
.remember();
const vdom$ = xs.combine(bmi$, weightVDom$, heightVDom$)
.map(([bmi, weightVDom, heightVDom]) =>
div([
weightVDom,
heightVDom,
h2('BMI is ' + bmi)
])
);
return {
DOM: vdom$
};
}