angularremoteconf2015
Me:
Rob Wormald
Technical Architect @ Universal Mind
Author of angularSails
Plnkr Addict
irc: robdubya
@robwormald
github.com/robwormald
"Angular is a data-driven framework"
literally everybody
"$scope events in Angular are terrible!"
almost everybody
"Angular2 is a reactive framework"
Victor Savkin
Events versus Data
Data is easy to work with.
We have all kinds of neat tools for working with data
var numbers = [1, 2, 3, 4, 5];
function double(number) {
return number * 2;
}
var doubledNumbers = numbers.map(double);
//[2, 4, 6, 8, 10]
.map
var numbers = [1, 2, 3, 4, 5];
function greaterThanThree(value) {
return value > 3;
}
var numbersGreaterThanThree =
numbers.filter(greaterThanThree);
//[4, 5]
.filter
var numbers = [1, 2, 3, 4, 5];
function sum(total, value) {
return total + value;
}
var total = numbers.reduce(sum, 0);
//15
.reduce
ES6 Iterables/Iterators
The iterable/iterator protocol lets us describe anything as collection
Something is iterable if we can traverse its values -
we can "pull" the next value.
We use an iterator to access those values.
var numbers = [1, 2, 3];
//.values() returns an iterator
var numIterator = numbers.values();
numIterator.next() //{value: 1, done: false }
numIterator.next() //{value: 2, done: false }
numIterator.next() //{value: 3, done: false }
numIterator.next() //{done: true}
Array as Iterables
var myMap = new Map();
myMap.set('a', 1);
myMap.set('b', 2);
myMap.set('c', 3);
//.values() returns an iterator
var valueIterator = myMap.values();
valueIterator.next() //{value: 1, done: false }
valueIterator.next() //{value: 2, done: false }
valueIterator.next() //{value: 3, done: false }
valueIterator.next() //{done: true}
//.keys() returns an iterator
var keyIterator = myMap.keys();
keyIterator.next() //{value: 'a', done: false }
keyIterator.next() //{value: 'b', done: false }
keyIterator.next() //{value: 'c', done: false }
keyIterator.next() //{done: true}
//.entries() returns an iterator
var entryIterator = myMap.entries();
entryIterator.next() //{value: ['a', 1], done: false }
entryIterator.next() //{value: ['b', 2], done: false }
entryIterator.next() //{value: ['c', 3], done: false }
entryIterator.next() //{done: true}
Map as Iterables
function* lazyNumbers(){
yield 1;
yield 2;
yield 3;
}
//generator functions return an iterator
var valueIterator = lazyNumbers();
valueIterator.next() //{value: 1, done: false }
valueIterator.next() //{value: 2, done: false }
valueIterator.next() //{value: 3, done: false }
valueIterator.next() //{done: true}
Generators
class MyCustomList {
constructor(){
this._values = [];
}
[Symbol.iterator](){
var nextIndex = 0;
return {
next: function(){
return nextIndex < this._values.length ?
{value: this._values[nextIndex++], done: false} :
{done: true};
}
}
}
append(val){
this._values.push(val)
}
}
var myList = new MyCustomList();
myList.append(1);
myList.append(2);
myList.append(3);
//.values() returns an iterator
var valueIterator = lazyNumbers();
valueIterator.next() //{value: 1, done: false }
valueIterator.next() //{value: 2, done: false }
valueIterator.next() //{value: 3, done: false }
valueIterator.next() //{done: true}
Custom Data Structures!
This is all awesome.
But...
This only works with synchronous things. If you want to pull a value from a collection, it has to be available *now*
Async
If we think about synchronous as "pull"...
We think about asynchronous as "push"
In Javascript, async is driven by events.
The event loop pushes values to us.
var myButton = document.getElementById('myButton');
function doSomethingOnClick(event){
doSomething(event);
//make sure you remember to tidy up after yourself!
myButton.removeEventListener('click', doSomethingOnClick);
}
myButton.addEventListener('click', doSomethingOnClick);
DOM Events
Callbacks are events!
getStuff(function(result){
getMoreStuff(function(results){
getSomeStuffForEachResult(results, function(moreStuff){
// note my code running off the page...^
doTheThingWeWantedToDo(moreStuff);
});
});
});
(ugh)
function getStuff(url, callback){
var req = new XMLHttpRequest();
function reqListener () {
callback(JSON.parse(req.responseText));
}
req.addEventListener("load", reqListener);
req.open("GET", url);
req.send();
}
getStuff('foos.json', function(foos){
getStuff('bars.json', function(bars){
//do stuff with foos and bars
});
});
XMLHttpRequest
Uh, Promises? Hello?
fetch('foos.json')
.then(function(res){
return res.json();
})
.then(function(foos){
return fetch('bars.json')
.then(function(res){
return res.json();
})
.then(function(bars){
return [foos, bars];
})
})
.then(function(results){
//do stuff with foos and bars
});
fetch
Promises are awesome.
but...
Promises don't solve everything.
function watchLocation(callback){
return new Promise(function(resolve, reject){
var watchID = navigator.geolocation.watchPosition(function(position) {
resolve([position.coords.latitude, position.coords.longitude]);
});
})
//how do i stop watching?!
}
watchLocation().then(function(location){
///only works once!
})
Geolocation
Here's a dirty little secret:
You can't cancel a promise.
Promises are great at single values.
Promises aren't so great at multiple values.
What if we could unify all our event-y / push type APIs, the same way iterables and iterators unify our pull APIs?
Better yet, what if we could unify all our event-y APIs...
...deal with multiple values easily...
...clean up after ourselves... automagically
cancel things we no longer care about...
AND...
remember our awesome data tools? map/filter/reduce/etc?
what if we could use those to work with our events?
How do we do that?
I'm glad you asked.
"Reactive programming is programming with asynchronous data streams."
So what's a stream?
A stream is simply a collection that arrives over time.
Collection
Stream
[ 1, 2, 3]
1
2
3
Observables
Observables are like collections...
Except they arrive over time - asynchronously.
Observables are like Promises...
Except they work with multiple values.
They clean up after themselves.
They can be cancelled.
Oh, and they let you map, filter, reduce (and more!)
Observable API
//Observable constructor
let myObservable = new Observable(observer => {
//the observer lets us *push* values
observer.next(1);
observer.next(2);
observer.next(3);
//it lets us propagate errors
observer.error('oops');
//and lets us (optionally) complete the stream
observer.complete();
});
Observable API
//Observable constructor
let myObservable = new Observable(observer => {
//the observer lets us *push* values
observer.next(1);
observer.next(2);
observer.next(3);
//it lets us propagate errors
observer.error('oops');
//and lets us (optionally) complete the stream
observer.complete();
});
Consuming Observables
//Observable constructor
let myObservable = new Observable(observer => {
observer.next(1);
observer.next(2);
observer.next(3);
observer.complete();
});
myObservable.subscribe(
val => console.log(val),
err => console.log(err),
_ => console.log('done')
);
Duality
function* values(){
yield 1;
yield 2;
yield 3;
}
let i = values();
i.next() //{value: 1, done: false}
i.next() //{value: 2, done: false}
i.next() //{value: 3, done: false}
i.next() //{done: true}
let o = new Observable(observer => {
observer.next(1);
observer.next(2);
observer.next(3);
observer.complete();
});
o.subscribe(
value => console.log(value),
err => console.error(err),
_ => console.log('done')
);
Pull
Push
Disposing Observables
//Observable constructor
let myObservable = new Observable(observer => {
let count = 0;
let interval = setInterval(() => {
observer.next(count++);
}, 100);
//disposal function
return () => {
clearInterval(interval);
}
});
let subscriber = myObservable.subscribe(
val => console.log(val),
err => console.log(err),
_ => console.log('done')
);
subscriber.unsubscribe();
const myButton = document.getElementById('myButton');
let clicks$ = new Observable(observer => {
let onClick = ev => observer.next(ev);
myButton.addEventListener('click', onClick);
return () => {
myButton.removeEventListener('click', onClick);
}
});
let clickListener = clicks$.subscribe(event => console.log(event));
//later...
clickListener.unsubscribe();
My First Observable
const myButton = document.getElementById('myButton');
let clicks$ = Observable.fromEvent(myButton, 'click');
let clickListener = clicks$.subscribe(event => console.log(event));
//ClickEvent
//ClickEvent
//ClickEvent
//later...
clickListener.unsubscribe();
(easy mode)
const myInput = document.getElementById('myInput');
//stream of keyup Events
let keyups$ = Observable.fromEvent(myInput, 'keyup');
//map to the values
let inputs$ = keyups$.map(ev => ev.target.value);
inputs$.subscribe(text => console.log(text));
//h
//he
//hel
//hell
//hello
Inputs - .map
const incrementButton = document.getElementById('increment');
const decrementButton = document.getElementById('decrement');
const counterOutput = document.getElementById('output');
const getValue = ev => parseInt(ev.target.value,10);
let increments$ = Observable.fromEvent(incrementButton, 'click');
let decrements$ = Observable.fromEvent(decrementButton, 'click');
//merge into a single stream, map to int values
let changes$ = Observable.merge(increments$, decrements$).map(getValue);
//scan (reduce) to track the state
let total$ = changes$.scan((total, value) => total + value, 0);
//set count
total$.subscribe(count => {
counterOutput.innerText = count;
console.log(count);
});
Counter - .reduce/.scan
import {Http} from 'angular2/http'
const myInput = document.getElementById('myInput');
//stream of keyups Events
let keyups$ = Observable.fromEvent(myInput, 'keyup');
//map to the values
let inputs$ = keyups$.map(ev => ev.target.value);
//flatMap our inputs to *http responses*
let responses$ =
inputs$
.flatMap(text => Http.get(`foo.com/search/${text}`))
.map(res => res.json());
responses$.subscribe(text => console.log(text));
//[{name: 'harry'},{name: 'hettie'}, {name: 'hellboy'}];
//[{name: 'harry'}, {name: 'hellboy'}];
//[{name: 'hellboy'}];
Angular2 Http
import {Http} from 'angular2/http'
const myInput = document.getElementById('myInput');
//stream of click Events
let clicks$ = Observable.fromEvent(myInput, 'keyup');
//map to the values
let inputs$ = clicks$.map(ev => ev.target.value);
//flatMap our inputs to *http responses*
let responses$ =
inputs$
.flatMapLatest(text => Http.get(`foo.com/search/${text}`))
.map(res => res.json());
responses$.subscribe(text => console.log(text));
//[{name: 'harry'},{name: 'hettie'}, {name: 'hellboy'}];
//[{name: 'harry'}, {name: 'hellboy'}];
//[{name: 'hellboy'}];
Http - flatMapLatest
class MyAutoCompleteComponent {
constructor(http:Http, formBuilder:FormBuilder){
this.searchForm = formBuilder.group({
name: ""
});
this.people = searchForm.controls.name.valueChanges
.flatMapLatest(text => Http.get(`foo.com/search/${text}`))
.map(res => res.json());
}
}
Angular2 Http + Async Pipe
<div [ng-form-model]="searchForm">
<input type="text" ng-control="name">
</div>
<div>
<ul>
<li *ng-for="#person in people | async">{{person.name}}</li>
</ul>
</div>
let responses$ =
http.get('somebadconnection.json')
.retry(3)
.map(res => res.json());
responses$.subscribe(
res => console.log(res),
err => console.log('couldn't connect!')
);
Angular2 Http Retry
let ticks$ = Observable.interval(5000);
let responses$ =
ticks$
.flatMapLatest(() => http.get('stocks.json'))
.map(res => res.json());
let stockPoller = responses$.subscribe(res => console.log(res));
//later
stockPoller.unsubscribe();
Angular2 Http Polling
redux
export const todos = (state = [], action) => {
switch(action.type){
case ADD_TODO:
let todo = Object.assign(
{},
action.payload,
{id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1})
return [...state, todo];
case REMOVE_TODO:
return state.filter(todo => todo.id !== action.payload.id)
case UPDATE_TODO:
return state.map(todo =>
todo.id === action.payload.id ?
Object.assign({}, todo, action.payload) : todo)
case TOGGLE_TODO:
return state.map(todo =>
todo.id === action.payload.id ?
Object.assign({}, todo, action.payload): todo)
case TOGGLE_ALL_TODOS:
return state.map(todo => {
return Object.assign({},todo, action.payload)
})
case REMOVE_COMPLETED_TODOS:
return state.filter(todo => !todo.completed)
default:
return state;
}
};
Remember:
Resources
Questions?
Everything is a stream.
By Rob Wormald
Everything is a stream.
- 34,907