├── .editorconfig ├── .gitignore ├── 404.html ├── LICENSE ├── README.md ├── _config.yml ├── _includes ├── footer.html ├── homepage-content.md └── img │ ├── actuators-senses-input-output.svg │ ├── actuators-senses.svg │ ├── completed-stream.svg │ ├── custom-element-drivers.svg │ ├── cycle-nested-frontpage.svg │ ├── dataflow-component.svg │ ├── dataflow-minimap.svg │ ├── egghead.svg │ ├── fold-counter.svg │ ├── hci-inputs-outputs.svg │ ├── human-computer-diagram.svg │ ├── intent-translation.svg │ ├── main-domdriver-side-effects.svg │ ├── main-eq-mvi.svg │ ├── modules-foo-bar.svg │ ├── mvc-diagram.svg │ ├── nested-components.svg │ ├── passive-foo-bar.svg │ ├── reactive-foo-bar.svg │ ├── simple-human-computer.svg │ └── view-translation.svg ├── _layouts ├── blank.html └── default.html ├── _posts ├── 2015-01-21-community.md ├── 2015-01-22-documentation.md ├── 2015-01-23-drivers.md ├── 2015-01-25-components.md ├── 2015-01-26-model-view-intent.md ├── 2015-01-28-basic-examples.md ├── 2015-01-29-streams.md ├── 2015-01-30-dialogue.md └── 2015-01-31-getting-started.md ├── _sass ├── basic.sass ├── colors.sass ├── dimens.sass ├── documentation.sass ├── homepage.sass ├── mixins.sass ├── typography.sass └── vendor.sass ├── android-icon-144x144.png ├── android-icon-192x192.png ├── android-icon-36x36.png ├── android-icon-48x48.png ├── android-icon-72x72.png ├── android-icon-96x96.png ├── apple-icon-114x114.png ├── apple-icon-120x120.png ├── apple-icon-144x144.png ├── apple-icon-152x152.png ├── apple-icon-180x180.png ├── apple-icon-57x57.png ├── apple-icon-60x60.png ├── apple-icon-72x72.png ├── apple-icon-76x76.png ├── apple-icon-precomposed.png ├── apple-icon.png ├── archive.html ├── browserconfig.xml ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon-96x96.png ├── favicon.ico ├── img ├── cyclejs_logo.svg └── egghead.png ├── index.html ├── js └── example-hello-world.min.js ├── main.sass ├── manifest.json ├── ms-icon-144x144.png ├── ms-icon-150x150.png ├── ms-icon-310x310.png └── ms-icon-70x70.png /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | indent_style = space 7 | indent_size = 2 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .sass-cache/ 3 | .DS_Store 4 | _site/ 5 | 6 | # Editors 7 | .idea/ 8 | 9 | .jekyll-metadata 10 | -------------------------------------------------------------------------------- /404.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | --- 4 | 5 |
6 |
7 |

404

8 |
Page not found
9 |
10 |
11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Andre Staltz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cycle.js documentation site 2 | 3 | cyclejs.github.io 4 | 5 | Built with Jekyll 6 | 7 | [![JS.ORG](https://img.shields.io/badge/js.org-cycle-ffb400.svg?style=flat-square)](http://js.org) 8 | 9 | ## Why are Issues unavailable? 10 | 11 | We use only one repository for issues. [**Open the issue at Cycle Core repo.**](https://github.com/cyclejs/core/issues) 12 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | title: "Cycle.js" 2 | author: "Andre Staltz" 3 | description: "A functional and reactive JavaScript framework for predictable code" 4 | more: "read more" 5 | url: "http://cycle.js.org/" 6 | permalink: none 7 | 8 | excerpt_separator: "\n\n\n" 9 | defaults: [{scope: {path: ""}, values: {layout: "default"}}, {scope: {path: "", type: "posts"}, values: {comments: true}}] 10 | gems: 11 | - octopress-autoprefixer 12 | - kramdown 13 | - jekyll-redirect-from 14 | whitelist: 15 | - jekyll-redirect-from 16 | -------------------------------------------------------------------------------- /_includes/footer.html: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /_includes/homepage-content.md: -------------------------------------------------------------------------------- 1 |

Your app and the external world as a circuit

2 | 3 |

4 | {% include /img/cycle-nested-frontpage.svg %} 5 |

6 | 7 | Cycle's core abstraction is your application as a pure function `main()` where inputs are read effects (*sources*) from the external world and outputs (*sinks*) are write effects to affect the external world. These I/O effects in the external world are managed by *drivers*: plugins that handle DOM effects, HTTP effects, etc. 8 | 9 | The internals of `main()` are built using Reactive Programming primitives, which maximizes separation of concerns and provides a fully declarative way of organizing your code. The *dataflow* is plainly visible in the code, making it readable and traceable. 10 | 11 | ## Example 12 | 13 | {% highlight text %} 14 | npm install xstream @cycle/xstream-run @cycle/dom 15 | {% endhighlight %} 16 | 17 | {% highlight js %} 18 | import {run} from '@cycle/xstream-run'; 19 | import {div, label, input, hr, h1, makeDOMDriver} from '@cycle/dom'; 20 | 21 | function main(sources) { 22 | const sinks = { 23 | DOM: sources.DOM.select('.field').events('input') 24 | .map(ev => ev.target.value) 25 | .startWith('') 26 | .map(name => 27 | div([ 28 | label('Name:'), 29 | input('.field', {attrs: {type: 'text'}}), 30 | hr(), 31 | h1('Hello ' + name), 32 | ]) 33 | ) 34 | }; 35 | return sinks; 36 | } 37 | 38 | run(main, { DOM: makeDOMDriver('#app-container') }); 39 | {% endhighlight %} 40 | 41 |
42 | 43 |
44 | ## Functional and Reactive 45 | 46 | Functional enables "predictable" code, and Reactive enables "separated" code. Cycle.js apps are made of pure functions, which means you know they only take inputs and generate predictable outputs, without performing any I/O effects. The building blocks are reactive streams from libraries like [xstream](http://staltz.com/xstream), [RxJS](http://reactivex.io/rxjs) or [Most.js](https://github.com/cujojs/most/), which greatly simplify code related to events, asynchrony, and errors. Structuring the application with streams also separates concerns, because all [dynamic updates to a piece of data are co-located](/observables.html#reactive-programming) and impossible to change from outside. As a result, apps in Cycle are entirely `this`-less and have nothing comparable to imperative calls such as `setState()` or `update()`. 47 |
48 | 49 |
50 | ## Simple and Concise 51 | 52 | Cycle.js is a framework with very few concepts to learn. The core API has just one function: `run(app, drivers)`. Besides that, there are **streams**, **functions**, **drivers** (plugins for different types of I/O effects), and a helper function to isolate scoped components. This is a framework with very little "magic". Most of the building blocks are just JavaScript functions. Usually the lack of "magic" leads to very verbose code, but since functional reactive streams are able to build complex dataflows with a few operations, you will come to see how apps in Cycle.js are small and readable. 53 |
54 | 55 |
56 | ## Extensible and Testable 57 | 58 | Drivers are plugin-like simple functions that take messages from sinks and call imperative functions. All I/O effects are contained in drivers. This means your application is just a pure function, and it becomes easy to swap drivers around. The community has built drivers for [React Native](https://github.com/cyclejs/cycle-react-native), [HTML5 Notification](https://github.com/cyclejs/cycle-notification-driver), [Socket.io](https://github.com/cgeorg/cycle-socket.io), etc. Sources and sinks can be easily used as [Adapters and Ports](https://iancooper.github.io/Paramore/ControlBus.html). This also means testing is mostly a matter of feeding inputs and inspecting the output. No deep mocking needed. Your application is just a pure transformation of data. 59 |
60 | 61 | ## Explicit dataflow 62 | 63 | In every Cycle.js app, each of the stream declarations is a node in a dataflow graph, and the dependencies between declarations are arrows. This means there is a one-to-one correspondence between your code and *minimap*-like graph of the dataflow between external inputs and external outputs. 64 | 65 |
66 |
67 |

68 | {% include /img/dataflow-minimap.svg %} 69 |

70 |
71 |
72 | {% highlight js %} 73 | function main(sources) { 74 | const decrement$ = sources.DOM 75 | .select('.decrement').events('click').mapTo(-1); 76 | 77 | const increment$ = sources.DOM 78 | .select('.increment').events('click').mapTo(+1); 79 | 80 | const action$ = xs.merge(decrement$, increment$); 81 | const count$ = action$.fold((x, y) => x + y, 0); 82 | 83 | const vtree$ = count$.map(count => 84 | div([ 85 | button('.decrement', 'Decrement'), 86 | button('.increment', 'Increment'), 87 | p('Counter: ' + count) 88 | ]) 89 | ); 90 | return { DOM: vtree$ }; 91 | } 92 | {% endhighlight %} 93 |
94 |
95 | 96 | In many frameworks the flow of data is *implicit*: you need to build a mental model of how data moves around in your app. In Cycle.js, the flow of data is clear by reading your code. 97 | 98 | ## Composable 99 | 100 | Cycle.js has components, but unlike other frameworks, every single Cycle.js app, no matter how complex, is a function that can be reused in a larger Cycle.js app. 101 | 102 |

103 | {% include /img/nested-components.svg %} 104 |

105 | 106 | Sources and sinks are the interface between the application and the drivers, but they are also the interface between a child component and its parent. Cycle.js components can be simply GUI widgets like in other frameworks, but they are not limited to GUIs only. You can make Web Audio components, network requests components, and others, since the sources/sinks interface is not exclusive to the DOM. 107 | 108 | ## Learn it in 1h 37min 109 | 110 |

111 | {% include /img/egghead.svg %} 112 |

113 | 114 | Got 1 hour and 37 minutes? That is all it takes to learn the essentials of Cycle.js. Watch [**this free Egghead.io video course**](https://egghead.io/series/cycle-js-fundamentals) given by the creator of Cycle.js. Understand Cycle.js from within by following how it is built from scratch, then learn how to transform your ideas into applications. 115 | 116 | 117 | 118 | ## Supports... 119 | 120 | - [**Virtual DOM rendering**](https://github.com/cyclejs/cyclejs/tree/master/dom) 121 | - [**Server-side rendering**](https://github.com/cyclejs/cyclejs/tree/master/examples/isomorphic) 122 | - [**JSX**](http://cycle.js.org/getting-started.html) 123 | - [**TypeScript**](https://github.com/cyclejs/cyclejs/tree/master/examples/bmi-typescript) 124 | - [**React Native**](https://github.com/cyclejs/cycle-react-native) 125 | - [**Time traveling**](https://github.com/cyclejs/cycle-time-travel) 126 | - [**Routing with the History API**](https://github.com/cyclejs/history) 127 | - [**And more...**](https://github.com/cyclejs-community/awesome-cyclejs) 128 | -------------------------------------------------------------------------------- /_includes/img/actuators-senses-input-output.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Group 4 | Created with Sketch. 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | Human 24 | 25 | 26 | Senses 27 | 28 | 29 | Actuators 30 | 31 | 32 | Input 33 | 34 | 35 | Output 36 | 37 | 38 | Computer 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /_includes/img/actuators-senses.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Group 4 | Created with Sketch. 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | Human 24 | 25 | 26 | Senses 27 | 28 | 29 | Actuators 30 | 31 | 32 | Senses 33 | 34 | 35 | Actuators 36 | 37 | 38 | Computer 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /_includes/img/completed-stream.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | completed-observable 4 | Created with Sketch. 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | “next” Events 20 | 21 | 22 | complete 23 | 24 | 25 | time 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /_includes/img/custom-element-drivers.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | custom-element-drivers 4 | Created with Sketch. 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | customElem() 13 | 14 | 15 | 16 | app side 17 | 18 | 19 | (READ-ONLY) 20 | 21 | 22 | (WRITE-ONLY) 23 | 24 | 25 | Cycle.js 26 | 27 | 28 | Sink 29 | Observables 30 | 31 | 32 | Source 33 | Observables 34 | 35 | 36 | framework side 37 | 38 | 39 | 40 | 41 | props 42 | 43 | 44 | 45 | 46 | 47 | DOM 48 | 49 | 50 | 51 | 52 | 53 | events 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /_includes/img/cycle-nested-frontpage.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | cycle-nested-frontpage 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | Sources 12 | 13 | 14 | Sinks 15 | 16 | 17 | main() 18 | 19 | 20 | 21 | 22 | 23 | 24 | DOM side effects 25 | 26 | 27 | HTTP side effects 28 | 29 | 30 | Other side effects 31 | 32 | 33 | 34 | 35 | pure dataflow 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /_includes/img/dataflow-component.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dataflow component 4 | Created with Sketch. 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | props$ 27 | 28 | 29 | user events 30 | 31 | 32 | vtree$ 33 | 34 | 35 | value$ 36 | 37 | 38 | (a function) 39 | 40 | 41 | dataflow 42 | component 43 | 44 | 45 | Sources 46 | 47 | 48 | Sinks 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /_includes/img/dataflow-minimap.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | dataflow-minimap 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | decrement$ 38 | 39 | 40 | 41 | 42 | 43 | increment$ 44 | 45 | 46 | 47 | 48 | 49 | 50 | action$ 51 | 52 | 53 | 54 | 55 | 56 | count$ 57 | 58 | 59 | 60 | 61 | 62 | vtree$ 63 | 64 | 65 | 66 | 67 | 68 | DOMSink 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | DOMSource 81 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /_includes/img/egghead.svg: -------------------------------------------------------------------------------- 1 | Egghead_Logo_2 -------------------------------------------------------------------------------- /_includes/img/fold-counter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | scan-counter 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -1 27 | 28 | 29 | +1 30 | 31 | 32 | +1 33 | 34 | 35 | +1 36 | 37 | 38 | -1 39 | 40 | 41 | -1 42 | 43 | 44 | fold((x,y) => x+y, 0) 45 | 46 | 47 | 0 48 | 49 | 50 | 0 51 | 52 | 53 | 1 54 | 55 | 56 | 2 57 | 58 | 59 | 1 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /_includes/img/hci-inputs-outputs.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Group 4 | Created with Sketch. 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | Human 24 | 25 | 26 | Input 27 | 28 | 29 | Output 30 | 31 | 32 | Input 33 | 34 | 35 | Output 36 | 37 | 38 | Computer 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /_includes/img/human-computer-diagram.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | human-computer-diagram2 4 | Created with Sketch. 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | computer() 13 | 14 | 15 | 16 | app side 17 | 18 | 19 | a function 20 | 21 | 22 | Cycle.js 23 | 24 | 25 | Sink 26 | Streams 27 | 28 | 29 | Source 30 | Streams 31 | 32 | 33 | a function 34 | 35 | 36 | framework side 37 | 38 | 39 | 40 | 41 | human() 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /_includes/img/intent-translation.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | intent-translation 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | User’s 12 | mental 13 | model 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | Computer’s 24 | digital 25 | model 26 | 27 | 28 | 29 | 30 | Intent 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /_includes/img/main-domdriver-side-effects.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | main-domdriver-side-effects 4 | Created with Sketch. 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | main() 13 | 14 | 15 | 16 | app side 17 | 18 | 19 | VDOM to DOM 20 | 21 | 22 | WRITE 23 | 24 | 25 | DOM events 26 | 27 | 28 | READ 29 | 30 | 31 | Cycle.js 32 | 33 | 34 | Sink 35 | Streams 36 | 37 | 38 | Source 39 | Streams 40 | 41 | 42 | framework side 43 | 44 | 45 | 46 | 47 | domDriver() 48 | 49 | 50 | 51 | 52 | 53 | DOM 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /_includes/img/main-eq-mvi.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | main-eq-mvi 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | main 13 | 14 | 15 | equivalent to… 16 | 17 | 18 | DOM 19 | source 20 | 21 | 22 | DOM 23 | sink 24 | 25 | 26 | DOM 27 | source 28 | 29 | 30 | DOM 31 | sink 32 | 33 | 34 | intent 35 | 36 | 37 | model 38 | 39 | 40 | view 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /_includes/img/modules-foo-bar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | modules 4 | Created with Sketch. 5 | 6 | 7 | 8 | 9 | 10 | 11 | Foo 12 | 13 | 14 | Bar 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /_includes/img/mvc-diagram.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | mvc-diagram 4 | Created with Sketch. 5 | 6 | 7 | 8 | 9 | Updates 10 | 11 | 12 | Manipulates 13 | 14 | 15 | Manipulates 16 | 17 | 18 | Sees 19 | 20 | 21 | Uses 22 | 23 | 24 | 25 | 26 | Model 27 | 28 | 29 | 30 | 31 | 32 | View 33 | 34 | 35 | 36 | 37 | 38 | Controller 39 | 40 | 41 | 42 | 43 | 44 | User 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /_includes/img/nested-components.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | nested-components 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | Sources 13 | 14 | 15 | Sources 16 | 17 | 18 | Sinks 19 | 20 | 21 | Sinks 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /_includes/img/passive-foo-bar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | passive 4 | Created with Sketch. 5 | 6 | 7 | 8 | 9 | 10 | 11 | Foo 12 | 13 | 14 | Proactive 15 | 16 | 17 | Passive 18 | 19 | 20 | Bar 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /_includes/img/reactive-foo-bar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | reactive 4 | Created with Sketch. 5 | 6 | 7 | 8 | 9 | 10 | 11 | Foo 12 | 13 | 14 | Listenable 15 | 16 | 17 | Reactive 18 | 19 | 20 | Bar 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /_includes/img/simple-human-computer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Group 4 | Created with Sketch. 5 | 6 | 7 | 8 | 9 | 10 | 11 | Human 12 | 13 | 14 | Computer 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /_includes/img/view-translation.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | view-translation 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | User’s 12 | mental 13 | model 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | Computer’s 24 | digital 25 | model 26 | 27 | 28 | 29 | 30 | View 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /_layouts/blank.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {% if page.author %}{% endif %} 7 | 8 | {% if page.title %}{{ page.title }} › {% endif %}{{ site.title }} 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | {{ content }} 35 | 43 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /_layouts/default.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: blank 3 | --- 4 |
5 | 20 |
21 |
22 |

{{ page.title }}

23 | {% if page.path %} 24 | Edit on GitHub 25 | {% endif %} 26 |
27 |
28 | {{ content }} 29 |
30 |
31 |
32 | {% if page.next %}{% endif %} 33 | {% if page.previous %}{% endif %} 34 |
35 |
36 |
37 |
38 | {% if page.next %}{% endif %} 39 | {% if page.previous %}{% endif %} 40 |
41 |
42 | {% include footer.html %} 43 |
44 |
45 | -------------------------------------------------------------------------------- /_posts/2015-01-21-community.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Community" 3 | tags: chapters 4 | --- 5 | 6 | To engage with the community around Cycle.js, follow these resources: 7 | 8 | * The GitHub organization, [cyclejs](https://github.com/cyclejs), is the default location for finding most of the packages you'll need. 9 | * Ask, "_How do I...?_" questions in Gitter: [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/cyclejs/cyclejs) 10 | * Report bugs, discuss, and propose significant changes as a [GitHub issue](https://github.com/cyclejs/cyclejs/issues). 11 | 12 | ### Resources 13 | 14 | Visit [Awesome Cycle.js](https://github.com/cyclejs-community/awesome-cyclejs) to see a curated list of examples, presentations, blog posts, utilities and resources built by the community. 15 | 16 | ### Acknowledgements 17 | 18 | - This project is a grateful recipient of the [Futurice Open Source sponsorship program](http://futurice.com/blog/sponsoring-free-time-open-source-activities). 19 | - [@TylorS](https://github.com/TylorS) and [@Frikki](https://github.com/Frikki/) for fantastic contributions and involvement with Cycle.js. 20 | - [@dobrite](https://github.com/dobrite) for [boilerplate reduction ideas](https://github.com/cyclejs/core/issues/56). 21 | - [Nick Johnstone](https://github.com/Widdershin/) for writing libraries and spreading the word about Cycle.js. 22 | - [@erykpiast](https://github.com/erykpiast) for pull requests and great ideas. 23 | - [@pH200](https://github.com/pH200) for pull requests and amazing contributions. 24 | - [@cgeorg](https://github.com/cgeorg), [@laszlokorte](https://github.com/laszlokorte), and others in the Gitter chat for ideas, feedback, and active involvement with Cycle.js. 25 | - Organizers and sponsors of [CycleConf](http://cycleconf.com/) for bringing the community together in one place. 26 | -------------------------------------------------------------------------------- /_posts/2015-01-22-documentation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Documentation" 3 | tags: chapters 4 | --- 5 | 6 | Cycle.js is split in packages. Each package has its own API documentation, while this website covers how to integrate them all together. 7 | 8 | ## Run 9 | 10 | You will always need a `run()` function matching your stream library of choice. In the following list you can find documentation for these specific packages. 11 | 12 | - [`@cycle/xstream-run`](https://github.com/cyclejs/cyclejs/tree/master/xstream-run) [![npm (scoped)](https://img.shields.io/npm/v/@cycle/xstream-run.svg?maxAge=2592000)](https://www.npmjs.com/package/@cycle/xstream-run) 13 | - [`@cycle/rxjs-run`](https://github.com/cyclejs/cyclejs/tree/master/rxjs-run) [![npm (scoped)](https://img.shields.io/npm/v/@cycle/rxjs-run.svg?maxAge=2592000)](https://www.npmjs.com/package/@cycle/rxjs-run) 14 | - [`@cycle/rx-run`](https://github.com/cyclejs/cyclejs/tree/master/rx-run) [![npm (scoped)](https://img.shields.io/npm/v/@cycle/core.svg?maxAge=2592000)](https://www.npmjs.com/package/@cycle/rx-run) 15 | - [`@cycle/most-run`](https://github.com/cyclejs/cyclejs/tree/master/most-run) [![npm (scoped)](https://img.shields.io/npm/v/@cycle/most-run.svg?maxAge=2592000)](https://www.npmjs.com/package/@cycle/most-run) 16 | 17 | ## Drivers 18 | 19 | The following are official Cycle.js drivers and links to their documentation. 20 | 21 | - [`@cycle/dom`](https://github.com/cyclejs/cyclejs/tree/master/dom) [![npm (scoped)](https://img.shields.io/npm/v/@cycle/dom.svg?maxAge=2592000)](https://www.npmjs.com/package/@cycle/dom) 22 | - [`@cycle/http`](https://github.com/cyclejs/cyclejs/tree/master/http) [![npm (scoped)](https://img.shields.io/npm/v/@cycle/http.svg?maxAge=2592000)](https://www.npmjs.com/package/@cycle/http) 23 | - [`@cycle/jsonp`](https://github.com/cyclejs/cyclejs/tree/master/jsonp) [![npm (scoped)](https://img.shields.io/npm/v/@cycle/jsonp.svg?maxAge=2592000)](https://www.npmjs.com/package/@cycle/jsonp) 24 | 25 | ## Utilities 26 | 27 | - [`@cycle/isolate`](https://github.com/cyclejs/cyclejs/tree/master/isolate) [![npm (scoped)](https://img.shields.io/npm/v/@cycle/isolate.svg?maxAge=2592000)](https://www.npmjs.com/package/@cycle/isolate) 28 | - [`@cycle/collection`](https://github.com/cyclejs/collection/blob/master/README.md) [![npm (scoped)](https://img.shields.io/npm/v/@cycle/collection.svg?maxAge=2592000)](https://www.npmjs.com/package/@cycle/collection) 29 | -------------------------------------------------------------------------------- /_posts/2015-01-23-drivers.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Drivers" 3 | tags: chapters 4 | --- 5 | 6 | Throughout this documentation site we have extensively used *drivers*. The DOM Driver has been the most common one, but also the HTTP driver was used. 7 | 8 | What are drivers and when should you use them? When should you create your own driver, and how do they work? These are a few questions we will address in this chapter. 9 | 10 | #### Drivers are functions that listen to sink streams (their input), perform imperative side effects, and may return source streams (their output). 11 | 12 | They are meant for encapsulating imperative side effects in JavaScript. The rule of thumb is: whenever you have a JavaScript function such as `doSomething()` which returns nothing, it should be contained in a driver. 13 | 14 | Let's study what drivers do by analyzing the most common one: the DOM Driver. 15 | 16 | >

Why the name "driver"?

17 | > 18 | > In Haskell 1.0 Stream I/O, similar in nature to Cycle.js, there is a cyclic interaction between the program's `main` function and Haskell's `os` function. In operating systems, drivers are software interfaces to use some hardware devices, which incur side effects in the external world. 19 | > 20 | > In Cycle.js, one can consider the "operating system" to be the execution environment surrounding your application. Roughly speaking, the DOM, the console, JavaScript and JS APIs assume the role of the operating system for the web. We need *software adapters* to interface with the browser and other environments such as Node.js. Cycle.js drivers are there as adapters between the external world (including the user and the JavaScript execution environment) and the application world built with Cycle.js tools. 21 | 22 |

DOM Driver

23 | 24 | The DOM Driver is the most important and most common driver in Cycle.js. When building interactive web apps, it is probably the most important tool in Cycle.js. In fact, while Cycle *Run* function is only about 200 lines of code, Cycle *DOM* is at least 4 times larger. 25 | 26 | It's main purpose is to be a proxy to the user using the browser. Conceptually we would like to work assuming the existence of a `human()` function, as this diagram reminds us: 27 | 28 |

29 | {% include /img/human-computer-diagram.svg %} 30 |

31 | 32 | However, in practice, we write our `main()` function targeted at a `domDriver()`. For a user interacting with a browser, we only need to make our `main()` interact with the DOM. Whenever we need to show something to the user, we instead show that to the DOM, and the DOM together with the browser shows that to our user. When we need to detect the user's interaction events, we attach event listeners on the DOM, and the DOM will notify us when the user interacts with the browser on the computer. 33 | 34 |

35 | {% include /img/main-domdriver-side-effects.svg %} 36 |

37 | 38 | Notice there are two directions of interaction with the external world through the DOM. The *write* effect is the renderization of our Snabbdom VNodes to DOM elements which can be shown on the user's screen. The *read* effect is the detection of DOM events generated by the user manipulating the computer. 39 | 40 | The `domDriver()` manages these two effects while allowing them to be interfaced with the `main()`. The *input* to `domDriver()` captures instructions for the *write* effect, and the *read* effect is exposed as the *output* of `domDriver()`. The anatomy of the `domDriver()` function is roughly the following: 41 | 42 | {% highlight js %} 43 | function domDriver(vdom$) { 44 | // Use vdom$ as instructions to create DOM elements 45 | // ... 46 | return { 47 | select: function select(selector) { 48 | // returns an object with two functions: `events()` 49 | // and `elements()`. Function `events(eventType)` 50 | // returns the stream of `eventType` DOM events 51 | // happening on the elements matched by `selector`. 52 | // Function `elements()` is the stream of DOM 53 | // elements matching the given `selector`. 54 | } 55 | }; 56 | } 57 | {% endhighlight %} 58 | 59 | The input `vdom$` is the output from `main()`, and the output of `domDriver()` is the input to `main()`: 60 | 61 | {% highlight js %} 62 | function main(sources) { 63 | // Use sources.DOM.select(selector).events(eventType) 64 | // ... 65 | // Create vdom$ somehow 66 | // ... 67 | return { 68 | DOM: vdom$ 69 | }; 70 | } 71 | {% endhighlight %} 72 | 73 | As a recap: 74 | 75 | - `main()`: takes **sources** as input, returns **sinks** 76 | - `domDriver()`: takes **sinks** as input, performs write and read effects, returns **sources**. 77 | 78 |

Isolating side effects

79 | 80 | Drivers should always be associated with some side effect. As we saw, even though the DOM Driver's main purpose is to represent the user, it has write and read effects. 81 | 82 | In JavaScript, nothing stops you from writing your `main()` function with side effects. A simple `console.log()` is already a side effect. However, to keep `main()` pure and reap its benefits like testability and predictability, it is better to encapsulate all side effects in drivers. 83 | 84 | Imagine, for instance, a driver for network requests. By isolating the network request side effect, your application's `main()` function can focus on business logic related to the app's behavior, and not on lower-level instructions to interface with external resources. This also allows a simple method for testing network requests: you can replace the actual network driver with a fake network driver. It just needs to be a function that mimics the network driver function, and makes assertions. 85 | 86 | Avoid making drivers if they do not have effects to the external world somehow. Especially do not create drivers to contain business logic. This is most likely a code smell. 87 | 88 | Drivers should focus solely on being an interface for effects, and usually are libraries that simply enable your Cycle.js app to perform different effects. Sometimes, though, a one-liner driver can be created on the fly instead of being a library, for instance this simple logging driver: 89 | 90 | {% highlight js %} 91 | run(main, { 92 | log: msg$ => { msg$.addListener({next: msg => console.log(msg)}) } 93 | }); 94 | {% endhighlight %} 95 | 96 |

Read-only and write-only drivers

97 | 98 | Most drivers, like the DOM Driver, take *sinks* (to describe a *write*) and return *sources* (to catch *reads*). However, we might have valid cases for write-only drivers and read-only drivers. 99 | 100 | For instance, the one-liner `log` driver we just saw above is a write-only driver. Notice how it is a function that does not return any stream, it simply consumes the sink `msg$` it receives. 101 | 102 | Other drivers only create source streams that emit events to the `main()`, but don't take in any `sink` from `main()`. An example of such would be a read-only Web Socket driver, drafted below: 103 | 104 | {% highlight js %} 105 | function WSDriver(/* no sinks */) { 106 | return xs.create({ 107 | start: listener => { 108 | this.connection = new WebSocket('ws://localhost:4000'); 109 | connection.onerror = (err) => { 110 | listener.error(err) 111 | } 112 | connection.onmessage = (msg) => { 113 | listener.next(msg) 114 | } 115 | }, 116 | stop: () => { 117 | this.connection.close(); 118 | }, 119 | }); 120 | } 121 | {% endhighlight %} 122 | 123 |

How to make drivers

124 | 125 | You should only be reading this section if you have clear intentions to make a driver and expose it as a library (unless it's a one-liner driver). Typically, when writing a Cycle.js app, you do not need to create your own drivers. 126 | 127 | Consider first carefully which side effects your driver is responsible for. And can it have both read and write effects? 128 | 129 | Once you map out the read/write effects, consider how diverse those can be. Create an empathic API which covers the common cases elegantly. 130 | 131 | The **input** to the driver function is expected to be a single stream. This is a practical API for the app developer to use when returning the `sinks` object in `main()`. Notice how the DOM Driver takes a single `vdom$` stream as input, and how sophisticated and expressive VNodes from Snabbdom can be. On the other hand, don't always choose JavaScript objects as the values emitted in the Observable. Use objects when they make sense, and remember to keep the API simple rather than overly-generic. Don't over-engineer. 132 | 133 | As a second, optional, argument to the driver function, you can expect `runStreamAdapter`. A "Stream Adapter" is a library that knows how to convert between one specific stream library (like xstream or RxJS) to a generic adapter interface used internally in Cycle.js. This is needed for those cases your driver function needs to return a sophisticated source object which can create arbitrary streams using the same stream library that your application uses. If your app uses `@cycle/xstream-run`, then it uses under the hood `@cycle/xstream-adapter`, which is the argument `runStreamAdapter` given to drivers. If your app uses `@cycle/rxjs-run`, then `runStreamAdapter` is `@cycle/rxjs-adapter`, and so forth. Hence the driver function signature is: 134 | 135 | {% highlight js %} 136 | function myDriver(sink$, runStreamAdapter /* optional */) 137 | {% endhighlight %} 138 | 139 | The **output** of the driver function can either be a single stream or a *queryable collection* of streams. 140 | 141 | In the case of a single stream as output source, depending on how diverse the values emitted by this stream are, you might want to make those values easily filterable (using the xstream or RxJS `filter()` operator). Design an API which makes it easy to filter the stream, keeping in mind what was provided as the sink stream to the driver function. 142 | 143 | In some cases it is necessary to output a queryable collection of Observables, instead of a single one. A **queryable collection of Observables** is essentially a JavaScript object with a function used to choose a particular stream based on a parameter, e.g. `get(param)`. 144 | 145 | The DOM Driver, for instance, outputs a queryable collection of streams. The collection is in fact lazy: none of the streams outputted by `select(selector).events(eventType)` existed prior to the call of `events()`. This is because we cannot afford creating streams for *all* possible events on *all* elements on the DOM. Take inspiration from the lazy queryable collection of streams from the DOM Driver whenever the output source contains a large (possibly infinite) amount of streams. 146 | 147 | You most likely will need `runStreamAdapter` only for queryable collections of streams **and** if you want your driver usable by any other stream library. If you are building your driver only for your own use, then just write the driver using the same stream library that your application uses. 148 | 149 |

Example driver implementation

150 | 151 | Suppose you have a fake real-time channel API called `Sock`. It is able to connect to a remote peer, send messages, and receive push-based messages. The API for `Sock` is: 152 | 153 | {% highlight js %} 154 | // Establish a connection to the peer 155 | let sock = new Sock('unique-identifier-of-the-peer'); 156 | 157 | // Subscribe to messages received from the peer 158 | sock.onReceive(function (msg) { 159 | console.log('Received message: ' + msg); 160 | }); 161 | 162 | // Send a single message to the peer 163 | sock.send('Hello world'); 164 | {% endhighlight %} 165 | 166 | **How do we build a driver for `Sock`?** We start by identifying the effects. The *write* effect is `sock.send(msg)` and the *read* effect is the listener for received messages. Our `sockDriver(sink)` should take `sink` as instructions to perform the `send(msg)` calls. The output from `sockDriver()` should be `source`, containing all received messages. 167 | 168 | Since both input and output should be streams, it's easy to see `sink` in `sockDriver(sink)` should be a stream of outgoing messages to the peer. And conversely, the source should be a stream of incoming messages. This is a draft of our driver function: 169 | 170 | {% highlight js %} 171 | function sockDriver(outgoing$) { 172 | outgoing$.addListener({ 173 | next: outgoing => { 174 | sock.send(outgoing); 175 | }, 176 | error: () => {}, 177 | complete: () => {}, 178 | }); 179 | 180 | return xs.create({ 181 | start: listener => { 182 | sock.onReceive(function (msg) { 183 | listener.next(msg); 184 | }); 185 | }, 186 | stop: () => {}, 187 | }); 188 | } 189 | {% endhighlight %} 190 | 191 | The listener of `outgoing$` performs the `send()` side effect, and the stream returned based on `sock.onReceive` takes data from the external world. However, `sockDriver` is assuming `sock` to be available in the closure. As we saw, `sock` needs to be created with a constructor `new Sock()`. To solve this dependency, we need to create a factory that makes `sockDriver()` functions. 192 | 193 | {% highlight js %} 194 | function makeSockDriver(peerId) { 195 | let sock = new Sock(peerId); 196 | 197 | function sockDriver(outgoing$) { 198 | outgoing$.addListener({ 199 | next: outgoing => { 200 | sock.send(outgoing)); 201 | }, 202 | error: () => {}, 203 | complete: () => {}, 204 | }); 205 | 206 | return xs.create({ 207 | start: listener => { 208 | sock.onReceive(function (msg) { 209 | listener.next(msg); 210 | }); 211 | }, 212 | stop: () => {}, 213 | }); 214 | } 215 | 216 | return sockDriver; 217 | } 218 | {% endhighlight %} 219 | 220 | `makeSockDriver(peerId)` creates the `sock` instance, and returns the `sockDriver()` function. We use this in a Cycle.js app as such: 221 | 222 | {% highlight js %} 223 | function main(sources) { 224 | const incoming$ = sources.sock; 225 | // Create outgoing$ (stream of string messages) 226 | // ... 227 | return { 228 | sock: outgoing$ 229 | }; 230 | } 231 | 232 | run(main, { 233 | sock: makeSockDriver('B23A79D5-some-unique-id-F2930') 234 | }); 235 | {% endhighlight %} 236 | 237 | Notice we have the `peerId` specified when the driver is created in `makeSockDriver(peerId)`. If the `main()` needs to dynamically connect to different peers according to some logic, then we shouldn't use this API anymore. Instead, we need the driver function to take instructions as input, such as "connect to peerId", or "send message to peerId". This is one example of the considerations you should take when designing a driver API. 238 | 239 |

Drivers make Cycle.js extensible

240 | 241 | Cycle *Core* is a very small framework, and Cycle *DOM*'s Driver is available as an optional plugin for your app. This means it is simple to replace the DOM Driver with any other driver function providing interaction with the user. 242 | 243 | You can for instance fork the DOM Driver, adapt it to your preferences, and use it in a Cycle.js app. You can create a driver to interface with sockets. Drivers to perform network requests. Drivers meant for Node.js. Drivers that target other UI trees, such as `` or even native mobile UI. 244 | 245 | As a framework, it cannot be compared to monoliths which have ruled web development in the recent years. Cycle.js itself is after all just a small tool and a convention to create reactive dialogues with the external world using reactive streams. 246 | -------------------------------------------------------------------------------- /_posts/2015-01-26-model-view-intent.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Model-View-Intent" 3 | tags: chapters 4 | --- 5 | 6 | We can write our entire Cycle.js program inside the `main()` function, like we did in the [previous chapter](/basic-examples.html#body-mass-index-calculator). However, any programmer knows this isn't a good idea. Once `main()` grows too large, it becomes hard to maintain. 7 | 8 | #### MVI is a simple pattern to refactor the main() function into three parts: Intent (to listen to the user), Model (to process information), and View (to output back to the user). 9 | 10 |

11 | {% include img/main-eq-mvi.svg %} 12 |

13 | 14 | Let's see how we can refactor the `main()` function we wrote for calculating BMI: 15 | 16 | {% highlight js %} 17 | import xs from 'xstream'; 18 | import {run} from '@cycle/xstream-run'; 19 | import {div, input, h2, makeDOMDriver} from '@cycle/dom'; 20 | 21 | function main(sources) { 22 | const changeWeight$ = sources.DOM.select('.weight') 23 | .events('input') 24 | .map(ev => ev.target.value); 25 | 26 | const changeHeight$ = sources.DOM.select('.height') 27 | .events('input') 28 | .map(ev => ev.target.value); 29 | 30 | const weight$ = changeWeight$.startWith(70); 31 | const height$ = changeHeight$.startWith(170); 32 | 33 | const state$ = xs.combine(weight$, height$) 34 | .map(([weight, height]) => { 35 | const heightMeters = height * 0.01; 36 | const bmi = Math.round(weight / (heightMeters * heightMeters)); 37 | return {weight, height, bmi}; 38 | }); 39 | 40 | const vdom$ = state$.map(({weight, height, bmi}) => 41 | div([ 42 | div([ 43 | 'Weight ' + weight + 'kg', 44 | input('.weight', {type: 'range', min: 40, max: 140, value: weight}) 45 | ]), 46 | div([ 47 | 'Height ' + height + 'cm', 48 | input('.height', {type: 'range', min: 140, max: 210, value: height}) 49 | ]), 50 | h2('BMI is ' + bmi) 51 | ]) 52 | ); 53 | 54 | return { 55 | DOM: vdom$ 56 | }; 57 | } 58 | 59 | run(main, { 60 | DOM: makeDOMDriver('#app') 61 | }); 62 | {% endhighlight %} 63 | 64 | We have plenty of anonymous functions which could be refactored away from `main`, such as the BMI calculation, VNode rendering, etc. 65 | 66 | {% highlight diff %} 67 | import xs from 'xstream'; 68 |  import {run} from '@cycle/xstream-run'; 69 | import {div, input, h2, makeDOMDriver} from '@cycle/dom'; 70 | 71 | +function renderWeightSlider(weight) { 72 | + return div([ 73 | + 'Weight ' + weight + 'kg', 74 | + input('.weight', {type: 'range', min: 40, max: 140, value: weight}) 75 | + ]); 76 | +} 77 | 78 | +function renderHeightSlider(height) { 79 | + return div([ 80 | + 'Height ' + height + 'cm', 81 | + input('.height', {type: 'range', min: 140, max: 210, value: height}) 82 | + ]); 83 | +} 84 | 85 | +function bmi(weight, height) { 86 | + const heightMeters = height * 0.01; 87 | + return Math.round(weight / (heightMeters * heightMeters)); 88 | +} 89 | 90 | function main(sources) { 91 | const changeWeight$ = sources.DOM.select('.weight') 92 | .events('input') 93 | .map(ev => ev.target.value); 94 | 95 | const changeHeight$ = sources.DOM.select('.height') 96 | .events('input') 97 | .map(ev => ev.target.value); 98 | 99 | const weight$ = changeWeight$.startWith(70); 100 | const height$ = changeHeight$.startWith(170); 101 | 102 | const state$ = xs.combine(weight$, height$) 103 | .map(([weight, height]) => { 104 | - const heightMeters = height * 0.01; 105 | - const bmi = Math.round(weight / (heightMeters * heightMeters)); 106 | - return {weight, height, bmi}; 107 | + return {weight, height, bmi: bmi(weight, height)}; 108 | }); 109 | 110 | const vdom$ = state$.map(({weight, height, bmi}) => 111 | div([ 112 | - div([ 113 | - 'Weight ' + weight + 'kg', 114 | - input('.weight', {type: 'range', min: 40, max: 140, value: weight}) 115 | - ]), 116 | - div([ 117 | - 'Height ' + height + 'cm', 118 | - input('.height', {type: 'range', min: 140, max: 210, value: height}) 119 | - ]), 120 | + renderWeightSlider(weight), 121 | + renderHeightSlider(height), 122 | h2('BMI is ' + bmi) 123 | ]) 124 | ); 125 | 126 | return { 127 | DOM: vdom$ 128 | }; 129 | } 130 | 131 | run(main, { 132 | DOM: makeDOMDriver('#app') 133 | }); 134 |   135 | {% endhighlight %} 136 | 137 | `main` still has to handle too many concerns. Can we do better? Yes, we can, by using the insight that `state$.map(state => someVNode)` is a *View* function: renders visual elements as a transformation of state. Let's introduce `function view(state$)`. 138 | 139 | {% highlight diff %} 140 | import xs from 'xstream'; 141 |  import {run} from '@cycle/xstream-run'; 142 | import {div, input, h2, makeDOMDriver} from '@cycle/dom'; 143 | 144 | function renderWeightSlider(weight) { 145 | return div([ 146 | 'Weight ' + weight + 'kg', 147 | input('.weight', {type: 'range', min: 40, max: 140, value: weight}) 148 | ]); 149 | } 150 | 151 | function renderHeightSlider(height) { 152 | return div([ 153 | 'Height ' + height + 'cm', 154 | input('.height', {type: 'range', min: 140, max: 210, value: height}) 155 | ]); 156 | } 157 | 158 | function bmi(weight, height) { 159 | const heightMeters = height * 0.01; 160 | return Math.round(weight / (heightMeters * heightMeters)); 161 | } 162 | 163 | +function view(state$) { 164 | + return state$.map(({weight, height, bmi}) => 165 | + div([ 166 | + renderWeightSlider(weight), 167 | + renderHeightSlider(height), 168 | + h2('BMI is ' + bmi) 169 | + ]) 170 | + ); 171 | +} 172 | 173 | function main(sources) { 174 | const changeWeight$ = sources.DOM.select('.weight') 175 | .events('input') 176 | .map(ev => ev.target.value); 177 | 178 | const changeHeight$ = sources.DOM.select('.height') 179 | .events('input') 180 | .map(ev => ev.target.value); 181 | 182 | const weight$ = changeWeight$.startWith(70); 183 | const height$ = changeHeight$.startWith(170); 184 | 185 | const state$ = xs.combine(weight$, height$) 186 | .map(([weight, height]) => { 187 | return {weight, height, bmi: bmi(weight, height)}; 188 | }); 189 | 190 | - const vdom$ = state$.map(({weight, height, bmi}) => 191 | - div([ 192 | - renderWeightSlider(weight), 193 | - renderHeightSlider(height), 194 | - h2('BMI is ' + bmi) 195 | - ]) 196 | - ); 197 | + const vdom$ = view(state$); 198 | 199 | return { 200 | DOM: vdom$ 201 | }; 202 | } 203 | 204 | run(main, { 205 | DOM: makeDOMDriver('#app') 206 | }); 207 |   208 | {% endhighlight %} 209 | 210 | Now, `main` is much smaller. But is it doing *one thing*? We still have `changeWeight$`, `changeHeight$`, `weight$`, `height$`, `state$`, and the return using `view(state$)`. Normally when we work with a *View*, we also have a *Model*. What Models normally do is **manage state**. In our case, however, we have `state$` which is self-responsible for its own changes, because it is [reactive](/observables.html#reactive-programming). But anyway we have code that defines how `state$` depends on `changeWeight$` and `changeHeight$`. We can put that code inside a `model()` function. 211 | 212 | {% highlight diff %} 213 | import xs from 'xstream'; 214 |  import {run} from '@cycle/xstream-run'; 215 | import {div, input, h2, makeDOMDriver} from '@cycle/dom'; 216 | 217 | // ... 218 | 219 | +function model(changeWeight$, changeHeight$) { 220 | + const weight$ = changeWeight$.startWith(70); 221 | + const height$ = changeHeight$.startWith(170); 222 | + 223 | + return xs.combine(weight$, height$) 224 | + .map(([weight, height]) => { 225 | + return {weight, height, bmi: bmi(weight, height)}; 226 | + }); 227 | +} 228 | 229 | function view(state$) { 230 | return state$.map(({weight, height, bmi}) => 231 | div([ 232 | renderWeightSlider(weight), 233 | renderHeightSlider(height), 234 | h2('BMI is ' + bmi) 235 | ]) 236 | ); 237 | } 238 | 239 | function main(sources) { 240 | const changeWeight$ = sources.DOM.select('.weight') 241 | .events('input') 242 | .map(ev => ev.target.value); 243 | 244 | const changeHeight$ = sources.DOM.select('.height') 245 | .events('input') 246 | .map(ev => ev.target.value); 247 | 248 | - const weight$ = changeWeight$.startWith(70); 249 | - const height$ = changeHeight$.startWith(170); 250 | - 251 | - const state$ = xs.combine(weight$, height$) 252 | - .map(([weight, height]) => { 253 | - return {weight, height, bmi: bmi(weight, height)}; 254 | - }); 255 | + const state$ = model(changeWeight$, changeHeight$); 256 | 257 | const vdom$ = view(state$); 258 | 259 | return { 260 | DOM: view(state$) 261 | }; 262 | } 263 | 264 | run(main, { 265 | DOM: makeDOMDriver('#app') 266 | }); 267 |   268 | {% endhighlight %} 269 | 270 | `main` still defines `changeWeight$` and `changeHeight$`. What are these streams? They are event streams of *Actions*. In the [previous chapter about basic examples](/basic-examples.html#increment-and-decrement-a-counter) we had an `action$` stream for incrementing and decrementing a counter. These Actions are deduced or interpreted from DOM events. Their names indicate the user's *intentions*. We can group these stream definitions in an `intent()` function: 271 | 272 | {% highlight diff %} 273 | import xs from 'xstream'; 274 |  import {run} from '@cycle/xstream-run'; 275 | import {div, input, h2, makeDOMDriver} from '@cycle/dom'; 276 | 277 | // ... 278 | 279 | +function intent(domSource) { 280 | + return { 281 | + changeWeight$: domSource.select('.weight').events('input') 282 | + .map(ev => ev.target.value), 283 | + changeHeight$: domSource.select('.height').events('input') 284 | + .map(ev => ev.target.value) 285 | + }; 286 | +} 287 | 288 | -function model(changeWeight$, changeHeight$) { 289 | - const weight$ = changeWeight$.startWith(70); 290 | - const height$ = changeHeight$.startWith(170); 291 | +function model(actions) { 292 | + const weight$ = actions.changeWeight$.startWith(70); 293 | + const height$ = actions.changeHeight$.startWith(170); 294 | 295 | return xs.combine(weight$, height$) 296 | .map(([weight, height]) => { 297 | return {weight, height, bmi: bmi(weight, height)}; 298 | }); 299 | } 300 | 301 | function view(state$) { 302 | return state$.map(({weight, height, bmi}) => 303 | div([ 304 | renderWeightSlider(weight), 305 | renderHeightSlider(height), 306 | h2('BMI is ' + bmi) 307 | ]) 308 | ); 309 | } 310 | 311 | function main(sources) { 312 | - const changeWeight$ = sources.DOM.select('.weight') 313 | - .events('input') 314 | - .map(ev => ev.target.value); 315 | - 316 | - const changeHeight$ = sources.DOM.select('.height') 317 | - .events('input') 318 | - .map(ev => ev.target.value); 319 | + const actions = intent(sources.DOM); 320 | 321 | - const state$ = model(changeWeight$, changeHeight$); 322 | + const state$ = model(actions); 323 | 324 | const vdom$ = view(state$); 325 | 326 | return { 327 | DOM: vdom$ 328 | }; 329 | } 330 | 331 | run(main, { 332 | DOM: makeDOMDriver('#app') 333 | }); 334 |   335 | {% endhighlight %} 336 | 337 | `main` is finally small enough, and works on one level of abstraction, defining how actions are created from DOM events, flowing to model and then to view, and finally back to the DOM. Because these steps are a chain, we can refactor `main` to compose those three functions `intent`, `model`, and `view` together: 338 | 339 | {% highlight js %} 340 | function main(sources) { 341 | return {DOM: view(model(intent(sources.DOM)))}; 342 | } 343 | {% endhighlight %} 344 | 345 | Seems like we cannot achieve a simpler format for `main`. 346 | 347 |

Recap

348 | 349 | - `intent()` function 350 | - Purpose: interpret DOM events as user's intended actions 351 | - Input: DOM source 352 | - Output: Action Streams 353 | - `model()` function 354 | - Purpose: manage state 355 | - Input: Action Streams 356 | - Output: State Stream 357 | - `view()` function 358 | - Purpose: visually represent state from the Model 359 | - Input: State Stream 360 | - Output: Stream of Virtual DOM nodes as the DOM Driver sink 361 | 362 | **Is Model-View-Intent an architecture?** Is this a new architecture? If so, how is it different to Model-View-Controller? 363 | 364 |

What MVC is really about

365 | 366 | [Model-View-Controller](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller) (MVC) has existed since the 80s as the cornerstone architecture for user interfaces. It has inspired multiple other important architectures such as [MVVM](https://en.wikipedia.org/wiki/Model_View_ViewModel) and [MVP](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93presenter). 367 | 368 | MVC is characterized by the Controller: a component which manipulates the other parts, updating them accordingly whenever the user does an action. 369 | 370 |

371 | {% include img/mvc-diagram.svg %} 372 |

373 | 374 | The Controller in MVC is incompatible with our reactive ideals, because it is a proactive component (implying either passive Model or passive View). However, the original idea in MVC was a method for translating information between two worlds: that of the computer's digital realm and the user's mental model. In Trygve's own words: 375 | 376 | > *The essential purpose of MVC is to bridge the gap between the human user's mental model and the digital model that exists in the computer.*
– [Trygve Reenskaug](http://heim.ifi.uio.no/~trygver/themes/mvc/mvc-index.html), inventor of MVC 377 | 378 | We can keep the MVC idea while avoiding a proactive Controller. In fact, if you observe our `view()` function, it does nothing else than transform state (digital model in the computer) to a visual representation useful for the user. View is a translation from one language to another: from binary data to English and other human-friendly languages. 379 | 380 |

381 | {% include img/view-translation.svg %} 382 |

383 | 384 | The opposite direction should be also a straightforward translation from the user's actions to *new* digital data. This is precisely what `intent()` does: interprets what the user is trying to affect in the context of the digital model. 385 | 386 |

387 | {% include img/intent-translation.svg %} 388 |

389 | 390 | Model-View-Intent (MVI) is **reactive**, **functional**, and follows the **core idea in MVC**. It is reactive because Intent observes the User, Model observes the Intent, View observes the Model, and the User observes the View. It is functional because each of these components is expressed as a [referentially transparent](https://en.wikipedia.org/wiki/Referential_transparency_%28computer_science%29) function over streams. It follows the original MVC purpose because View and Intent bridge the gap between the user and the digital model, each in one direction. 391 | 392 | >

Why CSS selectors for querying DOM events?

393 | > 394 | > Some programmers get concerned about `DOM.select(selector).events(eventType)` being a bad practice because it resembles spaghetti code in jQuery-based programs. They would rather prefer the virtual DOM elements to specify handler callbacks for events, such as `onClick={this.handleClick()}`. 395 | > 396 | > The choice for selector-based event querying in Cycle *DOM* is an informed and rational decision. This strategy enables MVI to be reactive and is inspired by the [open-closed principle](https://en.wikipedia.org/wiki/Open/closed_principle). 397 | > 398 | > **Important for reactivity and MVI.** If we had Views with `onClick={this.handleClick()}`, it would mean Views would *not* be anymore a simple translation from digital model to user mental model, because we also specify what happens as a consequence of the user's actions. To keep all parts in a Cycle.js app reactive, we need the View to simply declare a visual representation of the Model. Otherwise the View becomes a Proactive component. It is beneficial to keep the View responsible only for declaring how state is visually represented: it has a [single responsibility](https://en.wikipedia.org/wiki/Single_responsibility_principle) and is friendly to UI designers. It is also conceptually aligned with the [original View in MVC](http://heim.ifi.uio.no/~trygver/1979/mvc-2/1979-12-MVC.pdf): "*... a view should never know about user input, such as mouse operations and 399 | keystrokes.*" 400 | > 401 | > **Adding user actions shouldn't affect the View.** If you need to change Intent code to grab new kinds of events from the element, you don't need to modify code in the VTree element. The View stays untouched, and it should, because translation from state to DOM hasn't changed. 402 | > 403 | > The MVI strategy in Cycle DOM is to name most elements in your View with appropriate semantic classnames. Then you do not need to worry which of those can have event handlers, if all of them can. The classname is the common artifact which the View (DOM sink) and the Intent (DOM source) can use to refer to the same element. 404 | > 405 | > As we will see in the [Components](/components.html) chapter, risk of global className collision is not a problem in Cycle.js because of the `isolate()` helper. 406 | 407 | MVI is an architecture, but in Cycle it is nothing else than simply a function decomposition of `main()`. 408 | 409 |

410 | {% include img/main-eq-mvi.svg %} 411 |

412 | 413 | In fact, MVI itself just naturally emerged from our refactoring of `main()` split into functions. This means Model, View, and Intent are not rigorous containers where you should place code. Instead, they are just a convenient way of organizing code, and are very cheap to create because they are simply functions. Whenever convenient, you should split a function if it becomes too large. Use MVI as a guide on how to organize code, but don't confine your code within its limits if it doesn't make sense. 414 | 415 | This is what it means to say Cycle.js is *sliceable*. MVI is just one way of slicing `main()`. 416 | 417 | >

"Sliceable"?

418 | > 419 | > We mean the ability to refactor the program by extracting pieces of code without having to significantly modify their surroundings. Sliceability is a feature often found in functional programming languages, especially in LISP-based languages like [Clojure](https://en.wikipedia.org/wiki/Clojure), which use S-expressions to enable treating [*code as data*](https://en.wikipedia.org/wiki/Homoiconicity). 420 | 421 |

Pursuing DRY

422 | 423 | As good programmers writing good codebases, we must follow [DRY: Don't Repeat Yourself](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself). The MVI code we wrote is not entirely DRY. 424 | 425 | For instance, the View rendering of the sliders share a significant amount of code. And in the Intent, we have some duplication of the `DOM.select().events()` streams. 426 | 427 | {% highlight js %} 428 | function renderWeightSlider(weight) { 429 | return div([ 430 | 'Weight ' + weight + 'kg', 431 | input('.weight', {type: 'range', min: 40, max: 140, value: weight}) 432 | ]); 433 | } 434 | 435 | function renderHeightSlider(height) { 436 | return div([ 437 | 'Height ' + height + 'cm', 438 | input('.height', {type: 'range', min: 140, max: 210, value: height}) 439 | ]); 440 | } 441 | 442 | function intent(domSource) { 443 | return { 444 | changeWeight$: domSource.select('.weight') 445 | .events('input') 446 | .map(ev => ev.target.value), 447 | changeHeight$: domSource.select('.height') 448 | .events('input') 449 | .map(ev => ev.target.value) 450 | }; 451 | } 452 | {% endhighlight %} 453 | 454 | We could create functions to remove this duplication, as such: 455 | 456 | {% highlight js %} 457 | function renderSlider(label, value, unit, className, min, max) { 458 | return div([ 459 | '' + label + ' ' + value + unit, 460 | input('.' + className, {type: 'range', min, max, value}) 461 | ]); 462 | } 463 | 464 | function renderWeightSlider(weight) { 465 | return renderSlider('Weight', weight, 'kg', 'weight', 40, 140); 466 | } 467 | 468 | function renderHeightSlider(height) { 469 | return renderSlider('Height', height, 'cm', 'height', 140, 210); 470 | } 471 | 472 | function getSliderEvent(domSource, className) { 473 | return domSource.select('.' + className) 474 | .events('input') 475 | .map(ev => ev.target.value); 476 | } 477 | 478 | function intent(domSource) { 479 | return { 480 | changeWeight$: getSliderEvent(domSource, 'weight'), 481 | changeHeight$: getSliderEvent(domSource, 'height') 482 | }; 483 | } 484 | {% endhighlight %} 485 | 486 | But this still isn't ideal: we seem to have *more* code now. What we really want is just to create *labeled sliders*: one for height, and the other for weight. We should be able to build a generic and reusable labeled slider. In other words, we want the labeled slider to be a [component](/components.html). 487 | -------------------------------------------------------------------------------- /_posts/2015-01-29-streams.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Streams" 3 | tags: chapters 4 | redirect_from: 5 | - /observables 6 | --- 7 | 8 | Cycle.js is heavily dependent on reactive and functional streams. One such common example is the Observable from [RxJS](http://reactivex.io/intro.html). The name "Observable" immediately indicates a relation to the [Observer pattern](https://en.wikipedia.org/wiki/Observer_pattern). This pattern is key in many flavors of the [Model-View-Controller](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller) architectural pattern for user interfaces. For instance, typically the View observes changes in the Model. In the Observer pattern, this means the Model would be the "Subject" being observed. 9 | 10 | However, an Observable is not exactly the same concept as a traditional Subject from the Observer pattern, because Observables share features with Iterables from the [Iterator pattern](https://en.wikipedia.org/wiki/Iterator_pattern) as well. 11 | 12 | #### Observables are lazy event streams which can emit zero or more events, and may or may not finish. 13 | 14 | Observables originated from [ReactiveX](http://reactivex.io/intro.html), a Reactive Programming library. Reactivity is an important aspect in Cycle.js, and part of the core principles that led to the creation of this framework. There is a lot of confusion surrounding what Reactive means, so let's focus on that topic for a while. 15 | 16 |

Reactive Programming

17 | 18 | Say you have a module Foo and a module Bar. A *module* can be considered to be an object of an [OOP](https://en.wikipedia.org/wiki/Object-oriented_programming) class, or any other mechanism of encapsulating state. Let's assume all code lives in some module. Here we have an arrow from Foo to Bar, indicating that Foo somehow affects state living inside Bar. 19 | 20 |

21 | {% include img/modules-foo-bar.svg %} 22 |

23 | 24 | A practical example of such arrow would be: *whenever Foo does a network request, increment a counter in Bar*. If all code lives in some module, **where does this arrow live?** Where is it defined? The typical choice would be to write code inside Foo which calls a method in Bar to increment the counter. 25 | 26 | {% highlight js %} 27 | // This is inside the Foo module 28 | 29 | function onNetworkRequest() { 30 | // ... 31 | Bar.incrementCounter(); 32 | // ... 33 | } 34 | {% endhighlight %} 35 | 36 | Because Foo owns the relationship "*when network request happens, increment counter in Bar*", we say the arrow lives at the arrow tail, i.e., Foo. 37 | 38 |

39 | {% include img/passive-foo-bar.svg %} 40 |

41 | 42 | Bar is **passive**: it allows other modules to change its state. Foo is proactive: it is responsible for making Bar's state function correctly. The passive module is unaware of the existence of the arrow which affects it. 43 | 44 | The alternative to this approach inverts the ownership of the arrow, without inverting the arrow's direction. 45 | 46 |

47 | {% include img/reactive-foo-bar.svg %} 48 |

49 | 50 | With this approach, Bar listens to an event happening in Foo, and manages its own state when that event happens. 51 | 52 | {% highlight js %} 53 | // This is inside the Bar module 54 | 55 | Foo.addOnNetworkRequestListener(() => { 56 | self.incrementCounter(); // self is Bar 57 | }); 58 | {% endhighlight %} 59 | 60 | Bar is **reactive**: it is fully responsible for managing its own state by reacting to external events. Foo, on the other hand, is unaware of the existence of the arrow originating from its network request event. 61 | 62 | What is the benefit of this approach? It is Inversion of Control, mainly because Bar is responsible for itself. Plus, we can hide Bar's `incrementCounter()` as a private function. In the passive case, it was required to have `incrementCounter()` public, which means we are exposing Bar's internal state management outwards. It also means if we want to discover how Bar's counter works, we need to find all usages of `incrementCounter()` in the codebase. In this regard, Reactive and Passive seem to be dual to each other. 63 | 64 | | | Passive | Reactive | 65 | |-----------------------|-------------------------|---------------| 66 | | How does Bar work? | *Find usages* | Look inside | 67 | 68 | On the other hand, when applying the Reactive pattern, if you want to discover which modules are affected by an event in a Listenable module, you must find all usages of that event. 69 | 70 | | | Proactive | Listenable | 71 | |-----------------------------|-------------------------|---------------| 72 | | Which modules are affected? | Look inside | *Find Usages* | 73 | 74 | Passive/Proactive programming has been the default way of working for most programmers in imperative languages. Sometimes the Reactive pattern is used, but sporadically. The selling point for widespread Reactive programming is to build self-responsible modules which focus on their own functionality rather than changing external state. This leads to Separation of Concerns. 75 | 76 | The challenge with Reactive programming is this paradigm shift where we attempt to choose the Reactive/Listenable approach by default, before considering Passive/Proactive. After rewiring your brain to think Reactive-first, the learning curve flattens and most tasks become straightforward, especially when using a Reactive library like RxJS or *xstream*. 77 | 78 |

Streams in xstream

79 | 80 | Reactive programming can be implemented with: event listeners, [RxJS](http://reactivex.io/rxjs), [Bacon.js](http://baconjs.github.io/), [Kefir](https://rpominov.github.io/kefir/), [most.js](https://github.com/cujojs/most), [EventEmitter](https://nodejs.org/api/events.html), [Actors](https://en.wikipedia.org/wiki/Actor_model), and more. Even [spreadsheets](https://en.wikipedia.org/wiki/Reactive_programming) utilize the same idea of the cell formula defined at the arrow head. The above definition of Reactive programming is not limited to streams, and does not conflict with previous definitions of Reactive Programming. Cycle.js supports multiple stream libraries, such as [RxJS v4](https://github.com/Reactive-Extensions/RxJS), [RxJS v5](http://reactivex.io/rxjs), [xstream](http://staltz.com/xstream), and [most.js](https://github.com/cujojs/most), but by default we choose *xstream* because it was custom built for Cycle.js. 81 | 82 | In short, a *Stream* in *xstream* is an event stream which can emit zero or more events, and may or may not finish. If it finishes, then it does so by either emitting an error or a special "complete" event. 83 | 84 | {% highlight text %} 85 | Stream contract: (next)* (complete|error){0,1} 86 | {% endhighlight %} 87 | 88 | As an example, here is a typical Stream: it emits some events, then it eventually completes. 89 | 90 |

91 | {% include img/completed-stream.svg %} 92 |

93 | 94 | Streams can be listened to, just like EventEmitters and DOM events can. 95 | 96 | {% highlight js %} 97 | myStream.addListener({ 98 | next: function handleNextEvent(event) { 99 | // do something with `event` 100 | }, 101 | error: function handleError(error) { 102 | // do something with `error` 103 | }, 104 | complete: function handleCompleted() { 105 | // do something when it completes 106 | }, 107 | }); 108 | {% endhighlight %} 109 | 110 | Notice there are 3 handlers: one for events, one for errors, and one for "complete". 111 | 112 | *xstream* Streams become very useful when you transform them with the so-called *operators*, pure functions that create new Streams on top of existing ones. Given a Stream of click events, you can easily make a Stream of the number of times the user clicked. 113 | 114 | {% highlight js %} 115 | const clickCountStream = clickStream 116 | // each click represents "1 amount" 117 | .mapTo(1) 118 | // sum all events `1` over time, starting from 0 119 | .fold((count, x) => count + x, 0); 120 | {% endhighlight %} 121 | 122 | [Succinctness is Power](http://www.paulgraham.com/power.html), and *xstream* operators demonstrate that you can achieve a lot with a few well-placed operators. With only about [26 operators](https://github.com/staltz/xstream#methods-and-operators), you can build almost all programming patterns needed in a Cycle.js app. 123 | 124 | Knowing the basics of reactive streams programming is a prerequisite to getting work done with Cycle.js. Instead of teaching RxJS or *xstream* on this site, we recommend a few great learning resources, in case you need to learn more. *xstream* is similar to *RxJS*, so these resources apply: 125 | 126 | - [The introduction to Reactive Programming you've been missing](https://gist.github.com/staltz/868e7e9bc2a7b8c1f754): a thorough introduction to RxJS by Cycle.js author Andre Staltz. 127 | - [Introduction to Rx](http://introtorx.com/): an online book focused on Rx.NET, but most concepts map directly to RxJS. 128 | - [ReactiveX.io](http://reactivex.io/): official cross-language documentation site for ReactiveX. 129 | - [Learn Rx](http://reactivex.io/learnrx/): an interactive tutorial with arrays and Observables, by Jafar Husain. 130 | - [RxJS lessons at Egghead.io](https://egghead.io/technologies/rx) 131 | - [RxJS GitBook](http://xgrommx.github.io/rx-book/) 132 | - [RxMarbles](http://rxmarbles.com/): interactive diagrams of RxJS operators, built with Cycle.js. 133 | - [Async JavaScript at Netflix](https://www.youtube.com/watch?v=XRYN2xt11Ek): video of Jafar Husain introducing RxJS. 134 | 135 |

Streams in Cycle.js

136 | 137 | Now we are able to explain the types of `senses` and `actuators`, and what it means for the computer and human to be "mutually observed." 138 | 139 | In the simplest case, the computer generates pixels on the screen, and the human generates mouse and keyboard events. The computer observes these user inputs and the human observes the screen state generated by the computer. Notice that we can model each of these as *Streams*: 140 | 141 | - Computer's output: a stream of screen images. 142 | - Human's output: a stream of mouse/keyboard events. 143 | 144 | The `computer()` function takes the human's output as its input, and vice versa. They mutually observe each other's output. In JavaScript, we could write the computer function as a simple chain of *xstream* transformations on the input stream. 145 | 146 | {% highlight js %} 147 | function computer(userEventsStream) { 148 | return userEventsStream 149 | .map(event => /* ... */) 150 | .filter(someCondition) 151 | .map(transformItToScreenPixels) 152 | .flatten(); 153 | } 154 | {% endhighlight %} 155 | 156 | While doing the same with the `human()` function would be elegant, we cannot do that as a simple chain of operators because we need to leave the JavaScript environment and affect the external world. While conceptually the `human()` function can exist, in practice, we need to use *driver* functions in order to reach the external world. 157 | 158 | [Drivers](/drivers.html) are adapters to the external world, and each driver represents one aspect of external effects. For instance, the DOM Driver takes a "screen" Stream generated by the computer, and returns Streams of mouse and keyboard events. In between, the DOM Driver function produces "*write*" side effects to render elements on the DOM, and catches "*read*" side effects to detect user interaction. This way, the DOM Driver function can act on behalf of the user. The name "driver" is based off Operating System drivers, which have a similar kind of role: to create a bridge between devices and your software. 159 | 160 | Joining both parts, we have a computer function, often called `main()`, and a driver function, where the output of one is the input of the other. 161 | 162 | {% highlight text %} 163 | y = domDriver(x) 164 | x = main(y) 165 | {% endhighlight %} 166 | 167 | The circular dependency above cannot be solved if `=` means assignment, because that would be equivalent to the command `x = g(f(x))`, and `x` is undefined on the right-hand side. 168 | 169 | This is where Cycle.js comes in: you only need to specify `main()` and `domDriver()`, and give it to the Cycle.js `run()` command which connects them circularly. 170 | 171 | {% highlight js %} 172 | function main(sources) { 173 | const sinks = { 174 | DOM: // transform sources.DOM through 175 | // a series of xstream operators 176 | }; 177 | return sinks; 178 | } 179 | 180 | const drivers = { 181 | DOM: makeDOMDriver('#app') // a Cycle.js helper factory 182 | }; 183 | 184 | run(main, drivers); // solve the circular dependency 185 | {% endhighlight %} 186 | 187 | This is how the name "*Cycle.js*" came to be. It is a framework that solves the cyclic dependency of Observables which emerge during dialogues (mutual observations) between the Human and the Computer. 188 | 189 | >

Is Cycle.js a framework?

190 | > 191 | > The Cycle `run()` function is implemented in about 200 lines of code. It's a very small library. 192 | > 193 | > In the TodoMVC built with Cycle.js, these are the proportions of code each library or section comprises: 194 | > 195 | > - snabbdom (Virtual DOM library): 39.1 kB 196 | > - @cycle/dom: 28.9 kB 197 | > - xstream: 22.2 kB 198 | > - TodoMVC src: 15.3 kB 199 | > - @cycle/xstream-run: 4 kB 200 | > - misc: 59.5 kB 201 | > 202 | > Notice how small `@cycle/xstream-run` is. Cycle.js is simply an architecture for building reactive web applications: a set of ideas about how you should structure your app using *xstream* or *RxJS* or *most.js*. To help you out, it also provides some libraries to address common use cases: Cycle *DOM*, to help interact with the DOM, and Cycle run functions, to help create loops between the program and the drivers. 203 | 204 | Next, read the [basic examples](/basic-examples.html) which apply what we've learned so far about Cycle.js. 205 | -------------------------------------------------------------------------------- /_posts/2015-01-30-dialogue.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Dialogue Abstraction" 3 | tags: chapters 4 | --- 5 | 6 | Cycle.js introduces a [message passing](https://en.wikipedia.org/wiki/Message_passing) architecture to model [Human-Computer Interaction (HCI)](https://en.wikipedia.org/wiki/Human%E2%80%93computer_interaction). While most Frontend frameworks focus on Graphical User Interfaces on the DOM, Cycle.js is more holistic and suitable when you need to create any kind of Human-Computer Interaction. This chapter provides some insights into how the Cycle.js architecture was devised. HCI is a two-way process: both sides listen and speak. 7 | 8 |

9 | {% include img/simple-human-computer.svg %} 10 |

11 | 12 | The computer usually "speaks" visual information through a screen, which the human can "listen". Through different devices, the computer can reach different human senses. The human, on the other hand, often "speaks" commands to the computer through its hand, manipulating a mouse or touching a screen. 13 | 14 | #### Human-Computer Interaction is a dialogue: an ongoing exchange of messages between both sides. 15 | 16 | Both parties are able to "listen" to each other, and utter messages through their devices. In other words, we can say the human and the computer are mutually observed, or simply having a dialogue. The reason why we point out "mutual observation" is because it is related to Reactive Programming and will affect how we model this system. 17 | 18 |
19 |

Similarity with Haskell 1.0?

20 |

21 | This same dialogue concept can be found in Haskell 1.0 Stream-based I/O, where
type Dialogue = [Response] -> [Request]
is the model of interaction with the Operating System. [Response] is a stream of messages from the OS, and [Request] is a stream of messages to the OS. 22 |

23 |

24 | Cycle.js' abstraction was discovered independently from Haskell's Stream I/O. We try to take some inspiration from Haskell's Dialogue whenever convenient, but there are a few conceptual differences. Not all problems with Haskell's Dialogue exist in Cycle.js or matter to Cycle.js users, and vice-versa. This is due to different execution environment assumptions and different design decisions on modelling event streams. If you want more details on this topic, the following GOTO talk is recommended, centered around the history and theory behind Cycle.js: 25 |

26 |

27 | 28 |

29 |
30 | 31 | The computer is made of devices to interact with the human. *Output* devices present information to the human, and *input* devices detect actions from the human. The human possesses *actuators* and *senses*, which are connected to the computer's *input* and *output* devices, respectively. 32 | 33 |

34 | {% include /img/actuators-senses-input-output.svg %} 35 |

36 | 37 | The *inputs* and *outputs* of the computer suggest the computer's role in HCI can be expressed as a function. 38 | 39 | {% highlight js %} 40 | function computer(inputDevices) { 41 | // define the behavior of `outputDevices` somehow 42 | return outputDevices; 43 | } 44 | {% endhighlight %} 45 | 46 | We do not yet know what `inputDevices` and `outputDevices` should be in JavaScript, but for now try to appreciate the elegance of `computer()` as a pure function. Refactoring and architecture is just a matter of choosing the right composition of functions to make up `computer()`. 47 | 48 | When talking about inputs, outputs, senses, and actuators, it becomes difficult to describe the difference between senses and inputs, other than the former is often associated with humans, and the latter with computers. From the computer's perspective, a microphone and its drivers are how the computer is able to *sense* auditory information. In essence, when we ignore the nature of the human body and the physics of computing machines, the human and the computer are both simply agents with senses and actuators. 49 | 50 | {% highlight js %} 51 | function computer(senses) { 52 | // define the behavior of `actuators` somehow 53 | return actuators; 54 | } 55 | {% endhighlight %} 56 | 57 | The agents in this interaction are now **symmetric**: the actuator of one is connected to the senses of the other, and conversely. 58 | 59 |

60 | {% include /img/actuators-senses.svg %} 61 |

62 | 63 | The diagram above is an [anthropomorphism](https://en.wikipedia.org/wiki/Anthropomorphism) of the computer. If we take the opposite approach of objectifying humans as machines, then both human and computer would be functions with inputs and outputs. 64 | 65 |

66 | {% include /img/hci-inputs-outputs.svg %} 67 |

68 | 69 | Which suggests the human would be a function from its senses to its actuators. 70 | 71 | {% highlight js %} 72 | function human(senses) { 73 | // define the behavior of `actuators` somehow 74 | return actuators; 75 | } 76 | {% endhighlight %} 77 | 78 |
79 |

JSConf Budapest talk

80 |

81 | Watch Andre Staltz's talk on What if the user was a function? which addresses the same topics as this chapter does. 82 |

83 |

84 | 85 |

86 |
87 | 88 | While these abstractions seem to be natural choices for user interfaces, many questions still remain: 89 | 90 | - What are the types of `senses` and `actuators`? 91 | - When is the `human()` function called? 92 | - When is the `computer()` function called? 93 | - If the output of one is the input of the other, how do we solve the circular dependency `y = human(x)` and `x = computer(y)`? 94 | 95 | These are questions that drive the core architecture of Cycle.js, but to understand our solution, we first need to understand reactive streams: our building block for everything in Cycle.js. [Keep reading](/streams.html). 96 | -------------------------------------------------------------------------------- /_posts/2015-01-31-getting-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Getting Started" 3 | tags: chapters 4 | --- 5 | 6 |

Using create-cycle-app

7 | 8 | The quickest way to create a new project with Cycle.js is by using [create-cycle-app](https://github.com/cyclejs-community/create-cycle-app). 9 | 10 | {% highlight text %} 11 | npm install --global create-cycle-app 12 | create-cycle-app my-awesome-app 13 | {% endhighlight %} 14 | 15 | This will create a project called *my-awesome-app* (or the name you choose) with Cycle *Run* and Cycle *DOM*. 16 | 17 |

npm

18 | 19 | If you want to have more control over your project, the recommended channel for downloading Cycle.js as a package is through [npm](http://npmjs.org/). Create a new directory and run this inside that directory: 20 | 21 | {% highlight text %} 22 | npm install xstream @cycle/xstream-run @cycle/dom 23 | {% endhighlight %} 24 | 25 | This installs [xstream](http://staltz.com/xstream), Cycle *Run* using *xstream*, and Cycle *DOM*. Packages *xstream* and *Run* are the minimum required API to work with Cycle.js. The *Run* package includes a single function `run()`, and Cycle *DOM* is the standard DOM Driver providing a way to interface with the DOM. You can also use Cycle.js with other stream libraries like RxJS. Your options are: 26 | 27 | - `npm install xstream @cycle/xstream-run` (recommended if you don't know what to choose) 28 | - `npm install rx @cycle/rx-run` (for [RxJS v4](https://github.com/Reactive-Extensions/RxJS)) 29 | - `npm install rxjs @cycle/rxjs-run` (for [RxJS v5+](http://reactivex.io/rxjs)) 30 | - `npm install most @cycle/most-run` (for cujo.js [most.js](https://github.com/cujojs/most)) 31 | 32 | Packages of the type `@org/package` are [npm scoped packages](https://docs.npmjs.com/getting-started/scoped-packages), supported if your npm installation is version 2.11 or higher. Check your npm version with `npm --version` and upgrade in order to install Cycle.js. 33 | 34 | In case you are not dealing with a DOM-interfacing web application, you can omit `@cycle/dom` when installing. 35 | 36 |

First steps

37 | 38 | We recommend the use of a bundling tool such as [browserify](http://browserify.org/) or [webpack](http://webpack.github.io/), in combination with ES6 (a.k.a. ES2015) through a transpiler (e.g. [Babel](http://babeljs.io/) or [TypeScript](http://typescriptlang.org/)). Most of the code examples in this documentation assume some basic familiarity with ES6. Once your build system is set up, **write your main JavaScript source file like**: 39 | 40 | {% highlight js %} 41 | import xs from 'xstream'; 42 | import {run} from '@cycle/xstream-run'; 43 | import {makeDOMDriver} from '@cycle/dom'; 44 | 45 | // ... 46 | {% endhighlight %} 47 | 48 | The second line imports the function `run(main, drivers)`, where `main` is the entry point for our whole application, and `drivers` is a record of driver functions labeled by some name. 49 | 50 | **Create the `main` function and the `drivers` record:** 51 | 52 | {% highlight js %} 53 | import xs from 'xstream'; 54 | import {run} from '@cycle/xstream-run'; 55 | import {makeDOMDriver} from '@cycle/dom'; 56 | 57 | function main() { 58 | // ... 59 | } 60 | 61 | const drivers = { 62 | DOM: makeDOMDriver('#app') 63 | }; 64 | 65 | run(main, drivers); 66 | {% endhighlight %} 67 | 68 | `makeDOMDriver(container)` from Cycle *DOM* returns a driver function to interact with the DOM. This function is registered under the key `DOM` in the `drivers` object above. 69 | 70 | **Send messages from `main` to the `DOM` driver:** 71 | 72 | {% highlight js %} 73 | import xs from 'xstream'; 74 | import {run} from '@cycle/xstream-run'; 75 | import {makeDOMDriver, h1} from '@cycle/dom'; 76 | 77 | function main() { 78 | const sinks = { 79 | DOM: xs.periodic(1000).map(i => 80 | h1('' + i + ' seconds elapsed') 81 | ) 82 | }; 83 | return sinks; 84 | } 85 | 86 | const drivers = { 87 | DOM: makeDOMDriver('#app') 88 | }; 89 | 90 | run(main, drivers); 91 | {% endhighlight %} 92 | 93 | We have filled the `main()` function with some code: returns an object `sinks` which has an `xstream` stream defined under the name `DOM`. This indicates `main()` is sending the stream as messages to the DOM driver. Sinks are outgoing messages. The stream emits Virtual DOM `

` elements displaying `${i} seconds elapsed` changing over time every second, where `${i}` is replaced by `0`, `1`, `2`, etc. 94 | 95 | **Catch messages from `DOM` into `main` and vice-versa:** 96 | 97 | {% highlight js %} 98 | import xs from 'xstream'; 99 | import {run} from '@cycle/xstream-run'; 100 | import {makeDOMDriver, div, input, p} from '@cycle/dom'; 101 | 102 | function main(sources) { 103 | const sinks = { 104 | DOM: sources.DOM.select('input').events('click') 105 | .map(ev => ev.target.checked) 106 | .startWith(false) 107 | .map(toggled => 108 | div([ 109 | input({attrs: {type: 'checkbox'}}), 'Toggle me', 110 | p(toggled ? 'ON' : 'off') 111 | ]) 112 | ) 113 | }; 114 | return sinks; 115 | } 116 | 117 | const drivers = { 118 | DOM: makeDOMDriver('#app') 119 | }; 120 | 121 | run(main, drivers); 122 | {% endhighlight %} 123 | 124 | Function `main()` now takes `sources` as input. Just like the output `sinks`, the input `sources` follow the same structure: an object containing `DOM` as a property. `sources.DOM` is an object with a queryable API to get streams. Use `sources.DOM.select(selector).events(eventType)` to get a stream of `eventType` DOM events happening on the element(s) specified by `selector`. This `main()` function takes the stream of clicks happening on `input` elements, and maps those toggling events to Virtual DOM elements displaying a togglable checkbox. 125 | 126 | We used the `div()`, `input()`, `p()` helper functions to create virtual DOM elements for the respective `
`, ``, `

` DOM elements, but you can also use JSX with Babel. The following only works if you are building with Babel: (1) install the npm packages [babel-plugin-transform-react-jsx](http://babeljs.io/docs/plugins/transform-react-jsx/) and [snabbdom-jsx](https://www.npmjs.com/package/snabbdom-jsx); (2) specify a pragma for JSX as shown in the following example `.babelrc` file: 127 | 128 | {% highlight json %} 129 | { 130 | "presets": [ 131 | "es2015" 132 | ], 133 | "plugins": [ 134 | "syntax-jsx", 135 | ["transform-react-jsx", {"pragma": "html"}] 136 | ] 137 | } 138 | {% endhighlight %} 139 | 140 | (3) import Snabbdom JSX as `import {html} from 'snabbdom-jsx';`, and then you can utilize JSX: 141 | 142 | {% highlight html %} 143 | import xs from 'xstream'; 144 | import {run} from '@cycle/xstream-run'; 145 | import {makeDOMDriver} from '@cycle/dom'; 146 | import {html} from 'snabbdom-jsx'; 147 | 148 | function main(sources) { 149 | const sinks = { 150 | DOM: sources.DOM.select('input').events('click') 151 | .map(ev => ev.target.checked) 152 | .startWith(false) 153 | .map(toggled => 154 |

155 | Toggle me 156 |

{toggled ? 'ON' : 'off'}

157 |
158 | ) 159 | }; 160 | return sinks; 161 | } 162 | 163 | const drivers = { 164 | DOM: makeDOMDriver('#app') 165 | }; 166 | 167 | run(main, drivers); 168 | {% endhighlight %} 169 | 170 | This example portrays the most common problem-solving pattern in Cycle.js: formulate the computer's behavior as a function of streams: continuously listen to source messages from drivers and continuously provide sinks messages (in our case, Virtual DOM elements) to the drivers. Read the next chapter to get familiar with this pattern. 171 | 172 |

Quick Start

173 | 174 | In the future, you can quickly set up a development and production ready Cycle.js project using the [cyc](https://github.com/edge/cyc) boilerplate. 175 | 176 | It comes with babel transpilation, hot-reloading, and an isomorphic server. 177 | 178 |

Cycle.js as a script

179 | 180 | In the rare occasion you need Cycle.js scripts as standalone JavaScript files, you can download them from [unpkg](https://unpkg.com): 181 | 182 | - Latest Cycle.js [xstream run](https://unpkg.com/@cycle/xstream-run/dist/cycle.js) 183 | - Latest Cycle.js [most.js run](https://unpkg.com/@cycle/most-run/dist/cycle-most-run.js) 184 | - Latest Cycle.js [RxJS v5 run](https://unpkg.com/@cycle/rxjs-run/dist/cycle.js) 185 | - Latest Cycle.js [RxJS v4 run](https://unpkg.com/@cycle/rx-run/dist/cycle.js) 186 | - Latest Cycle.js [DOM](https://unpkg.com/@cycle/dom/dist/cycle-dom.js) 187 | - Latest Cycle.js [HTTP](https://unpkg.com/@cycle/http/dist/cycle-http-driver.js) 188 | - Latest Cycle.js [Isolate](https://unpkg.com/@cycle/isolate/dist/cycle-isolate.js) 189 | -------------------------------------------------------------------------------- /_sass/basic.sass: -------------------------------------------------------------------------------- 1 | *, *:before, *:after 2 | box-sizing: border-box 3 | 4 | body 5 | background: $white 6 | font-family: $body-font 7 | font-weight: 400 8 | font-size: 19px 9 | line-height: 1.6 10 | color: $black 11 | margin: 0 12 | padding: 0 $normal-space 13 | 14 | body > header h1 15 | font-weight: bold 16 | margin-top: .25em 17 | margin-bottom: 1em 18 | 19 | footer 20 | display: flex 21 | justify-content: space-between 22 | color: $gray-dark 23 | margin-top: $normal-space 24 | margin-bottom: $normal-space 25 | 26 | h1, h2, h3, h4, h5, h6 27 | font-weight: 300 28 | line-height: 1.5em 29 | font-family: $header-font 30 | header & 31 | text-transform: uppercase 32 | 33 | .wide-page-body, .page-body 34 | margin: 0 auto 35 | 36 | .wide-page-body 37 | max-width: $total-page-width 38 | 39 | .page-body 40 | max-width: $max-article-width 41 | padding: $large-space $small-space 42 | 43 | nav ul 44 | margin: 0 45 | padding-left: 0 46 | 47 | a 48 | +anchor_style($teal) 49 | 50 | strong, b, th 51 | font-weight: bold 52 | 53 | hr 54 | border: 1px solid $gray-lighter 55 | 56 | pre 57 | overflow: auto 58 | padding: $small-space 2px $small-space $small-space 59 | font-size: .8em 60 | 61 | code 62 | background-color: $almost-white 63 | color: $black 64 | padding: 2px 4px 65 | word-break: break-all 66 | 67 | a 68 | code 69 | +anchor_style($teal) 70 | 71 | iframe, img, embed, object, video 72 | max-width: 100% 73 | border-radius: 2px 74 | 75 | table 76 | border-collapse: collapse 77 | width: 100% 78 | th, td 79 | padding: .5em 1em 80 | border: 1px solid 81 | 82 | p > img, p > svg 83 | margin: $normal-space auto 84 | display: block 85 | width: 65% 86 | max-width: 30em 87 | min-width: 16em 88 | max-height: 15em 89 | 90 | .mobile-hidden 91 | display: inherit 92 | .mobile-shown 93 | display: none 94 | 95 | @media only screen and (max-width: 680px) 96 | body 97 | font-size: calc(.6vw + .8em) 98 | padding: 0 99 | .wide-page-body 100 | padding: 0 $small-space 101 | .mobile-hidden 102 | display: none 103 | .mobile-shown 104 | display: inherit 105 | -------------------------------------------------------------------------------- /_sass/colors.sass: -------------------------------------------------------------------------------- 1 | 2 | $white: #ffffff 3 | $almost-white: #f5f5f5 4 | $gray-lighter: #e5e5e5 5 | $gray-light: #a6a7b3 6 | $gray: #727481 7 | $gray-dark: #383C4D 8 | $black: #24242D 9 | 10 | $teal-light: #58D3D8 11 | $teal: #409B9E 12 | 13 | $green-light: #8FE8B4 14 | $green: #67AB83 15 | 16 | $lime-light: #C6FC93 17 | 18 | $yellow-light: #F3FC93 19 | 20 | $red-light: #FF9D9B 21 | $red: #DA5754 22 | -------------------------------------------------------------------------------- /_sass/dimens.sass: -------------------------------------------------------------------------------- 1 | 2 | $padding: 33% 3 | $small-space: 1rem 4 | $normal-space: 2.5rem 5 | $large-space: 4rem 6 | $side-menu-width: 220px 7 | $total-page-width: 880px 8 | $max-article-width: 720px 9 | -------------------------------------------------------------------------------- /_sass/documentation.sass: -------------------------------------------------------------------------------- 1 | img.cycle-home-logo 2 | width: 5em 3 | display: block 4 | margin-bottom: $small-space 5 | 6 | .cycle-home-link 7 | font-family: $header-font 8 | font-weight: bold 9 | 10 | .side-menu 11 | position: fixed 12 | top: $normal-space 13 | height: 100% 14 | overflow: hidden 15 | font-size: 3vh 16 | a 17 | +anchor_style($black) 18 | nav ul 19 | white-space: nowrap 20 | .github-link 21 | border-bottom: none 22 | &:hover 23 | border-bottom: none 24 | background-color: $almost-white 25 | border-radius: 5px 26 | 27 | .paginator 28 | a 29 | +box_link() 30 | 31 | article 32 | min-height: 500px 33 | margin-top: $large-space 34 | max-width: $max-article-width 35 | margin-left: calc(#{$side-menu-width} + #{$normal-space}) 36 | overflow-x: visible 37 | padding-top: 1px 38 | blockquote 39 | margin: 0 40 | padding: 1px 1em 41 | color: $gray-dark 42 | background-color: $almost-white 43 | h1, h2, h3, h4, h5, h6 44 | text-align: left 45 | th 46 | border-bottom-width: 2px 47 | th, td 48 | border-color: $gray-lighter 49 | header 50 | text-align: center 51 | h2 52 | display: inline-block 53 | .edit-post 54 | @extend .mobile-hidden 55 | font-size: 80% 56 | top: $normal-space 57 | right: $normal-space 58 | position: absolute 59 | h4 60 | text-align: center 61 | > div 62 | margin-bottom: 2em 63 | .paginator 64 | display: flex 65 | justify-content: space-between 66 | 67 | @media only screen and (max-width: 680px) 68 | .side-menu 69 | position: relative 70 | width: inherit 71 | height: inherit 72 | font-size: 150% 73 | padding-bottom: $normal-space 74 | border-bottom: 2px solid $gray-lighter 75 | margin-bottom: $normal-space 76 | ul > li 77 | padding-top: 0.5em 78 | article 79 | margin-left: 0 80 | 81 | @media only screen and (min-height: 800px) 82 | .side-menu 83 | font-size: 24px 84 | -------------------------------------------------------------------------------- /_sass/homepage.sass: -------------------------------------------------------------------------------- 1 | .hero 2 | background-color: $black 3 | color: $white 4 | margin: 0 calc(0px - #{$normal-space}) 5 | position: relative 6 | .hero-title 7 | img.hero-logo 8 | width: 7em 9 | vertical-align: bottom 10 | h1 11 | display: inline-block 12 | margin: 0 0 0 0.5em 13 | font-weight: bold 14 | font-size: 4em 15 | h2 16 | margin-top: $normal-space 17 | h3 18 | margin-top: $large-space 19 | margin-bottom: $small-space 20 | text-align: center 21 | display: block 22 | a 23 | +box_link($lime-light, $black, $lime-light) 24 | letter-spacing: 0 25 | display: inline-block 26 | h4 27 | margin-top: $small-space 28 | margin-bottom: $large-space 29 | text-align: center 30 | display: block 31 | a 32 | +box_link($teal-light, $black, $teal-light) 33 | letter-spacing: 0 34 | display: inline-block 35 | 36 | .hero-gh-star 37 | position: absolute 38 | top: $large-space 39 | right: $large-space 40 | 41 | .homepage-bottom-cta 42 | margin: $normal-space 0 43 | 44 | .features 45 | text-align: justify 46 | ul 47 | width: 50% 48 | margin: 0 auto 49 | h1, h2, h3 50 | text-align: center 51 | margin-top: $normal-space 52 | a 53 | display: inline-block 54 | +box_link() 55 | letter-spacing: 0 56 | .first-feature 57 | margin-top: 0 58 | 59 | 60 | .homepage-features 61 | margin-top: $normal-space 62 | display: flex 63 | h2 64 | flex-basis: 2em 65 | flex-grow: 1 66 | p 67 | flex-basis: 2em 68 | flex-grow: 5 69 | text-align: justify 70 | 71 | .homepage-features:nth-child(odd) 72 | h2 73 | order: 1 74 | margin-right: $normal-space 75 | p 76 | order: 2 77 | 78 | .homepage-features:nth-child(even) 79 | h2 80 | order: 2 81 | margin-left: $normal-space 82 | p 83 | order: 1 84 | 85 | .explicit-dataflow 86 | margin-top: $normal-space 87 | display: flex 88 | justify-content: center 89 | align-items: center 90 | .dataflow-minimap svg 91 | width: inherit 92 | height: 18em 93 | margin: 0 auto 94 | min-width: inherit 95 | max-width: inherit 96 | max-height: inherit 97 | .dataflow-minimap-code 98 | font-size: 0.8em 99 | 100 | .example-hello-world-container 101 | box-sizing: border-box 102 | background-color: #EEE 103 | padding: 8px 12px 104 | label 105 | font-size: 1.3em 106 | input 107 | font-size: 1.2em 108 | margin-left: 0.5em 109 | hr 110 | border-color: #d3d3d3 111 | h1 112 | text-align: left 113 | margin: 20px 0 10px 114 | font-size: 2em 115 | 116 | img.egghead-logo 117 | width: 200px 118 | height: 200px 119 | max-width: 200px 120 | min-width: 200px 121 | max-height: 200px 122 | min-height: 200px 123 | 124 | #opencollective-banner 125 | h1 126 | font-size: 1.5em 127 | h2 128 | font-size: 1em 129 | 130 | @media only screen and (max-width: 680px) 131 | .hero 132 | margin: 0 133 | .hero-gh-star 134 | right: $small-space 135 | top: $small-space 136 | .hero-title 137 | img.hero-logo 138 | width: 3em 139 | h1 140 | font-size: 2em 141 | top: 0.75em 142 | h2 143 | font-size: 1.2em 144 | .features 145 | ul 146 | width: 100% 147 | .homepage-features 148 | display: block 149 | .homepage-features:nth-child(odd) 150 | h2 151 | margin-right: 0 152 | .homepage-features:nth-child(even) 153 | h2 154 | margin-left: 0 155 | .explicit-dataflow 156 | .dataflow-minimap svg 157 | height: 44vw // 11.5em 158 | .dataflow-minimap-code 159 | font-size: 2vw // 0.55em 160 | -------------------------------------------------------------------------------- /_sass/mixins.sass: -------------------------------------------------------------------------------- 1 | =anchor_style($anchor-color) 2 | color: $anchor-color 3 | text-decoration: inherit 4 | border-bottom: 1px dotted $anchor-color 5 | &:hover 6 | border-bottom: 1px solid $anchor-color 7 | 8 | =box_link($base-color: $teal, $hover-fg: $white, $hover-bg: $green-light) 9 | font-family: $body-font 10 | font-weight: 400 11 | color: $base-color 12 | border: 2px solid $base-color 13 | border-radius: 5px 14 | padding: 5px 15px 15 | transition: background-color .2s ease-out, color .2s ease-out 16 | &:hover 17 | border: 2px solid $hover-bg 18 | background-color: $hover-bg 19 | color: $hover-fg 20 | -------------------------------------------------------------------------------- /_sass/typography.sass: -------------------------------------------------------------------------------- 1 | 2 | $body-font: "Source Sans Pro", "Calibri", "Helvetica", sans-serif 3 | $header-font: "Merriweather", "Lora", "Georgia", serif -------------------------------------------------------------------------------- /_sass/vendor.sass: -------------------------------------------------------------------------------- 1 | .highlight 2 | pre 3 | code 4 | background: transparent 5 | color: white 6 | padding: 0 7 | color: white 8 | margin: 0 9 | background-color: #333 10 | .c 11 | color: #75715e 12 | .err 13 | color: #FF0000 14 | .k 15 | color: #66d9ef 16 | .l 17 | color: #ae81ff 18 | .n 19 | color: #f8f8f2 20 | .o 21 | color: #f92672 22 | .p 23 | color: #f8f8f2 24 | .cm, .cp, .c1, .cs 25 | color: #75715e 26 | .ge 27 | font-style: italic 28 | .gs 29 | font-weight: bold 30 | .kc, .kd 31 | color: #66d9ef 32 | .kn 33 | color: #f92672 34 | .kp, .kr, .kt 35 | color: #66d9ef 36 | .ld 37 | color: #e6db74 38 | .m 39 | color: #ae81ff 40 | .s 41 | color: #e6db74 42 | .na 43 | color: #a6e22e 44 | .nb 45 | color: #f8f8f2 46 | .nc 47 | color: #a6e22e 48 | .no 49 | color: #66d9ef 50 | .nd 51 | color: #a6e22e 52 | .ni 53 | color: #f8f8f2 54 | .ne, .nf 55 | color: #a6e22e 56 | .nl, .nn 57 | color: #f8f8f2 58 | .nx 59 | color: #a6e22e 60 | .py 61 | color: #f8f8f2 62 | .nt 63 | color: #f92672 64 | .nv 65 | color: #f8f8f2 66 | .ow 67 | color: #f92672 68 | .w 69 | color: #f8f8f2 70 | .mf, .mh, .mi, .mo 71 | color: #ae81ff 72 | .sb, .sc, .sd, .s2 73 | color: #e6db74 74 | .se 75 | color: #ae81ff 76 | .sh, .si, .sx, .sr, .s1, .ss 77 | color: #e6db74 78 | .bp, .vc, .vg, .vi 79 | color: #f8f8f2 80 | .il 81 | color: #ae81ff 82 | .gu 83 | color: #75715e 84 | .gd 85 | color: #f92672 86 | .gi 87 | color: #a6e22e 88 | 89 | .fa 90 | margin: 0 0.25em 91 | 92 | html 93 | -webkit-font-smoothing: antialiased 94 | -moz-osx-font-smoothing: grayscale 95 | 96 | body > footer 97 | display: -webkit-flex 98 | -webkit-justify-content: space-between 99 | 100 | \::-webkit-scrollbar 101 | width: 6px 102 | 103 | \::-webkit-scrollbar-track 104 | background-color: transparent 105 | 106 | \::-webkit-scrollbar-thumb 107 | border-radius: 3px 108 | box-shadow: inset 0 0 1px rgba(0, 0, 0, 0.3) 109 | background-color: #555 110 | -------------------------------------------------------------------------------- /android-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyclejs/old-website/bb2edce2634f379cd4b550672dbb38b4d1e84165/android-icon-144x144.png -------------------------------------------------------------------------------- /android-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyclejs/old-website/bb2edce2634f379cd4b550672dbb38b4d1e84165/android-icon-192x192.png -------------------------------------------------------------------------------- /android-icon-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyclejs/old-website/bb2edce2634f379cd4b550672dbb38b4d1e84165/android-icon-36x36.png -------------------------------------------------------------------------------- /android-icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyclejs/old-website/bb2edce2634f379cd4b550672dbb38b4d1e84165/android-icon-48x48.png -------------------------------------------------------------------------------- /android-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyclejs/old-website/bb2edce2634f379cd4b550672dbb38b4d1e84165/android-icon-72x72.png -------------------------------------------------------------------------------- /android-icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyclejs/old-website/bb2edce2634f379cd4b550672dbb38b4d1e84165/android-icon-96x96.png -------------------------------------------------------------------------------- /apple-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyclejs/old-website/bb2edce2634f379cd4b550672dbb38b4d1e84165/apple-icon-114x114.png -------------------------------------------------------------------------------- /apple-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyclejs/old-website/bb2edce2634f379cd4b550672dbb38b4d1e84165/apple-icon-120x120.png -------------------------------------------------------------------------------- /apple-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyclejs/old-website/bb2edce2634f379cd4b550672dbb38b4d1e84165/apple-icon-144x144.png -------------------------------------------------------------------------------- /apple-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyclejs/old-website/bb2edce2634f379cd4b550672dbb38b4d1e84165/apple-icon-152x152.png -------------------------------------------------------------------------------- /apple-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyclejs/old-website/bb2edce2634f379cd4b550672dbb38b4d1e84165/apple-icon-180x180.png -------------------------------------------------------------------------------- /apple-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyclejs/old-website/bb2edce2634f379cd4b550672dbb38b4d1e84165/apple-icon-57x57.png -------------------------------------------------------------------------------- /apple-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyclejs/old-website/bb2edce2634f379cd4b550672dbb38b4d1e84165/apple-icon-60x60.png -------------------------------------------------------------------------------- /apple-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyclejs/old-website/bb2edce2634f379cd4b550672dbb38b4d1e84165/apple-icon-72x72.png -------------------------------------------------------------------------------- /apple-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyclejs/old-website/bb2edce2634f379cd4b550672dbb38b4d1e84165/apple-icon-76x76.png -------------------------------------------------------------------------------- /apple-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyclejs/old-website/bb2edce2634f379cd4b550672dbb38b4d1e84165/apple-icon-precomposed.png -------------------------------------------------------------------------------- /apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyclejs/old-website/bb2edce2634f379cd4b550672dbb38b4d1e84165/apple-icon.png -------------------------------------------------------------------------------- /archive.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Archive" 3 | --- 4 |
    5 | {% for post in site.posts %} 6 | 7 | {% unless post.next %} 8 |

    {{ post.date | date: '%Y' }}

    9 | {% else %} 10 | {% capture year %}{{ post.date | date: '%Y' }}{% endcapture %} 11 | {% capture nyear %}{{ post.next.date | date: '%Y' }}{% endcapture %} 12 | {% if year != nyear %} 13 |

    {{ post.date | date: '%Y' }}

    14 | {% endif %} 15 | {% endunless %} 16 | 17 |
  • {{ post.date | date:"%B" }}: {{ post.title }}
  • 18 | {% endfor %} 19 |
20 | -------------------------------------------------------------------------------- /browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | #ffffff -------------------------------------------------------------------------------- /favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyclejs/old-website/bb2edce2634f379cd4b550672dbb38b4d1e84165/favicon-16x16.png -------------------------------------------------------------------------------- /favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyclejs/old-website/bb2edce2634f379cd4b550672dbb38b4d1e84165/favicon-32x32.png -------------------------------------------------------------------------------- /favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyclejs/old-website/bb2edce2634f379cd4b550672dbb38b4d1e84165/favicon-96x96.png -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyclejs/old-website/bb2edce2634f379cd4b550672dbb38b4d1e84165/favicon.ico -------------------------------------------------------------------------------- /img/cyclejs_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /img/egghead.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyclejs/old-website/bb2edce2634f379cd4b550672dbb38b4d1e84165/img/egghead.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: blank 3 | --- 4 |
5 |
6 |
7 | 8 |

Cycle.js

9 |
10 |

A functional and reactive JavaScript framework for predictable code

11 |

Read the docs

12 |

Or try it live!

13 | 14 |
15 |
16 |
17 |
18 | {% capture homepage %}{% include homepage-content.md %}{% endcapture %} 19 | {{ homepage | markdownify }} 20 |

Read the docs

21 |
22 | 23 | {% include footer.html %} 24 |
25 | 26 | -------------------------------------------------------------------------------- /main.sass: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | @import colors 5 | @import dimens 6 | @import typography 7 | @import mixins 8 | @import basic 9 | @import homepage 10 | @import documentation 11 | @import vendor 12 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "App", 3 | "icons": [ 4 | { 5 | "src": "\/android-icon-36x36.png", 6 | "sizes": "36x36", 7 | "type": "image\/png", 8 | "density": "0.75" 9 | }, 10 | { 11 | "src": "\/android-icon-48x48.png", 12 | "sizes": "48x48", 13 | "type": "image\/png", 14 | "density": "1.0" 15 | }, 16 | { 17 | "src": "\/android-icon-72x72.png", 18 | "sizes": "72x72", 19 | "type": "image\/png", 20 | "density": "1.5" 21 | }, 22 | { 23 | "src": "\/android-icon-96x96.png", 24 | "sizes": "96x96", 25 | "type": "image\/png", 26 | "density": "2.0" 27 | }, 28 | { 29 | "src": "\/android-icon-144x144.png", 30 | "sizes": "144x144", 31 | "type": "image\/png", 32 | "density": "3.0" 33 | }, 34 | { 35 | "src": "\/android-icon-192x192.png", 36 | "sizes": "192x192", 37 | "type": "image\/png", 38 | "density": "4.0" 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /ms-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyclejs/old-website/bb2edce2634f379cd4b550672dbb38b4d1e84165/ms-icon-144x144.png -------------------------------------------------------------------------------- /ms-icon-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyclejs/old-website/bb2edce2634f379cd4b550672dbb38b4d1e84165/ms-icon-150x150.png -------------------------------------------------------------------------------- /ms-icon-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyclejs/old-website/bb2edce2634f379cd4b550672dbb38b4d1e84165/ms-icon-310x310.png -------------------------------------------------------------------------------- /ms-icon-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyclejs/old-website/bb2edce2634f379cd4b550672dbb38b4d1e84165/ms-icon-70x70.png --------------------------------------------------------------------------------