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