Gleb Bahmutov PRO
JavaScript ninja, image processing expert, software quality fanatic
CycleConf2017
Angular1, HyperApp,
Vue, Cycle, Node
Technology that brings transparency to complex systems
Harvard Sq, WTC NYC
Me: Have you heard about Cycle.js?
They: No
Me: It is not about popularity
They: Ok, what is it about?
Me: The very first Cycle.js example has reactive streams.
Me: other frameworks have it as advanced add-on
Me: like Angular2 "async"
@Component({
selector: 'async-promise-pipe',
template: `<div>
<code>promise|async</code>:
<button (click)="clicked()">
{{ arrived ? 'Reset' : 'Resolve' }}
</button>
<span>Wait for it... {{ greeting | async }}</span>
</div>`
})
export class AsyncPromisePipeComponent {...}
They: What's so great about streams?
Me: It makes the code simpler
// child component
Vue.component('person', {
props: ['first']
})
// parent template
<div>
<person v-bind:first="manager.ownName"></person>
</div>
// parent "manager.ownName"
// --- undefined ---- Joe --->
// child "first"
// --- undefined ---- Joe --->
Property binding in Vue
// child component
Vue.component('person', {
props: ['first'],
data: function () {
return { name: this.first }
}
})
// parent template
<div>
<person v-bind:first="manager.ownName"></person>
</div>
// parent "manager.ownName" --- undefined ---- Joe --->
// child "first" --- undefined ---- Joe --->
// child "name" --- undefined --->
Want to modify prop in component?
hmm
// child component
Vue.component('person', {
props: ['first'],
computed: {
name: function () {
return this.first || 'Unknown'
}
}
})
// parent template
<div>
<person v-bind:first="manager.ownName"></person>
</div>
// parent "manager.ownName" --- undefined ---- Joe --->
// child "first" --- undefined ---- Joe --->
// child "name" --- Unknown ------ Joe --->
One-way property binding with validation
Vue.js:
v-bind
v-once
prop
data
computed
parent
component
But that's not all ...
<!-- this passes down a plain string "1" -->
<comp some-prop="1"></comp>
<!-- this passes down an actual number -->
<comp v-bind:some-prop="1"></comp>
// Angular 1 props
app.directive('directiveName', function(){
return {
scope: {
name: '@' // or '=' or '&'
}
}
})
// parent
const Rx = require('rxjs/Rx')
const parent = {
manager: {
ownName: Rx.Observable.of(undefined, 'Joe', 'Mary')
}
}
// child
function person(parent) {
const {manager: {ownName}} = parent
ownName
.first(s => s) // <= right here!
.subscribe((name) => {
console.log('person got new name', name)
})
}
Passing streams
const child = person(parent)
// manager.name --- undefined --- Joe --- Mary ---|>
// child ----------------- Joe -|>
Passing streams
Common time or event logic can be handled easier with streams
first value
distinct valid value
Me: passing component properties in Cycle means passing streams!
function Component({props}) {
// ...
return sinks;
}
const c = Component({
props: () => xs.of({
label: 'Weight', unit: 'kg',
min: 40, value: 70, max: 140
})
})
Me: component returns a stream to communicate with its parent
import {prop} from 'ramda'
function Component({props}) {
// ...
const sinks = {
DOM: vdom$,
value: props$.map(prop('value'))
}
return sinks
}
props$
--- 10 --- 20 --- 20 --->
values$
<--- 10 --- 20 --- 20 ---
props$.map(I)
Me: component returns a stream to communicate with its parent
They: All that looks so weird
props$
--- 10 --- 20 --- 20 --->
values$
<--- 10 --- 20 --- 20 ---
Me: it gets weird when component has its own "UI cycle"
DOM$
--- 10 --- 20 --- 20 --->
<--- 10 --- 20 --- 20 ---
Note: this time diagram is inconsistent ๐
props$
--- 10 --- 20 --- 20 --->
values$
Aside: what should we test for this system?
DOM$
<--- 10 --- 20 --- 20 ---
props$
--- 10 --- 20 --- 20 --->
values$
DOM$
initial value only
Aside: what should we test for this system?
<--- 10 --- 20 --- 20 ---
props$
--- 10 --- 20 --- 20 --->
values$
DOM$
all input/output values
Aside: what should we test for this system?
<--- 10 --- 20 --- 20 ---
props$
--- 10 --- 20 --- 20 --->
values$
DOM$
input/output + DOM$
Aside: what should we test for this system?
<--- 10 --- 20 --- 20 ---
props$
--- 10 --- 20 --- 20 --->
values$
DOM$
input/output + UI
Aside: what should we test for this system?
<--- 10 --- 20 --- 20 ---
props$
--- {value:50, label:'kg'} --->
values$
<--- 50 ---
Merge props$ with slide value
newValue$
<--- 50 ---
kb
๐ค
props$
values$
<--- 70 ---
User moves the slider
newValue$
<--- 70 ---
70
kb
๐
props$
--- {value:100, label:'lb'} --->
values$
<--- 155 ---
New props are passed from parent
newValue$
155
lb
<--- 155 ---
๐
ignored
function Component({props}) {
// combine latest slider value newValue$
// with properties passed in props
const state$ = props
.map(props => newValue$
.map(val => ({
label: props.label,
unit: props.unit,
min: props.min,
value: val,
max: props.max
}))
.startWith(props)
)
.flatten()
.remember()
// ...
return sinks
}
They: You said "simple"!
Ramda is your friend
var stream =
.filter(R.prop('foo'))
.map(R.lens)
...
Lots of ways to avoid writing your own "small" pure functions
var stream = xs.periodic(1000)
.filter(i => i % 2 === 0)
.map(i => i * i)
.endWhen(xs.periodic(5000).take(1))
import {filter, map, pipe} from 'ramda'
const isEven = i => i % 2 === 0
const square = i => i * i
const evenSquares = pipe(
filter(isEven),
map(square)
)
const stream = evenSquares(xs.periodic(1000))
.endWhen(xs.periodic(5000).take(1))
They: Ok, can I try it myself?
They: Ok, can I try it myself?
They: Ok, can I try it myself?
Me: use create-cycle-app
npm install --global create-cycle-app
create-cycle-app example --flavor cycle-scripts-one-fits-all
cd example
npm start &
open localhost:8080
TypeScript
import xs, { Stream } from 'xstream';
import { VNode } from '@cycle/dom';
import { Sources, Sinks } from './interfaces';
export function App(sources : Sources) : Sinks
{
const vdom$ : Stream<VNode> = xs.of(
<div>My Awesome Cycle.js app</div>
);
return {
DOM: vdom$
};
}
ย I don't use type system like Typescript - it does not let me hack things together!
They: Can you just give me Cycle.js CDN script?
Cycle.js
They: Can you just give me Angular CDN script?
<script src="cdn/angular.js"></script>
<div ng-app="myApp">
<ul ng-controller="Todo">
<li ng-repeat="todo in todoList">
{{todo.label}}
</li>
</ul>
</div>
<script>
angular.module('myApp', [])
.controller('Todo', ...)
// BOOM App is working!
</script>
<script src="https://unpkg.com/@cycle/run@3.1.0/dist/cycle-run.js"></script>
<script src="https://unpkg.com/@cycle/dom@16.0.0/dist/cycle-dom.js"></script>
<script>
const {run} = Cycle
const {div, label, input, hr, h1, makeDOMDriver} = CycleDOM
// rest of the code
</script>
Uncaught TypeError: Cannot read property 'default' of undefined
at makeSinkProxies (cycle-run.js:33)
sinkProxies[name_1] = xstream_1.default.createWithMemory();
<script src="https://unpkg.com/xstream@10.3.0/dist/xstream.js"></script>
<script src="https://unpkg.com/@cycle/run@3.1.0/dist/cycle-run.js"></script>
<script src="https://unpkg.com/@cycle/dom@16.0.0/dist/cycle-dom.js"></script>
Me: fine, I will run webpack for you
<script src="https://web-packing.com/@cycle/run&@cycle/dom">
const {run} = packs.run
const {div, label, input, hr, makeDOMDriver} = packs.dom
Rollup/Browserify as a service
@lucamezzalira
@rich_harris
AWS Lambda to run webpack
Immutable webpack bundles on demand
They: Can Cycle work with my web app?
Me: I will write a driver for you!
Cycle.js
Rule of ๐: Can I drive your app from the DevTools?
// Angular 1
$(el).scope().foo = 'bar'
$(el).scope().$apply()
// Vue
vue = new Vue(...)
vue.$data.foo = 'bar'
// Cycle?
Is your Cycle App like this bottled garden?
Real systems have brass ball valves
Real systems have PVC plugs
Real systems allow easier access into their workings
// counter example + "PVC Plug"
const devToolsProducer = {
start: listener => {
window.cyclePlug = listener.next.bind(listener)
},
stop: () => {
delete window.cyclePlug
}
}
const devTools$ = xs.create(devToolsProducer)
const action$ = xs.merge(
sources.DOM.select('.dec').events('click').mapTo(-1),
sources.DOM.select('.inc').events('click').mapTo(+1),
devTools$
)
Plugs work great for exploratory testing
Just keep track of all the plugs that you have installed ๐
Placing plugs where it matters
const action$ = xs.merge(
sources.DOM.select('.dec').events('click').mapTo(-1),
sources.DOM.select('.inc').events('click').mapTo(+1),
devTools$
).debug('action')
action: 1
action: 1
action: -1
// with https://github.com/pimterry/loglevel
// log.setLevel('debug', true)
const action$ = xs.merge(
sources.DOM.select('.dec').events('click').mapTo(-1),
sources.DOM.select('.inc').events('click').mapTo(+1),
devTools$
).debug(log.debug)
๐ Requires page reload ๐
const action$ = xs.merge(
sources.DOM.select('.dec').events('click').mapTo(-1),
sources.DOM.select('.inc').events('click').mapTo(+1),
devTools$
)
window.action$ = action$
action$.setDebugListener({next:console.log})
emit events
Emitter
specific type of events
test('system', async t => {
subscribe()
await startSystem()
snapshot(events)
})
system under test
test('complex object', t => {
const obj = ...
t.snapshot(obj)
})
Time.assertEqual(
Time.diagram('---1---3---2--|'),
expected,
myCustomCompare
);
Time.run(err => console.error(err));
You can optionally pass a custom comparator function. This is useful if you want to do things like testing your DOM with tools such as html-looks-like.
-- A -- A ---------------->
-----B ----- B ----------->
-----C ----- C ---- C ---->
- D ---------- D -- D ---->
-- E ------- E ----------->
Not every test requires opening up the pipe
const count$ = action$
// delay events by 2 seconds
.map(i => xs.fromPromise(by2seconds(i)))
.flatten()
.fold((x, y) => x + y, 0);
Usual E2E runners need delays or run slow in this case ... (Karma, Protractor, Selenium)
beforeEach(() => {
cy.visit('index.html')
})
it('starts with 0', () => {
cy.contains('Counter: 0')
})
it('increments counter 3 times', () => {
cy.contains('Increment').click()
cy.contains('Counter: 1')
cy.contains('Increment').click()
cy.contains('Counter: 2')
cy.contains('Increment').click()
cy.contains('Counter: 3')
})
Not ย single "delay" yet it runs in 6 seconds!
๏ผ
๏ผ
I'm just using my app, and every error is recorded
Me: If there is a crash - something is wrong
function foo () {
throw new Error('Oops')
}
foo()
// Catch the exception with window.onerror
// event listener ๐
Sometimes even in promises
window.addEventListener('unhandledrejection', e => {
e.preventDefault();
console.log('Reason: ' + e.reason);
});
new Promise((resolve, reject) => {
reject(new Error('oops'))
})
// Catch the unhandled promise rejection ๐
Observables ...
var xs = xstream.default
xs.periodic(1000)
.map(x => {
if (x === 5) {
throw new Error('Something went wrong')
}
return x
})
.addListener({
next: i => console.log(i),
complete: () => console.log('completed'),
})
// streams just stops on error ๐ฉ
Other frameworks
// Angular 1
var myApp = angular.module('myApp', ['ngRaven'])
// Vue 2
import Vue from 'vue'
import Raven from 'raven-js'
Raven.addPlugin(RavenVue, Vue)
Report individual stream errors
Raven.config('<url>').install()
var xs = xstream.default
xs.periodic(1000)
.map(x => {
if (x === 5) {
throw new Error('Something went wrong')
}
return x
})
.addListener({
next: i => console.log(i),
error: Raven.captureException,
complete: () => console.log('completed'),
})
Use debug listener (xstream)
const count$ = action$.fold((x, y) => {
if (y === 20) {
throw new Error('Cannot add 20')
}
return x + y
}, 0);
vdom$.setDebugListener({
error: Raven.captureException
})
return {
DOM: vdom$
};
Most of my programming is transforming lists of items
const input = [1, 2, 3, 4]
const output = [2, 3, 4, 5]
// what is f(x)?
const f = x => x + 1
input.map(f) === output
const input = [1, 2, 3, 4]
const output = [2, 3, 4, 5]
const {?} = require('ramda')
const input = [1, 2, 3, 4]
const output = [2, 3, 4, 5]
const {solve} = require('rambo')
const S = solve(input, ouput)
console.log(S.name)
// "R.add(1)"
S.f(input) // [2, 3, 4, 5]
// observable system behavior
// --- 1 --- 2 --- 3 --- |->
// --- 2 --- 3 --- 4 --- |->
// what is the system?
// observable system behavior
// --- 1 --- 2 --- 3 --- |->
// --- 2 --------- 4 --- |->
// what is the system?
// observable system behavior
// --- 1 --- 2 --- |->
// ------ 2 --- 3 --- |->
// what is the system?
Give me the stream setup to do this!
By Gleb Bahmutov
What makes a framework great? How do you have others give it a try? How do you make sure the web application runs correctly? What is a brass ball valve? Video at https://vimeo.com/album/4578937/video/216829554
JavaScript ninja, image processing expert, software quality fanatic