Cycle.js

Conceptions

  1. const userActions = human(computerMutations);
  2. 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 Streams
  • model() functionPurpose: manage stateInput: Action StreamsOutput: State Stream
  • view() 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$
   };
 }

Ref