24
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 driverRespons // through a series of RxJ }; 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

Basic Examples › Cycle

Embed Size (px)

DESCRIPTION

js

Citation preview

  • 7/19/2015 Basic examples Cycle.js

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

    BASIC EXAMPLES

    Cycle.js apps will always include at leastthree important components: main(),drivers, and run(). In main(), welisten to drivers (the input to main), andwe speak to drivers (the output ofmain). In Haskell 1.0 jargon, main()

    takes driver response Observables andoutput driver request Observables.Cycle.run() ties main() and

    drivers together, as we saw in the lastchapter.

    function main(driverResponses) { let driverRequests = { DOM: // transform driverResponses.DOM // through a series of RxJS operators }; return driverRequests;}

    let drivers = { DOM: makeDOMDriver('#app')};

    Cycle.run(main, drivers);

    In the case of the DOM Driver, ourmain() will interact with the user

    through the DOM. Most of our examples

  • 7/19/2015 Basic examples Cycle.js

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

    will use the DOM Driver, but keep in mindCycle.js is modular and extensible. Youcould build an application, targettingnative mobile for instance, without usingthe DOM Driver.

    Toggle a checkbox

    Lets start from the assumption we havean index.html file with an element tocontain our app.

  • 7/19/2015 Basic examples Cycle.js

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

    let requests = {DOM: null}; return requests;}

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

    Cycle DOM is a package containing twodrivers and some helpers to use thoselibraries. A DOM Driver is created withmakeDOMDriver() and an HTML

    Driver (for server-side rendering) iscreated with makeHTMLDriver().CycleDOM also includes h() andsvg(), these are functions that outputvirtual-dom virtual elements, usually

    called VTree.

    Our main() for now does nothing. Ittakes driver responses and outputsdriver requests. To make somethingappear on the screen, we need to outputan Observable of VTree inrequests.DOM. The name DOM inrequests must match the name we

    gave in the drivers object given toCycle.run(). This is how Cycle.js

    knows which drivers to match with whichrequest Observables. This is also true forresponses: we listen to DOM events byusing responses.DOM.

  • 7/19/2015 Basic examples Cycle.js

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

    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 falsemapped to a VTree.Observable.just(x) creates a

    simple Observable which simply emits x

  • 7/19/2015 Basic examples Cycle.js

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

    once, then we used map() to convertthat to the virtual-dom VTreecontaining an and a element displaying off if thetoggled boolean was false, and

    displaying ON otherwise.

    This is nice: we can see the DOM elementsgenerated by the virtual-domelements created with h(). But if weclick the Toggle me checkbox, the labeloff under it does not change to ON.That is because we are not listening toDOM events. In essence, our main()isnt listening to the user. We do that byusing 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;}

  • 7/19/2015 Basic examples Cycle.js

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

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

    Notice the lines we changed, with NEW!.We now map change events on thecheckbox to the checked value of theelement (the first map()) to VTreesdisplaying that value. However, we needa .startWith() to give a defaultvalue to be converted to a VTree. Withoutthis, nothing would be shown! Why?Because our requests is reacting toresponses, but responses is

    reacting to requests. If no one triggersthe first event, nothing will happen. It isthe same effect as meeting a stranger,and not having anything to say. Someoneneeds to take the initiative to start theconversation. That is what main() isdoing: kickstarting the interaction, and

  • 7/19/2015 Basic examples Cycle.js

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

    then letting subsequent actions bemutual reactions between main() andthe DOM Driver.

    Displaying data fromHTTP requests

    One of the most obvious requirementsweb apps normally have is to fetch somedata from the server and display that.How would we build that with Cycle.js?

    Suppose we have a backend with adatabase containing 10 users. We want tohave a frontend with one button get arandom user, and to display the usersdetails, like name and email. This is whatwe want to achieve:

    Essentially we just need to make arequest for the endpoint/user/:number whenever the button

  • 7/19/2015 Basic examples Cycle.js

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

    is clicked. Where would this HTTP requestfit in a Cycle.js app?

    Recall the dialogue abstraction,mentioned also in the previouscheckbox example. The app generatesVTree Observables as requests to theDOM. But our apps should also be ableto generate different kinds of requests.The most typical type of request is anHTTP request. Since these are not in anyway related to the DOM, we need adifferent driver to handle them.

    The HTTP Driver is similar in style to theDOM Driver: it expects a requestObservable, and gives you a responseObservable. Instead of studying in detailshow the HTTP Driver works, lets see howa basic HTTP example looks like.

    If HTTP requests are sent when clicks onthe button happen, then the HTTPrequest Observable should dependdirectly on the button click Observable.Roughly, this:

    function main(responses) { // ...

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

    const USERS_URL = 'http://jsonplaceholder.typicode.com/users/' // This is the HTTP request Observable let getRandomUser$ = click$.map(()

  • 7/19/2015 Basic examples Cycle.js

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

    let randomNum = Math.round(Math return { url: USERS_URL + String(randomNum method: 'GET' }; });

    // ...}

    getRandomUser$ is the Observablewe give to the HTTP Driver, by returning itfrom the main() function:

    function main(responses) { // ...

    return { // ... HTTP: getRandomUser$ };}

    We still need to display data for thecurrent user, and this comes only whenwe get an HTTP response. For thatpurpose we need the Observable of userdata to depend directly on the HTTPresponse Observable. This is availablefrom the mains input:responses.HTTP (the name HTTP

    needs to match the driver name you gavefor the HTTP driver when callingCycle.run()).

  • 7/19/2015 Basic examples Cycle.js

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

    function main(responses) { // ...

    let user$ = responses.HTTP .filter(res$ => res$.request.url .mergeAll() .map(res => res.body);

    // ...}

    responses.HTTP is an Observable ofall the network responses this app isobserving. Because it could potentiallyinclude responses unrelated to userdetails, we need to filter() it. Andwe also mergeAll(), to flatten theObservable of Observables. This mightfeel like magic right now, so read theHTTP Driver docs if youre curious of thedetails. We map each response res tores.body in order to get the JSON

    data from the response and ignore otherfields like HTTP status.

    We still havent defined the rendering inour app. We should display on the DOMwhatever data we have from the currentuser in user$. So the VTree Observablevtree$ should depend directly onuser$, like this:

    function main(responses) { // ...

  • 7/19/2015 Basic examples Cycle.js

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

    let vtree$ = user$.map(user => h('div.users', [ h('button.get-random', 'Get random user' 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 anyuser$ event, because those only

    happen when the user clicks. This is thesame conversation initiative problemwe saw in the previous checkboxexample. So we need to make user$start with a null user, and in casevtree$ sees a null user, it renders just

    the button. Unless, if we have real userdata, we display the name, the email, andthe website:

    function main(responses) { // ...

    let user$ = responses.HTTP .filter(res$ => res$.request.url .mergeAll() .map(res => res.body) .startWith(null); // NEW!

  • 7/19/2015 Basic examples Cycle.js

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

    let vtree$ = user$.map(user => h('div.users', [ h('button.get-random', 'Get random user' user === null ? null : h('div.user-details' h('h1.user-name', user.name h('h4.user-email', user.email h('a.user-website', {href: ]) ]) );

    // ...}

    We give vtree$ to the DOM Driver, andit renders those for us. All done, and thewhole 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.typicode.com/users/' 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);

  • 7/19/2015 Basic examples Cycle.js

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

    let vtree$ = user$.map(user => h('div.users', [ h('button.get-random', 'Get random user' user === null ? null : h('div.user-details' 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

  • 7/19/2015 Basic examples Cycle.js

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

    decrement a counter

    We saw how to use the dialogue patternof building user interfaces, but ourexamples didnt have state: the label justreacted to the checkbox event, and theuser details view just showed what camefrom the HTTP response. Normallyapplications have state in memory, solets see how to build a Cycle.js app forthat case.

    If we have a counter Observable (emittingevents 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 dollarsign `$` mean?

    Notice we used the name count$for the Observable of current countervalues. The dollar sign $ suffixed toa name is a soft convention toindicate that the variable is an

  • 7/19/2015 Basic examples Cycle.js

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

    Observable. It is a naming helper toindicate types.

    Suppose you have an Observable ofVTree depending on an Observableof name string

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

    Notice that the function inside maptakes name as argument, while theObservable is named name$. Thenaming convention indicates thatname is the value being emitted byname$. In general, foobar$

    emits foobar. Without thisconvention, if name$ would benamed simply name, it wouldconfuse readers about the typesinvolved. Also, name$ is succinctcompared to alternatives likenameObservable,nameStream, or nameObs. This

    convention can also be extended toarrays: use plurality to indicate thetype is an array. Example: vtreesis an array of vtree, but vtree$is an Observable of vtree.

    But how to create count$? Clearly itmust depend on increment clicks and

  • 7/19/2015 Basic examples Cycle.js

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

    decrement clicks. The former shouldmean 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 merge operator allows us to get anevent stream of actions, either incrementor decrement actions. In this sense,merge has OR semantics. But this still

    isnt count$, it is just action$.

    count$ should begin with zero, whichjustifies the use of a startWith(0)operator, but besides that we need ascan() as well:

    let count$ = action$.startWith(0).scan

    What does scan do? It is similar toreduce for Array, allowing us to

    accumulate values over the sequence. Infunctional programming languages, it isoften called fold. For instance, in Elm,foldp is equivalent to scan. The

    name foldp indicates we are foldingthe sequence from the past.

  • 7/19/2015 Basic examples Cycle.js

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

    10 +1+1+1 1

    1

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

    0 0 12 1

    If we put action$ and count$together in our main(), we canimplement 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')});

  • 7/19/2015 Basic examples Cycle.js

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

    Body mass indexcalculator

    Now that we got the hang of Cycle.js appswith state, lets tackle something a bitlarger. Consider the following BMIcalculator: it has a slider to select theweight, a slider to select the height, and atext indicates the calculated BMI fromthose weight and height values selected.

  • 7/19/2015 Basic examples Cycle.js

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

    In the previous example, we had theactions decrement and increment. In thisexample, we have change weight andchange height. These seemstraightforward 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 usetheir values to compute the BMI, we usethe RxJS combineLatest operator.We saw in the previous example thatmerge had OR semantics.combineLatest has, on the other

    hand, AND semantics. For instance, tocompute the BMI, we need a weightvalue and and a height value.

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

    Now we just need a function to visualizethe BMI result and the sliders. We do thatby mapping bmi$ to an Observable of

  • 7/19/2015 Basic examples Cycle.js

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

    VTree, and giving that to the DOM driver.

    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.combineLatest changeWeight$.startWith(70), changeHeight$.startWith(170), (weight, height) => { let heightMeters = height * 0.01 return Math.round(weight / (heightMeters } );

    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')});

  • 7/19/2015 Basic examples Cycle.js

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

    This code works, we can get thecalculated BMI when we move the slider.However, if you have noticed, the labelsfor weight and height do not show whatthe slider is selecting. Instead, they justshow e.g. Weight ___kg, which isuseless since we do not know what valuewe are choosing for the weight.

    The problem happens because when wemap on bmi$, we do not have anymorethe weight and height values.Therefore, for the function which rendersthe VTree, we need to use an Observablewhich emits a complete amount of datainstead of just BMI data. We need astate$ Observable.

    let state$ = Cycle.Rx.Observable.combineLatest changeWeight$.startWith(70), changeHeight$.startWith(170), (weight, height) => {

  • 7/19/2015 Basic examples Cycle.js

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

    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 tothe 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:

  • 7/19/2015 Basic examples Cycle.js

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

    ]), h('h2', 'BMI is ' + bmi) ]) ) };}

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

    Great, this program functions exactly likewe want it to. Weight and height labelsreact to the sliders being dragged, andthe BMI result gets recalculated as well.

    However, we wrote all code inside onefunction: main(). This approachdoesnt scale, and even for a small sizedapp like this, it already looks too large,doing too many things.

    We need a proper architecture for user

  • 7/19/2015 Basic examples Cycle.js

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

    interfaces that follows the reactive,functional, and cyclic principles ofCycle.js. That is the subject of our nextchapter.

    By Andr Staltz with thesupport of Futurice

    MIT License 2015