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.
Compare with React: