Skip to content

Instantly share code, notes, and snippets.

@staltz
Last active August 29, 2015 14:19
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save staltz/78140b885f366886d9b5 to your computer and use it in GitHub Desktop.
Save staltz/78140b885f366886d9b5 to your computer and use it in GitHub Desktop.
Terse Cycle.js

Terse Cycle.js

Take Cycle's primal example, on the README:

import Cycle from 'cyclejs';
let {Rx, h} = Cycle;

let name$ = Cycle.createStream(function model(changeName$) {
  return changeName$.startWith('');
});

let vtree$ = Cycle.createStream(function view(name$) {
  return name$.map(name =>
    h('div', [
      h('label', 'Name:'),
      h('input.field', {attributes: {type: 'text'}}),
      h('h1.header', `Hello ${name}`)
    ])
  );
});

let interaction$ = Cycle.createStream(function user(vtree$) {
  return Cycle.render(vtree$, '.js-container').interaction$;
});

let changeName$ = Cycle.createStream(function intent(interaction$) {
  return interaction$.choose('.field', 'input').map(ev => ev.target.value);
});

name$.inject(changeName$).inject(interaction$).inject(vtree$).inject(name$);

Notice the functions model, view, user, intent. We can rearrange the example code, with the functions defined outside the createStream calls:

import Cycle from 'cyclejs';
let {Rx, h} = Cycle;

function model(changeName$) {
  return changeName$.startWith('');
}

function view(name$) {
  return name$.map(name =>
    h('div', [
      h('label', 'Name:'),
      h('input.field', {attributes: {type: 'text'}}),
      h('h1.header', `Hello ${name}`)
    ])
  );
}

function user(vtree$) {
  return Cycle.render(vtree$, '.js-container').interaction$;
}

function intent(interaction$) {
  return interaction$.choose('.field', 'input').map(ev => ev.target.value);
}

let name$ = Cycle.createStream(model);
let vtree$ = Cycle.createStream(view);
let interaction$ = Cycle.createStream(user);
let changeName$ = Cycle.createStream(intent);

name$.inject(changeName$).inject(interaction$).inject(vtree$).inject(name$);

Whenever we have a = f(b) and b = g(c), we can simplify these as a = f(g(c)). We can apply this trick on model() and view(), to get:

function modelAndView(changeName$) {
  return changeName$.startWith('').map(name =>
    h('div', [
      h('label', 'Name:'),
      h('input.field', {attributes: {type: 'text'}}),
      h('h1.header', `Hello ${name}`)
    ])
  );
}

We can do this with intent(), model() and view() to achieve:

function intentAndModelAndView(interaction$) {
  return interaction$.choose('.field', 'input')
    .map(ev => ev.target.value)
    .startWith('')
    .map(name =>
      h('div', [
        h('label', 'Name:'),
        h('input.field', {attributes: {type: 'text'}}),
        h('h1.header', `Hello ${name}`)
      ])
    );
}

Let's rename intentAndModelAndView to computer so it represents the computer's role in Human-Computer Interaction. Then we have in total:

import Cycle from 'cyclejs';
let {Rx, h} = Cycle;

function computer(interaction$) {
  return interaction$.choose('.field', 'input')
    .map(ev => ev.target.value)
    .startWith('')
    .map(name =>
      h('div', [
        h('label', 'Name:'),
        h('input.field', {attributes: {type: 'text'}}),
        h('h1.header', `Hello ${name}`)
      ])
    );
}

function user(vtree$) {
  return Cycle.render(vtree$, '.js-container').interaction$;
}

let vtree$ = Cycle.createStream(computer);
let interaction$ = Cycle.createStream(user);

interaction$.inject(vtree$).inject(interaction$);

To be able to make circularly dependent Observables, we just need one of these Observables to be a Cycle Stream. Let's select, for that matter, vtree$:

// ...

let vtree$ = Cycle.createStream(computer);
let interaction$ = user(vtree$);

vtree$.inject(interaction$);

We can simplify that snippet by bypassing the temporary variable interaction$, and just using instead user(vtree$) inside the inject.

// ...

let vtree$ = Cycle.createStream(computer);

vtree$.inject(user(vtree$));

Because the user function is so simple, we can expand it inside the inject:

// ...

let vtree$ = Cycle.createStream(computer);

vtree$.inject(Cycle.render(vtree$, '.js-container').interaction$);

Finally, let's put the computer function back into createStream() directly:

let vtree$ = Cycle.createStream(function computer(interaction$) {
  return interaction$.choose('.field', 'input')
    .map(ev => ev.target.value)
    .startWith('')
    .map(name =>
      h('div', [
        h('label', 'Name:'),
        h('input.field', {attributes: {type: 'text'}}),
        h('h1.header', `Hello ${name}`)
      ])
    );
});

vtree$.inject(Cycle.render(vtree$, '.js-container').interaction$);

This program behaves just like the original one does. Model, View, Intent concerns are all condensed into the computer function.

Conclusion: architectures in Cycle are simply function compositions. You can break apart or join functions over Observables however you wish. You are not constrained to strict Model, View, Intent. Observables are very powerful to express any ongoing behaviour in the computer, and functions are the most powerful modularity tool we have. Architectures are just functions.

@staltz
Copy link
Author

staltz commented Apr 19, 2015

Compare with React:

var HelloWorld = React.createClass({
  getInitialState: function() {
    return {name: ''};
  },
  handleChange: function() {
    this.setState({name: React.findDOMNode(this.refs.field).value});
  },
  render: function() {
    return (
      <div>
        <label>Name:</label>
        <input
          type="text"
          onChange={this.handleChange}
          ref="field"
          defaultValue={this.state.name} />
        <h1>Hello {{this.state.name}}</h1>
      </div>
    );
  }
});

React.render(<HelloWorld />, document.querySelector('.js-container');

@insin
Copy link

insin commented Apr 19, 2015

(RE: https://twitter.com/andrestaltz/status/589867014476406785) Terse React can be the same length 😃

var HelloWorld = React.createClass({
  getInitialState() {
    return {name: ''}
  },
  render() {
    return <div>
      <label>Name:</label>
      <input onChange={(e) => this.setState({name: e.target.value})}/>
      <h1>Hello {this.state.name}</h1>
    </div>
  }
})

React.render(<HelloWorld/>, document.querySelector('.js-container'))

@staltz
Copy link
Author

staltz commented Apr 20, 2015

Nice one!

@staltz
Copy link
Author

staltz commented May 4, 2015

An upcoming version of Cycle will allow an even more terse implementation of the example, as short as the React one by @insin:

Cycle.applyToDOM('.js-container', function computer(interactions) {
  return interactions.get('.myinput', 'input')
    .map(function (ev) { return ev.target.value; })
    .startWith('')
    .map(function (name) {
      return h('div', [
        h('label', 'Name:'),
        h('input.myinput', {attributes: {type: 'text'}}),
        h('hr'),
        h('h1', 'Hello ' + name)
      ]);
    });
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment