go beast mode with realtime reactive interfaces in angular 2 and firebase
TRANSCRIPT
Go Beast Mode with Realtime Reactive Interfacesin Angular and Firebase
State management
Controlling flow
Code volume
Enter Observables
return this.http.get(this.URLS.FETCH) .map(res => res.json()) .toPromise();
Problem solved!
Observables give us a powerful way to encapsulate, transpor t and transform data from user interactions to create powerful and immersive experiences.
Encapsulate Transport Transform
Encapsulate Transport Transform
Encapsulate Transport Transform
Iterator Pattern Observer Pattern
State Communication
Communicate state over time
Observable stream
Values over time
SINGLE MULTIPLE
SYNCHRONOUS Function Enumerable
ASYNCHRONOUS Promise Observable
Value consumption
SINGLE MULTIPLE
PULL Function Enumerable
PUSH Promise Observable
But observables are hard!!!
The Observable Stream
input output
output input
The Basic Sequence
final input
initial output
magic
subscribe
event
operators
@ViewChild('btn') btn;message: string;ngOnInit() { Observable.fromEvent(this.getNativeElement(this.btn), 'click') .subscribe(result => this.message = 'Beast Mode Activated!');}getNativeElement(element) { return element._elementRef.nativeElement;}
@ViewChild('btn') btn;message: string; ngOnInit() { Observable.fromEvent(this.getNativeElement(this.btn), 'click') .subscribe(result => this.message = 'Beast Mode Activated!');}getNativeElement(element) { return element._elementRef.nativeElement;}
Initial output
@ViewChild('btn') btn;message: string; ngOnInit() { Observable.fromEvent(this.getNativeElement(this.btn), 'click') .subscribe(event => this.message = 'Beast Mode Activated!');}getNativeElement(element) { return element._elementRef.nativeElement;}
Final input
@ViewChild('btn') btn;message: string; ngOnInit() { Observable.fromEvent(this.getNativeElement(this.btn), 'click') .map(event => 'Beast Mode Activated!') .subscribe(result => this.message = result);}getNativeElement(element) { return element._elementRef.nativeElement;}
Everything in between
@ViewChild('btn') btn;message: string; ngOnInit() { Observable.fromEvent(this.getNativeElement(this.btn), 'click') .filter(event => event.shiftKey) .map(event => 'Beast Mode Activated!') .subscribe(result => this.message = result);}getNativeElement(element) { return element._elementRef.nativeElement;}
Everything in between
BASIC SEQUENCE
How do we preserve state in a stream?
<button #right>Right</button><div class="container"> <div #ball class="ball" [style.left]="position.x + 'px'" [style.top]="position.y + 'px'"> </div></div>
@ViewChild('right') right;position: any;ngOnInit() { Observable .fromEvent(this.getNativeElement(this.right), 'click') .map(event => 10) .startWith({x: 100, y: 100}) .scan((acc, curr) => { return { y: acc.y, x: acc.x + curr}}) .subscribe(result => this.position = result);}
@ViewChild('right') right;position: any; ngOnInit() { Observable .fromEvent(this.getNativeElement(this.right), 'click') .map(event => 10) .startWith({x: 100, y: 100}) .scan((acc, curr) => { return { y: acc.y, x: acc.x + curr}}) .subscribe(result => this.position = result);}
@ViewChild('right') right;position: any; ngOnInit() { Observable .fromEvent(this.getNativeElement(this.right), 'click') .map(event => 10) .startWith({x: 100, y: 100}) .scan((acc, curr) => { return { y: acc.y, x: acc.x + curr}}) .subscribe(result => this.position = result);}
@ViewChild('right') right;position: any; ngOnInit() { Observable .fromEvent(this.getNativeElement(this.right), 'click') .map(event => 10) .startWith({x: 100, y: 100}) .scan((acc, curr) => { return { y: acc.y, x: acc.x + curr}}) .subscribe(result => this.position = result);}
MAINTAINING STATE
What if we have more than one stream?
@ViewChild('left') left;@ViewChild('right') right;position: any;ngOnInit() { const left$ = Observable.fromEvent(this.getNativeElement(this.left), 'click') .map(event => -10); const right$ = Observable.fromEvent(this.getNativeElement(this.right), 'click') .map(event => 10); Observable.merge(left$, right$) .startWith({x: 100, y: 100}) .scan((acc, curr) => { return { y: acc.y, x: acc.x + curr}}) .subscribe(result => this.position = result);}
@ViewChild('left') left;@ViewChild('right') right;position: any;ngOnInit() { const left$ = Observable.fromEvent(this.getNativeElement(this.left), 'click') .map(event => -10); const right$ = Observable.fromEvent(this.getNativeElement(this.right), 'click') .map(event => 10); Observable.merge(left$, right$) .startWith({x: 100, y: 100}) .scan((acc, curr) => { return { y: acc.y, x: acc.x + curr}}) .subscribe(result => this.position = result);}
MERGING STREAMS
What can we put in a stream?
increment(obj, prop, value) { return Object.assign({}, obj, {[prop]: obj[prop] + value})}decrement(obj, prop, value) { return Object.assign({}, obj, {[prop]: obj[prop] - value})}ngOnInit() { const leftArrow$ = Observable.fromEvent(document, 'keydown') .filter(event => event.key === 'ArrowLeft') .mapTo(position => this.decrement(position, 'x', 10)); const rightArrow$ = Observable.fromEvent(document, 'keydown') .filter(event => event.key === 'ArrowRight') .mapTo(position => this.increment(position, 'x', 10)); Observable.merge(leftArrow$, rightArrow$) .startWith({x: 100, y: 100}) .scan((acc, curr) => curr(acc)) .subscribe(result => this.position = result); }
increment(obj, prop, value) { return Object.assign({}, obj, {[prop]: obj[prop] + value})}decrement(obj, prop, value) { return Object.assign({}, obj, {[prop]: obj[prop] - value})}ngOnInit() { const leftArrow$ = Observable.fromEvent(document, 'keydown') .filter(event => event.key === 'ArrowLeft') .mapTo(position => this.decrement(position, 'x', 10)); const rightArrow$ = Observable.fromEvent(document, 'keydown') .filter(event => event.key === 'ArrowRight') .mapTo(position => this.increment(position, 'x', 10)); Observable.merge(leftArrow$, rightArrow$) .startWith({x: 100, y: 100}) .scan((acc, curr) => curr(acc)) .subscribe(result => this.position = result); }
increment(obj, prop, value) { return Object.assign({}, obj, {[prop]: obj[prop] + value})}decrement(obj, prop, value) { return Object.assign({}, obj, {[prop]: obj[prop] - value})}ngOnInit() { const leftArrow$ = Observable.fromEvent(document, 'keydown') .filter(event => event.key === 'ArrowLeft') .mapTo(position => this.decrement(position, 'x', 10)); const rightArrow$ = Observable.fromEvent(document, 'keydown') .filter(event => event.key === 'ArrowRight') .mapTo(position => this.increment(position, 'x', 10)); Observable.merge(leftArrow$, rightArrow$) .startWith({x: 100, y: 100}) .scan((acc, curr) => curr(acc)) .subscribe(result => this.position = result); }
MAPPING TO FUNCTIONS
How can we sequence a stream?
@ViewChild('ball') ball; position: any;ngOnInit() { const OFFSET = 50; const move$ = Observable.fromEvent(document, 'mousemove') .map(event => { return {x: event.pageX - OFFSET, y: event.pageY - OFFSET}; }); const down$ = Observable.fromEvent(this.ball.nativeElement, 'mousedown'); down$ .switchMap(event => move$) .startWith({ x: 100, y: 100}) .subscribe(result => this.position = result);}
@ViewChild('ball') ball;position: any;ngOnInit() { const OFFSET = 50; const move$ = Observable.fromEvent(document, 'mousemove') .map(event => { return {x: event.pageX - OFFSET, y: event.pageY - OFFSET}; }); const down$ = Observable.fromEvent(this.ball.nativeElement, 'mousedown'); down$ .switchMap(event => move$) .startWith({ x: 100, y: 100}) .subscribe(result => this.position = result); }
@ViewChild('ball') ball;position: any;ngOnInit() { const OFFSET = 50; const move$ = Observable.fromEvent(document, 'mousemove') .map(event => { return {x: event.pageX - OFFSET, y: event.pageY - OFFSET}; }); const down$ = Observable.fromEvent(this.ball.nativeElement, 'mousedown'); down$ .switchMap(event => move$) .startWith({ x: 100, y: 100}) .subscribe(result => this.position = result);}
@ViewChild('ball') ball;position: any;ngOnInit() { const OFFSET = 50; const move$ = Observable.fromEvent(document, 'mousemove') .map(event => { return {x: event.pageX - OFFSET, y: event.pageY - OFFSET}; }); const down$ = Observable.fromEvent(this.ball.nativeElement, 'mousedown'); down$ .switchMap(event => move$) .startWith({ x: 100, y: 100}) .subscribe(result => this.position = result);}
@ViewChild('ball') ball;position: any;ngOnInit() { const OFFSET = 50; const move$ = Observable.fromEvent(document, 'mousemove') .map(event => { return {x: event.pageX - OFFSET, y: event.pageY - OFFSET}; }); const down$ = Observable.fromEvent(this.ball.nativeElement, 'mousedown'); const up$ = Observable.fromEvent(document, 'mouseup'); down$ .switchMap(event => move$.takeUntil(up$)) .startWith({ x: 100, y: 100}) .subscribe(result => this.position = result); }
TRIGGERS
What effect does the origin of the stream have on the output?
<div class="container"> <app-line *ngFor="let line of lines" [line]="line"> </app-line></div>
<svg> <line [attr.x1]="line.x1" [attr.y1]="line.y1" [attr.x2]="line.x2" [attr.y2]="line.y2" style="stroke:rgb(255,0,0);stroke-width:2"/> </svg>
lines: any[] = []; ngOnInit() { Observable.fromEvent(document, 'click') .map(event => { return {x: event.pageX, y: event.pageY}; }) .pairwise(2) .map(positions => { const p1 = positions[0]; const p2 = positions[1]; return { x1: p1.x, y1: p1.y, x2: p2.x, y2: p2.y }; }) .subscribe(line => this.lines = [...this.lines, line]);}
STREAM ORIGINS
lines: any[] = []; ngOnInit() { Observable.fromEvent(document, 'mousemove') .map(event => { return {x: event.pageX, y: event.pageY}; }) .pairwise(2) .map(position => { const p1 = positions[0]; const p2 = positions[1]; return { x1: p1.x, y1: p1.y, x2: p2.x, y2: p2.y }; }) .subscribe(line => this.lines = [...this.lines, line]);}
STREAM ORIGINS
What are some fun things we can do with a stream?
@ViewChild('ball') ball;ngOnInit() { const OFFSET = 50; Observable.fromEvent(document, 'click') .map(event => { return {x: event.clientX - OFFSET, y: event.clientY - OFFSET} }) .subscribe(props => TweenMax.to(this.ball.nativeElement, 1, props))}
SIMPLE ANIMATION
What are some MOAR fun things we can do with a stream?
circles: any[] = [];ngOnInit() { const OFFSET = 25; Observable.fromEvent(document, 'mousemove') .map(event => { return { x: event.clientX - OFFSET, y: event.clientY - OFFSET} }) .subscribe(circle => this.circles = [ ...this.circles, circle])}
<div class="container"> <app-circle *ngFor="let circle of circles" [style.left]="circle.x + 'px'" [style.top]="circle.y + 'px'"> </app-circle></div>
export class CircleComponent implements OnInit { @ViewChild('circle') circle; ngOnInit() { TweenMax.to(this.circle.nativeElement, 2, {alpha: 0, width: 0, height: 0}); }}
ANIMATION
The Realtime Observable Stream
Start with a realtime database
You called?
import { AngularFireModule } from 'angularfire2'; export const firebaseConfig = { apiKey: 'PETERBACONDARWINISABEASTINSHEEPSCLOTHING', authDomain: 'rxjsbeastmode.firebaseapp.com', databaseURL: 'https://rxjsbeastmode.firebaseio.com', storageBucket: ''};@NgModule({ declarations: [AppComponent], imports: [ BrowserModule, AngularFireModule.initializeApp(firebaseConfig), ], bootstrap: [AppComponent]})export class AppModule {}
import { AngularFireModule } from 'angularfire2'; export const firebaseConfig = { apiKey: 'PETERBACONDARWINISABEASTINSHEEPSCLOTHING', authDomain: 'rxjsbeastmode.firebaseapp.com', databaseURL: 'https://rxjsbeastmode.firebaseio.com', storageBucket: ''};@NgModule({ declarations: [AppComponent], imports: [ BrowserModule, AngularFireModule.initializeApp(firebaseConfig), ], bootstrap: [AppComponent]})export class AppModule {}
Consume the realtime stream
const remote$ = this.af.database.object('clicker/');remote$ .subscribe(result => this.count = result.ticker);
Update the realtime stream
const remote$ = this.af.database.object('clicker/');Observable.fromEvent(this.getNativeElement(this.btn), 'click') .startWith({ticker: 0}) .scan((acc, curr) => { return { ticker: acc.ticker + 1 }; }) .subscribe(event => remote$.update(event));
const remote$ = this.af.database.object('clicker/');
// Outgoing Observable.fromEvent(this.getNativeElement(this.btn), 'click') .startWith({ticker: 0}) .scan((acc, curr) => { return { ticker: acc.ticker + 1 }; }) .subscribe(event => remote$.update(event));
// Incoming remote$ .subscribe(result => this.message = result.message);
const remote$ = this.af.database.object('clicker/');
// Outgoing ——> Observable.fromEvent(this.getNativeElement(this.btn), 'click') .startWith({ticker: 0}) .scan((acc, curr) => { return { ticker: acc.ticker + 1 }; }) .subscribe(event => remote$.update(event));
// <—— Incoming remote$ .subscribe(result => this.message = result.message);
BEAST MODE TIME!
REALTIME COUNTER
REALTIME SLIDESHOW
REALTIME LOCATION
REALTIME MAP
REALTIME ANNOTATIONS
REALTIME GAME
BUSINESS MODE TIME!
REALTIME SLIDER
But observables are hard!!!
I YOU!
@simpulton
https://egghead.io/courses/step-by-step-async-javascript-with-rxjs
https://egghead.io/courses/introduction-to-reactive-programming
Thanks!