Вы находитесь на странице: 1из 24

7/19/2015

Basic examples Cycle.js

BASIC EXAMPLES
Cycle.js apps will always include at least
three important components: main(),
drivers, and run(). In main(), we
listen to drivers (the input to main), and
we speak to drivers (the output of
main). In Haskell 1.0 jargon, main()
takes driver response Observables and
output driver request Observables.
Cycle.run()ties main()and
drivers together, as we saw in the last
chapter.
function main(driverResponses) {
let driverRequests = {
DOM: // transform driverResponses.DOM

// through a series of RxJS operat


};
return driverRequests;
}
let drivers = {
DOM: makeDOMDriver('#app')
};
Cycle.run(main, drivers);

In the case of the DOM Driver, our


main()will interact with the user
through the DOM. Most of our examples
http://cycle.js.org/basic-examples.html

1/24

7/19/2015

Basic examples Cycle.js

will use the DOM Driver, but keep in mind


Cycle.js is modular and extensible. You
could build an application, targetting
native mobile for instance, without using
the DOM Driver.

Toggle a checkbox
Lets start from the assumption we have
an index.htmlfile with an element to
contain our app.
<!DOCTYPE html>
<html>
<head>
<script src="./checkbox-app.js"
<meta charset="utf-8">
<title>Cycle.js checkbox</title>
</head>
<body>

<div id="app"></div> <!-- Our container


</body>
</html>

We will point our Cycle.js app to live


inside #app. The checkbox-app.js
file should look like this (before it is
transpiled from ES6 to ES5, if that is
required):
import Cycle from '@cycle/core';

import {h, makeDOMDriver} from '@cycle/dom'


function main(responses) {
http://cycle.js.org/basic-examples.html

2/24

7/19/2015

Basic examples Cycle.js

let requests = {DOM: null};


return requests;
}
Cycle.run(main, {
DOM: makeDOMDriver('#app')
});

Cycle DOM is a package containing two


drivers and some helpers to use those
libraries. A DOM Driver is created with
makeDOMDriver()and an HTML
Driver (for server-side rendering) is
created with makeHTMLDriver().
CycleDOMalso includes h()and
svg(), these are functions that output
virtual-domvirtual elements, usually
called VTree.
Our main()for now does nothing. It
takes driver responsesand outputs
driver requests. To make something
appear on the screen, we need to output
an Observable of VTree in
requests.DOM. The name DOMin
requestsmust match the name we
gave in the drivers object given to
Cycle.run(). This is how Cycle.js
knows which drivers to match with which
request Observables. This is also true for
responses: we listen to DOM events by
using responses.DOM.

http://cycle.js.org/basic-examples.html

3/24

7/19/2015

Basic examples Cycle.js

import Cycle from '@cycle/core';

import {h, makeDOMDriver} from '@cycle/dom'


function main(responses) {
let requests = {
DOM: Cycle.Rx.Observable.just(false
.map(toggled =>
h('div', [
h('input', {type: 'checkbox'
h('p', toggled ? 'ON' : 'off'
])
)
};
return requests;
}
Cycle.run(main, {
DOM: makeDOMDriver('#app')
});

We just added an Observable of a false


mapped to a VTree.
Observable.just(x)creates a
simple Observable which simply emits x
http://cycle.js.org/basic-examples.html

4/24

7/19/2015

Basic examples Cycle.js

once, then we used map()to convert


that to the virtual-domVTree
containing an <input
type="checkbox">and a <p>
element displaying offif the
toggledboolean was false, and
displaying ONotherwise.
This is nice: we can see the DOM elements
generated by the virtual-dom
elements created with h(). But if we
click the Toggle me checkbox, the label
off under it does not change to ON.
That is because we are not listening to
DOM events. In essence, our main()
isnt listening to the user. We do that by
using responses.DOM:
import Cycle from '@cycle/core';

import {h, makeDOMDriver} from '@cycle/dom'


function main(responses) {
let requests = {
DOM: responses.DOM.get('input',
.map(ev => ev.target.checked)
.startWith(false) // NEW!
.map(toggled =>
h('div', [
h('input', {type: 'checkbox'
h('p', toggled ? 'ON' : 'off'
])
)
};
return requests;
}

http://cycle.js.org/basic-examples.html

5/24

7/19/2015

Basic examples Cycle.js

Cycle.run(main, {
DOM: makeDOMDriver('#app')
});

Notice the lines we changed, with NEW!.


We now map changeevents on the
checkbox to the checkedvalue of the
element (the first map()) to VTrees
displaying that value. However, we need
a .startWith()to give a default
value to be converted to a VTree. Without
this, nothing would be shown! Why?
Because our requestsis reacting to
responses, but responsesis
reacting to requests. If no one triggers
the first event, nothing will happen. It is
the same effect as meeting a stranger,
and not having anything to say. Someone
needs to take the initiative to start the
conversation. That is what main()is
doing: kickstarting the interaction, and
http://cycle.js.org/basic-examples.html

6/24

7/19/2015

Basic examples Cycle.js

then letting subsequent actions be


mutual reactions between main()and
the DOM Driver.

Displaying data from


HTTP requests
One of the most obvious requirements
web apps normally have is to fetch some
data from the server and display that.
How would we build that with Cycle.js?
Suppose we have a backend with a
database containing 10 users. We want to
have a frontend with one button get a
random user, and to display the users
details, like name and email. This is what
we want to achieve:

Essentially we just need to make a


request for the endpoint
/user/:numberwhenever the button
http://cycle.js.org/basic-examples.html

7/24

7/19/2015

Basic examples Cycle.js

is clicked. Where would this HTTP request


fit in a Cycle.js app?
Recall the dialogue abstraction,
mentioned also in the previous
checkbox example. The app generates
VTree Observables as requests to the
DOM. But our apps should also be able
to generate different kinds of requests.
The most typical type of request is an
HTTP request. Since these are not in any
way related to the DOM, we need a
different driver to handle them.
The HTTP Driver is similar in style to the
DOM Driver: it expects a request
Observable, and gives you a response
Observable. Instead of studying in details
how the HTTP Driver works, lets see how
a basic HTTP example looks like.
If HTTP requests are sent when clicks on
the button happen, then the HTTP
request Observable should depend
directly on the button click Observable.
Roughly, this:
function main(responses) {
// ...

let click$ = responses.DOM.get('.get-rand

const USERS_URL = 'http://jsonplaceholder


// This is the HTTP request Observable
let getRandomUser$ = click$.map(()
http://cycle.js.org/basic-examples.html

8/24

7/19/2015

Basic examples Cycle.js

let randomNum = Math.round(Math


return {
url: USERS_URL + String(randomNum
method: 'GET'
};
});
// ...
}

getRandomUser$is the Observable


we give to the HTTP Driver, by returning it
from the main()function:
function main(responses) {
// ...
return {
// ...
HTTP: getRandomUser$
};
}

We still need to display data for the


current user, and this comes only when
we get an HTTP response. For that
purpose we need the Observable of user
data to depend directly on the HTTP
response Observable. This is available
from the mains input:
responses.HTTP(the name HTTP
needs to match the driver name you gave
for the HTTP driver when calling
Cycle.run()).
http://cycle.js.org/basic-examples.html

9/24

7/19/2015

Basic examples Cycle.js

function main(responses) {
// ...
let user$ = responses.HTTP
.filter(res$ => res$.request.url
.mergeAll()
.map(res => res.body);
// ...
}

responses.HTTPis an Observable of
all the network responses this app is
observing. Because it could potentially
include responses unrelated to user
details, we need to filter()it. And
we also mergeAll(), to flatten the
Observable of Observables. This might
feel like magic right now, so read the
HTTP Driver docs if youre curious of the
details. We map each response resto
res.bodyin order to get the JSON
data from the response and ignore other
fields like HTTP status.
We still havent defined the rendering in
our app. We should display on the DOM
whatever data we have from the current
user in user$. So the VTree Observable
vtree$should depend directly on
user$, like this:
function main(responses) {
// ...
http://cycle.js.org/basic-examples.html

10/24

7/19/2015

Basic examples Cycle.js

let vtree$ = user$.map(user =>


h('div.users', [

h('button.get-random', 'Get random us


h('div.user-details', [
h('h1.user-name', user.name
h('h4.user-email', user.email
h('a.user-website', {href:
])
])
);
// ...
}

However, initially, there wont be any


user$event, because those only
happen when the user clicks. This is the
same conversation initiative problem
we saw in the previous checkbox
example. So we need to make user$
start with a nulluser, and in case
vtree$sees a null user, it renders just
the button. Unless, if we have real user
data, we display the name, the email, and
the website:
function main(responses) {
// ...
let user$ = responses.HTTP
.filter(res$ => res$.request.url
.mergeAll()
.map(res => res.body)
.startWith(null); // NEW!

http://cycle.js.org/basic-examples.html

11/24

7/19/2015

Basic examples Cycle.js

let vtree$ = user$.map(user =>


h('div.users', [

h('button.get-random', 'Get random us

user === null ? null : h('div.user-de


h('h1.user-name', user.name
h('h4.user-email', user.email
h('a.user-website', {href:
])
])
);
// ...
}

We give vtree$to the DOM Driver, and


it renders those for us. All done, and the
whole code looks like this:
import Cycle from '@cycle/core';

import {h, makeDOMDriver} from '@cycle/dom'


import {makeHTTPDriver} from '@cycle/http'
function main(responses) {

const USERS_URL = 'http://jsonplaceholder


let getRandomUser$ = responses.DOM
.map(() => {
let randomNum = Math.round(Math
return {
url: USERS_URL + String(randomNum
method: 'GET'
};
});
let user$ = responses.HTTP
.filter(res$ => res$.request.url
.mergeAll()
.map(res => res.body)
.startWith(null);
http://cycle.js.org/basic-examples.html

12/24

7/19/2015

Basic examples Cycle.js

let vtree$ = user$.map(user =>


h('div.users', [

h('button.get-random', 'Get random us

user === null ? null : h('div.user-de


h('h1.user-name', user.name
h('h4.user-email', user.email
h('a.user-website', {href:
])
])
);
return {
DOM: vtree$,
HTTP: getRandomUser$
};
}
Cycle.run(main, {
DOM: makeDOMDriver('#app'),
HTTP: makeHTTPDriver()
});

Increment and
http://cycle.js.org/basic-examples.html

13/24

7/19/2015

Basic examples Cycle.js

decrement a counter
We saw how to use the dialogue pattern
of building user interfaces, but our
examples didnt have state: the label just
reacted to the checkbox event, and the
user details view just showed what came
from the HTTP response. Normally
applications have state in memory, so
lets see how to build a Cycle.js app for
that case.
If we have a counter Observable (emitting
events to tell the current counter value),
displaying the counter is as simple as this:
count$.map(count =>
h('div', [
h('button.increment', 'Increment'
h('button.decrement', 'Decrement'
h('p', 'Counter: ' + count)
])
)

What does the suffixed dollar


sign `$` mean?
Notice we used the name count$
for the Observable of current counter
values. The dollar sign $suffixed to
a name is a soft convention to
indicate that the variable is an
http://cycle.js.org/basic-examples.html

14/24

7/19/2015

Basic examples Cycle.js

Observable. It is a naming helper to


indicate types.
Suppose you have an Observable of
VTree depending on an Observable
of name string
let vtree$ = name$.map(name =>

Notice that the function inside map


takes nameas argument, while the
Observable is named name$. The
naming convention indicates that
nameis the value being emitted by
name$. In general, foobar$
emits foobar. Without this
convention, if name$would be
named simply name, it would
confuse readers about the types
involved. Also, name$is succinct
compared to alternatives like
nameObservable,
nameStream, or nameObs. This
convention can also be extended to
arrays: use plurality to indicate the
type is an array. Example: vtrees
is an array of vtree, but vtree$
is an Observable of vtree.
But how to create count$? Clearly it
must depend on increment clicks and
http://cycle.js.org/basic-examples.html

15/24

7/19/2015

Basic examples Cycle.js

decrement clicks. The former should


mean a +1 operation, and the latter a
-1 operation.
let action$ = Cycle.Rx.Observable.merge
DOM.get('.decrement', 'click').map
DOM.get('.increment', 'click').map
);

The mergeoperator allows us to get an


event stream of actions, either increment
or decrement actions. In this sense,
mergehas OR semantics. But this still
isnt count$, it is just action$.
count$should begin with zero, which
justifies the use of a startWith(0)
operator, but besides that we need a
scan()as well:
let count$ = action$.startWith(0).scan

What does scando? It is similar to


reducefor Array, allowing us to
accumulate values over the sequence. In
functional programming languages, it is
often called fold. For instance, in Elm,
foldpis equivalent to scan. The
name foldp indicates we are folding
the sequence from the past.

http://cycle.js.org/basic-examples.html

16/24

7/19/2015

Basic examples Cycle.js

0 1

+1+1+1

scan((x,y)=>x+y)

0 1

0 12

If we put action$and count$


together in our main(), we can
implement the counter like this:
import Cycle from '@cycle/core';

import {h, makeDOMDriver} from '@cycle/dom'


function main({DOM}) {
let action$ = Cycle.Rx.Observable
DOM.get('.decrement', 'click').
DOM.get('.increment', 'click').
);
let count$ = action$.startWith(0).
return {
DOM: count$.map(count =>
h('div', [

h('button.decrement', 'Decrement'

h('button.increment', 'Increment'
h('p', 'Counter: ' + count
])
)
};
}
Cycle.run(main, {
DOM: makeDOMDriver('#app')
});

http://cycle.js.org/basic-examples.html

17/24

7/19/2015

Basic examples Cycle.js

Body mass index


calculator
Now that we got the hang of Cycle.js apps
with state, lets tackle something a bit
larger. Consider the following BMI
calculator: it has a slider to select the
weight, a slider to select the height, and a
text indicates the calculated BMI from
those weight and height values selected.

http://cycle.js.org/basic-examples.html

18/24

7/19/2015

Basic examples Cycle.js

In the previous example, we had the


actions decrement and increment. In this
example, we have change weight and
change height. These seem
straightforward to implement.
let changeWeight$ = DOM.get('#weight'
.map(ev => ev.target.value);
let changeHeight$ = DOM.get('#height'
.map(ev => ev.target.value);

To combine these two actions and use


their values to compute the BMI, we use
the RxJS combineLatestoperator.
We saw in the previous example that
mergehad OR semantics.
combineLatesthas, on the other
hand, AND semantics. For instance, to
compute the BMI, we need a weight
value and and a heightvalue.

let bmi$ = Cycle.Rx.Observable.combineLates


changeWeight$.startWith(70),
changeHeight$.startWith(170),
(weight, height) => {
let heightMeters = height * 0.01
return weight / (heightMeters *
}
);

Now we just need a function to visualize


the BMI result and the sliders. We do that
by mapping bmi$to an Observable of
http://cycle.js.org/basic-examples.html

19/24

7/19/2015

Basic examples Cycle.js

VTree, and giving that to the DOMdriver.


import Cycle from '@cycle/core';

import {h, makeDOMDriver} from '@cycle/dom'


function main({DOM}) {
let changeWeight$ = DOM.get('#weight'
.map(ev => ev.target.value);
let changeHeight$ = DOM.get('#height'
.map(ev => ev.target.value);

let bmi$ = Cycle.Rx.Observable.combineLat


changeWeight$.startWith(70),
changeHeight$.startWith(170),
(weight, height) => {
let heightMeters = height * 0.01

return Math.round(weight / (heightMet


}
);
return {
DOM: bmi$.map(bmi =>
h('div', [
h('div', [
'Weight ___kg',
h('input#weight', {type:
]),
h('div', [
'Height ___cm',
h('input#height', {type:
]),
h('h2', 'BMI is ' + bmi)
])
)
};
}
Cycle.run(main, {
DOM: makeDOMDriver('#app')
});
http://cycle.js.org/basic-examples.html

20/24

7/19/2015

Basic examples Cycle.js

This code works, we can get the


calculated BMI when we move the slider.
However, if you have noticed, the labels
for weight and height do not show what
the slider is selecting. Instead, they just
show e.g. Weight ___kg, which is
useless since we do not know what value
we are choosing for the weight.
The problem happens because when we
map on bmi$, we do not have anymore
the weightand heightvalues.
Therefore, for the function which renders
the VTree, we need to use an Observable
which emits a complete amount of data
instead of just BMI data. We need a
state$Observable.

let state$ = Cycle.Rx.Observable.combineLat


changeWeight$.startWith(70),
changeHeight$.startWith(170),
(weight, height) => {
http://cycle.js.org/basic-examples.html

21/24

7/19/2015

Basic examples Cycle.js

let heightMeters = height * 0.01


let bmi = Math.round(weight / (
return {weight, height, bmi};
}
);

Below is the program that uses state$


to render all dynamic values correctly to
the DOM.
import Cycle from '@cycle/core';

import {h, makeDOMDriver} from '@cycle/dom'


function main({DOM}) {
let changeWeight$ = DOM.get('#weight'
.map(ev => ev.target.value);
let changeHeight$ = DOM.get('#height'
.map(ev => ev.target.value);
let state$ = Cycle.Rx.Observable.
changeWeight$.startWith(70),
changeHeight$.startWith(170),
(weight, height) => {
let heightMeters = height * 0.01
let bmi = Math.round(weight /
return {weight, height, bmi};
}
);
return {
DOM: state$.map(({weight, height
h('div', [
h('div', [
'Weight ' + weight + 'kg'
h('input#weight', {type:
]),
h('div', [
'Height ' + height + 'cm'
h('input#height', {type:
http://cycle.js.org/basic-examples.html

22/24

7/19/2015

Basic examples Cycle.js

]),
h('h2', 'BMI is ' + bmi)
])
)
};
}
Cycle.run(main, {
DOM: makeDOMDriver('#app')
});

Great, this program functions exactly like


we want it to. Weight and height labels
react to the sliders being dragged, and
the BMI result gets recalculated as well.
However, we wrote all code inside one
function: main(). This approach
doesnt scale, and even for a small sized
app like this, it already looks too large,
doing too many things.
We need a proper architecture for user
http://cycle.js.org/basic-examples.html

23/24

7/19/2015

Basic examples Cycle.js

interfaces that follows the reactive,


functional, and cyclic principles of
Cycle.js. That is the subject of our next
chapter.

http://cycle.js.org/basic-examples.html

By Andr Staltz with the

MIT License

support of Futurice

2015

24/24

Вам также может понравиться