docs(aio): add Observable and Rx docs (#21423)

PR Close #21423
This commit is contained in:
Jason Aden 2018-01-09 11:31:41 -08:00 committed by Alex Eagle
parent d100f1b187
commit 79656e7f96
28 changed files with 1301 additions and 0 deletions

View File

@ -0,0 +1,131 @@
import { Component, Output, OnInit, EventEmitter, NgModule } from '@angular/core';
import { Observable } from 'rxjs/Observable';
// #docregion eventemitter
@Component({
selector: 'zippy',
template: `
<div class="zippy">
<div (click)="toggle()">Toggle</div>
<div [hidden]="!visible">
<ng-content></ng-content>
</div>
</div>`})
export class ZippyComponent {
visible = true;
@Output() open = new EventEmitter<any>();
@Output() close = new EventEmitter<any>();
toggle() {
this.visible = !this.visible;
if (this.visible) {
this.open.emit(null);
} else {
this.close.emit(null);
}
}
}
// #enddocregion eventemitter
// #docregion pipe
@Component({
selector: 'async-observable-pipe',
template: `<div><code>observable|async</code>:
Time: {{ time | async }}</div>`
})
export class AsyncObservablePipeComponent {
time = new Observable(observer =>
setInterval(() => observer.next(new Date().toString()), 1000)
);
}
// #enddocregion pipe
// #docregion router
import { Router, NavigationStart } from '@angular/router';
import { filter } from 'rxjs/operators';
@Component({
selector: 'app-routable',
templateUrl: './routable.component.html',
styleUrls: ['./routable.component.css']
})
export class Routable1Component implements OnInit {
navStart: Observable<NavigationStart>;
constructor(private router: Router) {
// Create a new Observable the publishes only the NavigationStart event
this.navStart = router.events.pipe(
filter(evt => evt instanceof NavigationStart)
) as Observable<NavigationStart>;
}
ngOnInit() {
this.navStart.subscribe(evt => console.log('Navigation Started!'));
}
}
// #enddocregion router
// #docregion activated_route
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-routable',
templateUrl: './routable.component.html',
styleUrls: ['./routable.component.css']
})
export class Routable2Component implements OnInit {
constructor(private activatedRoute: ActivatedRoute) {}
ngOnInit() {
this.activatedRoute.url
.subscribe(url => console.log('The URL changed to: ' + url));
}
}
// #enddocregion activated_route
// #docregion forms
import { FormGroup } from '@angular/forms';
@Component({
selector: 'my-component',
template: 'MyComponent Template'
})
export class MyComponent implements OnInit {
nameChangeLog: string[] = [];
heroForm: FormGroup;
ngOnInit() {
this.logNameChange();
}
logNameChange() {
const nameControl = this.heroForm.get('name');
nameControl.valueChanges.forEach(
(value: string) => this.nameChangeLog.push(value)
);
}
}
// #enddocregion forms
@NgModule({
declarations:
[ZippyComponent, AsyncObservablePipeComponent, Routable1Component, Routable2Component, MyComponent]
})
export class AppModule {
}

View File

@ -0,0 +1,66 @@
import { Observable } from 'rxjs/Observable';
// #docregion subscriber
// This function runs when subscribe() is called
function sequenceSubscriber(observer) {
// synchronously deliver 1, 2, and 3, then complete
observer.next(1);
observer.next(2);
observer.next(3);
observer.complete();
// unsubscribe function doesn't need to do anything in this
// because values are delivered synchronously
return {unsubscribe() {}};
}
// Create a new Observable that will deliver the above sequence
const sequence = new Observable(sequenceSubscriber);
// execute the Observable and print the result of each notification
sequence.subscribe({
next(num) { console.log(num); },
complete() { console.log('Finished sequence'); }
});
// Logs:
// 1
// 2
// 3
// Finished sequence
// #enddocregion subscriber
// #docregion fromevent
function fromEvent(target, eventName) {
return new Observable((observer) => {
const handler = (e) => observer.next(e);
// Add the event handler to the target
target.addEventListener(eventName, handler);
return () => {
// Detach the event handler from the target
target.removeEventListener(eventName, handler);
};
});
}
// #enddocregion fromevent
// #docregion fromevent_use
const ESC_KEY = 27;
const nameInput = document.getElementById('name') as HTMLInputElement;
const subscription = fromEvent(nameInput, 'keydown')
.subscribe((e: KeyboardEvent) => {
if (e.keyCode === ESC_KEY) {
nameInput.value = '';
}
});
// #enddocregion fromevent_use

View File

@ -0,0 +1,32 @@
import { Observable } from 'rxjs/Observable';
// #docregion
// Create an Observable that will start listening to geolocation updates
// when a consumer subscribes.
const locations = new Observable((observer) => {
// Get the next and error callbacks. These will be passed in when
// the consumer subscribes.
const {next, error} = observer;
let watchId;
// Simple geolocation API check provides values to publish
if ('geolocation' in navigator) {
watchId = navigator.geolocation.watchPosition(next, error);
} else {
error('Geolocation not available');
}
// When the consumer unsubscribes, clean up data ready for next subscription.
return {unsubscribe() { navigator.geolocation.clearWatch(watchId); }};
});
// Call subscribe() to start listening for updates.
const locationsSubscription = locations.subscribe({
next(position) { console.log('Current Position: ', position); },
error(msg) { console.log('Error Getting Location: ', msg); }
});
// Stop listening for location after 10 seconds
setTimeout(() => { locationsSubscription.unsubscribe(); }, 10000);
// #enddocregion

View File

@ -0,0 +1,5 @@
import './geolocation';
import './subscribing';
import './creating';
import './multicasting';

View File

@ -0,0 +1,155 @@
import { Observable } from 'rxjs/Observable';
// #docregion delay_sequence
function sequenceSubscriber(observer) {
const seq = [1, 2, 3];
let timeoutId;
// Will run through an array of numbers, emitting one value
// per second until it gets to the end of the array.
function doSequence(arr, idx) {
timeoutId = setTimeout(() => {
observer.next(arr[idx]);
if (idx === arr.length - 1) {
observer.complete();
} else {
doSequence(arr, idx++);
}
}, 1000);
}
doSequence(seq, 0);
// Unsubscribe should clear the timeout to stop execution
return {unsubscribe() {
clearTimeout(timeoutId);
}};
}
// Create a new Observable that will deliver the above sequence
const sequence = new Observable(sequenceSubscriber);
sequence.subscribe({
next(num) { console.log(num); },
complete() { console.log('Finished sequence'); }
});
// Logs:
// (at 1 second): 1
// (at 2 seconds): 2
// (at 3 seconds): 3
// (at 3 seconds): Finished sequence
// #enddocregion delay_sequence
// #docregion subscribe_twice
// Subscribe starts the clock, and will emit after 1 second
sequence.subscribe({
next(num) { console.log('1st subscribe: ' + num); },
complete() { console.log('1st sequence finished.'); }
});
// After 1/2 second, subscribe again.
setTimeout(() => {
sequence.subscribe({
next(num) { console.log('2nd subscribe: ' + num); },
complete() { console.log('2nd sequence finished.'); }
});
}, 500);
// Logs:
// (at 1 second): 1st subscribe: 1
// (at 1.5 seconds): 2nd subscribe: 1
// (at 2 seconds): 1st subscribe: 2
// (at 2.5 seconds): 2nd subscribe: 2
// (at 3 seconds): 1st subscribe: 3
// (at 3 seconds): 1st sequence finished
// (at 3.5 seconds): 2nd subscribe: 3
// (at 3.5 seconds): 2nd sequence finished
// #enddocregion subscribe_twice
// #docregion multicast_sequence
function multicastSequenceSubscriber() {
const seq = [1, 2, 3];
// Keep track of each observer (one for every active subscription)
const observers = [];
// Still a single timeoutId because there will only ever be one
// set of values being generated, multicasted to each subscriber
let timeoutId;
// Return the subscriber function (runs when subscribe()
// function is invoked)
return (observer) => {
observers.push(observer);
// When this is the first subscription, start the sequence
if (observers.length === 1) {
timeoutId = doSequence({
next(val) {
// Iterate through observers and notify all subscriptions
observers.forEach(obs => obs.next(val));
},
complete() {
// Notify all complete callbacks
observers.forEach(obs => obs.complete());
}
}, seq, 0);
}
return {
unsubscribe() {
// Remove from the observers array so it's no longer notified
observers.splice(observers.indexOf(observer), 1);
// If there's no more listeners, do cleanup
if (observers.length === 0) {
clearTimeout(timeoutId);
}
}
};
};
}
// Run through an array of numbers, emitting one value
// per second until it gets to the end of the array.
function doSequence(observer, arr, idx) {
return setTimeout(() => {
observer.next(arr[idx]);
if (idx === arr.length - 1) {
observer.complete();
} else {
doSequence(observer, arr, idx++);
}
}, 1000);
}
// Create a new Observable that will deliver the above sequence
const multicastSequence = new Observable(multicastSequenceSubscriber);
// Subscribe starts the clock, and begins to emit after 1 second
multicastSequence.subscribe({
next(num) { console.log('1st subscribe: ' + num); },
complete() { console.log('1st sequence finished.'); }
});
// After 1 1/2 seconds, subscribe again (should "miss" the first value).
setTimeout(() => {
multicastSequence.subscribe({
next(num) { console.log('2nd subscribe: ' + num); },
complete() { console.log('2nd sequence finished.'); }
});
}, 1500);
// Logs:
// (at 1 second): 1st subscribe: 1
// (at 2 seconds): 1st subscribe: 2
// (at 2 seconds): 2nd subscribe: 2
// (at 3 seconds): 1st subscribe: 3
// (at 3 seconds): 1st sequence finished
// (at 3 seconds): 2nd subscribe: 3
// (at 3 seconds): 2nd sequence finished
// #enddocregion multicast_sequence

View File

@ -0,0 +1,33 @@
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
// #docregion observer
// Create simple observable that emits three values
const myObservable = Observable.of(1, 2, 3);
// Create observer object
const myObserver = {
next: x => console.log('Observer got a next value: ' + x),
error: err => console.error('Observer got an error: ' + err),
complete: () => console.log('Observer got a complete notification'),
};
// Execute with the observer object
myObservable.subscribe(myObserver);
// Logs:
// Observer got a next value: 1
// Observer got a next value: 2
// Observer got a next value: 3
// Observer got a complete notification
// #enddocregion observer
// #docregion sub_fn
myObservable.subscribe(
x => console.log('Observer got a next value: ' + x),
err => console.error('Observer got an error: ' + err),
() => console.log('Observer got a complete notification')
);
// #enddocregion sub_fn

View File

@ -0,0 +1,26 @@
import { ajax } from 'rxjs/observable/dom/ajax';
import { range } from 'rxjs/observable/range';
import { timer } from 'rxjs/observable/timer';
import { pipe } from 'rxjs/util/pipe';
import { retryWhen, zip, map, mergeMap } from 'rxjs/operators';
function backoff(maxTries, ms) {
return pipe(
retryWhen(attempts => range(1, maxTries)
.pipe(
zip(attempts, (i) => i),
map(i => i * i),
mergeMap(i => timer(i * ms))
)
)
);
}
ajax('/api/endpoint')
.pipe(backoff(3, 250))
.subscribe(data => handleData(data));
function handleData(data) {
// ...
}

View File

@ -0,0 +1,18 @@
import { fromEvent } from 'rxjs/observable/fromEvent';
import { ajax } from 'rxjs/observable/dom/ajax';
import { map, filter, debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
const searchBox = document.getElementById('search-box');
const typeahead = fromEvent(searchBox, 'input').pipe(
map((e: KeyboardEvent) => e.target.value),
filter(text => text.length > 2),
debounceTime(10),
distinctUntilChanged(),
switchMap(() => ajax('/api/endpoint'))
);
typeahead.subscribe(data => {
// Handle the data from the API
});

View File

@ -0,0 +1,26 @@
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
// #docregion
import { ajax } from 'rxjs/observable/dom/ajax';
import { map, catchError } from 'rxjs/operators';
// Return "response" from the API. If an error happens,
// return an empty array.
const apiData = ajax('/api/data').pipe(
map(res => {
if (!res.response) {
throw new Error('Value expected!');
}
return res.response;
}),
catchError(err => Observable.of([]))
);
apiData.subscribe({
next(x) { console.log('data: ', x); },
error(err) { console.log('errors already caught... will not run'); }
});
// #enddocregion

View File

@ -0,0 +1,20 @@
import { Component } from '@angular/core';
import { Observable } from 'rxjs/Observable';
@Component({
selector: 'app-stopwatch',
templateUrl: './stopwatch.component.html'
})
export class StopwatchComponent {
stopwatchValue: number;
stopwatchValue$: Observable<number>;
start() {
this.stopwatchValue$.subscribe(num =>
this.stopwatchValue = num
);
}
}

View File

@ -0,0 +1,25 @@
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
// #docregion
import { pipe } from 'rxjs/util/pipe';
import { filter, map } from 'rxjs/operators';
const nums = Observable.of(1, 2, 3, 4, 5);
// Create a function that accepts an Observable.
const squareOddVals = pipe(
filter(n => n % 2),
map(n => n * n)
);
// Create an Observable that will run the filter and map functions
const squareOdd = squareOddVals(nums);
// Suscribe to run the combined functions
squareOdd.subscribe(x => console.log(x));
// #enddocregion

View File

@ -0,0 +1,18 @@
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
// #docregion
import { filter } from 'rxjs/operators/filter';
import { map } from 'rxjs/operators/map';
const squareOdd = Observable.of(1, 2, 3, 4, 5)
.pipe(
filter(n => n % 2),
map(n => n * n)
);
// Subscribe to get values
squareOdd.subscribe(x => console.log(x));
// #enddocregion

View File

@ -0,0 +1,21 @@
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
// #docregion
import { map } from 'rxjs/operators';
const nums = Observable.of(1, 2, 3);
const squareValues = map((val: number) => val * val);
const squaredNums = squareValues(nums);
squaredNums.subscribe(x => console.log(x));
// Logs
// 1
// 4
// 9
// #enddocregion

View File

@ -0,0 +1,27 @@
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
// #docregion
import { ajax } from 'rxjs/observable/dom/ajax';
import { map, retry, catchError } from 'rxjs/operators';
const apiData = ajax('/api/data').pipe(
retry(3), // Retry up to 3 times before failing
map(res => {
if (!res.response) {
throw new Error('Value expected!');
}
return res.response;
}),
catchError(err => Observable.of([]))
);
apiData.subscribe({
next(x) { console.log('data: ', x); },
error(err) { console.log('errors already caught... will not run'); }
});
// #enddocregion

View File

@ -0,0 +1,65 @@
// #docregion promise
import { fromPromise } from 'rxjs/observable/fromPromise';
// Create an Observable out of a promise
const data = fromPromise(fetch('/api/endpoint'));
// Subscribe to begin listening for async result
data.subscribe({
next(response) { console.log(response); },
error(err) { console.error('Error: ' + err); },
complete() { console.log('Completed'); }
});
// #enddocregion promise
// #docregion interval
import { interval } from 'rxjs/observable/interval';
// Create an Observable that will publish a value on an interval
const secondsCounter = interval(1000);
// Subscribe to begin publishing values
secondsCounter.subscribe(n =>
console.log(`It's been ${n} seconds since subscribing!`));
// #enddocregion interval
// #docregion event
import { fromEvent } from 'rxjs/observable/fromEvent';
const el = document.getElementById('my-element');
// Create an Observable that will publish mouse movements
const mouseMoves = fromEvent(el, 'mousemove');
// Subscribe to start listening for mouse-move events
const subscription = mouseMoves.subscribe((evt: MouseEvent) => {
// Log coords of mouse movements
console.log(`Coords: ${evt.clientX} X ${evt.clientY}`);
// When the mouse is over the upper-left of the screen,
// unsubscribe to stop listening for mouse movements
if (evt.clientX < 40 && evt.clientY < 40) {
subscription.unsubscribe();
}
});
// #enddocregion event
// #docregion ajax
import { ajax } from 'rxjs/observable/dom/ajax';
// Create an Observable that will create an AJAX request
const apiData = ajax('/api/data');
// Subscribe to create the request
apiData.subscribe(res => console.log(res.status, res.response));
// #enddocregion ajax

View File

@ -0,0 +1,313 @@
# Observables compared to other techniques
You can often use observables instead of promises to deliver values asynchronously. Similarly, observables can take the place of event handlers. Finally, because observables deliver multiple values, you can use them where you might otherwise build and operate on arrays.
Observables behave somewhat differently from the alternative techniques in each of these situations, but offer some significant advantages. Here are detailed comparisons of the differences.
## Observables compared to promises
Observables are often compared to promises. Here are some key differences:
* Observables are declarative; computation does not start until subscription. Promises execute immediately on creation. This makes observables useful for defining recipes that can be run whenever you need the result.
* Observables provide many values. Promises provide one. This makes observables useful for getting multiple values over time.
* Observables differentiate between chaining and subscription. Promises only have `.then()` clauses. This makes observables useful for creating complex transformation recipes to be used by other part of the system, without causing the work to be executed.
* Observables `subscribe()` is responsible for handling errors. Promises push errors to the child promises. This makes observables useful for centralized and predictable error handling.
### Creation and subscription
* Observables are not executed until a consumer subcribes. The `subscribe()` executes the defined behavior once, and it can be called again. Each subscription has its own computation. Resubscription causes recomputation of values.
<code-example hideCopy>
// declare a publishing operation
new Observable((observer) => { subscriber_fn });
// initiate execution
observable.subscribe(() => {
// observer handles notifications
});
</code-example>
* Promises execute immediately, and just once. The computation of the result is initiated when the promise is created. There is no way to restart work. All `then` clauses (subscriptions) share the same computation.
<code-example hideCopy>
// initiate execution
new Promise((resolve, reject) => { executer_fn });
// handle return value
promise.then((value) => {
// handle result here
});
</code-example>
### Chaining
* Observables differentiate between transformation function such as a map and subscription. Only subscription activates the subscriber function to start computing the values.
<code-example hideCopy>observable.map((v) => 2*v);</code-example>
* Promises do not differentiate between the last `.then` clauses (equivalent to subscription) and intermediate `.then` clauses (equivalent to map).
<code-example hideCopy>promise.then((v) => 2*v);</code-example>
### Cancellation
* Observable subscriptions are cancellable. Unsubscribing removes the listener from receiving further values, and notifies the subscriber function to cancel work.
<code-example hideCopy>
const sub = obs.subscribe(...);
sub.unsubscribe();
</code-example>
* Promises are not cancellable.
### Error handling
* Observable execution errors are delivered to the subscriber's error handler, and the subscriber automatically unsubscribes from the observable.
<code-example hideCopy>
obs.subscribe(() => {
throw Error('my error');
});
</code-example>
* Promises push errors to the child promises.
<code-example hideCopy>
promise.then(() => {
throw Error('my error');
});
</code-example>
### Cheat sheet
The following code snippets illustrate how the same kind of operation is defined using observables and promises.
<table>
<th>
<td>Operation</td>
<td>Observable</td>
<td>Promise</td>
</th>
<tr>
<td>Creation</td>
<td>
<pre>new Observable((observer) => {
observer.next(123);
});</pre>
</td>
<td>
<pre>new Promise((resolve, reject) => {
resolve(123);
});</pre>
</td>
</tr>
<tr>
<td>Transform</td>
<td><pre>obs.map((value) => value * 2 );</pre></td>
<td><pre>promise.then((value) => value * 2);</pre></td>
</tr>
<tr>
<td>Subscribe</td>
<td>
<pre>sub = obs.subscribe((value) => {
console.log(value)
});</pre>
</td>
<td>
<pre>promise.then((value) => {
console.log(value);
});</pre>
</td>
</tr>
<tr>
<td>Unsubscribe</td>
<td><pre>sub.unsubscribe();</pre></td>
<td>Implied by promise resolution.</td>
</tr>
</table>
## Observables compared to events API
Observables are very similar to event handlers that use the events API. Both techniques define notification handlers, and use them to process multiple values delivered over time. Subscribing to an observable is equivalent to adding an event listener. One significant difference is that you can configure an observable to transform an event before passing the event to the handler.
Using observables to handle events and asynchronous operations can have the advantage of greater consistency in contexts such as HTTP requests.
Here are some code samples that illustrate how the same kind of operation is defined using observables and the events API.
<table>
<th>
<td>Observable</td>
<td>Events API</td>
</th>
<tr>
<td>Creation & cancellation</td>
<td>
<pre>// Setup
let clicks$ = fromEvent(buttonEl, click);
// Begin listening
let subscription = clicks$
.subscribe(e => console.log(Clicked, e))
// Stop listening
subscription.unsubscribe();</pre>
</td>
<td>
<pre>function handler(e) {
console.log(Clicked, e);
}
// Setup & begin listening
button.addEventListener(click, handler);
// Stop listening
button.removeEventListener(click, handler);
</pre>
</td>
</tr>
<tr>
<td>Subscription</td>
<td>
<pre>observable.subscribe(() => {
// notification handlers here
});</pre>
</td>
<td>
<pre>element.addEventListener(eventName, (event) => {
// notification handler here
});</pre>
</td>
</tr>
<tr>
<td>Configuration</td>
<td>Listen for keystrokes, but provide a stream representing the value in the input.
<pre>fromEvent(inputEl, 'keydown').pipe(
map(e => e.target.value)
);</pre>
</td>
<td>Does not support configuration.
<pre>element.addEventListener(eventName, (event) => {
// Cannot change the passed Event into another
// value before it gets to the handler
});</pre>
</td>
</tr>
</table>
## Observables compared to arrays
An observable produces values over time. An array is created as a static set of values. In a sense, observables are asynchronous where arrays are synchronous. In the following examples, ➞ implies asynchronous value delivery.
<table>
<th>
<td>Observable</td>
<td>Array</td>
</th>
<tr>
<td>Given</td>
<td>
<pre>obs: ➞1➞2➞3➞5➞7</pre>
<pre>obsB: ➞'a'➞'b'➞'c'</pre>
</td>
<td>
<pre>arr: [1, 2, 3, 5, 7]</pre>
<pre>arrB: ['a', 'b', 'c']</pre>
</td>
</tr>
<tr>
<td><pre>concat()</pre></td>
<td>
<pre>obs.concat(obsB)</pre>
<pre>➞1➞2➞3➞5➞7➞'a'➞'b'➞'c'</pre>
</td>
<td>
<pre>arr.concat(arrB)</pre>
<pre>[1,2,3,5,7,'a','b','c']</pre>
</td>
</tr>
<tr>
<td><pre>filter()</pre></td>
<td>
<pre>obs.filter((v) => v>3)</pre>
<pre>➞5➞7</pre>
</td>
<td>
<pre>arr.filter((v) => v>3)</pre>
<pre>[5, 7]</pre>
</td>
</tr>
<tr>
<td><pre>find()</pre></td>
<td>
<pre>obs.find((v) => v>3)</pre>
<pre>➞5</pre>
</td>
<td>
<pre>arr.find((v) => v>10)</pre>
<pre>5</pre>
</td>
</tr>
<tr>
<td><pre>findIndex()</pre></td>
<td>
<pre>obs.findIndex((v) => v>3)</pre>
<pre>➞3</pre>
</td>
<td>
<pre>arr.findIndex((v) => v>3)</pre>
<pre>3</pre>
</td>
</tr>
<tr>
<td><pre>forEach()</pre></td>
<td>
<pre>obs.forEach((v) => {
console.log(v);
})
1
2
3
4
5</pre>
</td>
<td>
<pre>arr.forEach((v) => {
console.log(v);
})
1
2
3
4
5</pre>
</td>
</tr>
<tr>
<td><pre>map()</pre></td>
<td>
<pre>obs.map((v) => -v)</pre>
<pre>➞-1➞-2➞-3➞-5➞-7</pre>
</td>
<td>
<pre>arr.map((v) => -v)</pre>
<pre>[-1, -2, -3, -5, -7]</pre>
</td>
</tr>
<tr>
<td><pre>reduce()</pre></td>
<td>
<pre>obs.scan((s,v)=> s+v, 0)</pre>
<pre>➞1➞3➞6➞11➞18</pre>
</td>
<td>
<pre>arr.reduce((s,v) => s+v, 0)</pre>
<pre>18</pre>
</td>
</tr>
</table>

View File

@ -0,0 +1,51 @@
# Observables in Angular
Angular makes use of observables as an interface to handle a variety of common asynchronous operations. For example:
* The `EventEmitter` class extends `Observable`.
* The HTTP module uses observables to handle AJAX requests and responses.
* The Router and Forms modules use observables to listen for and respond to user-input events.
## Event emitter
Angular provides an `EventEmitter` class that is used when publishing values from a component through the `@Output()` decorator. `EventEmitter` extends `Observable`, adding an `emit()` method so it can send arbitrary values. When you call `emit()`, it passes the emitted value to the `next()` method of any subscribed observer.
A good example of usage can be found on the [EventEmitter](https://angular.io/api/core/EventEmitter) documentation. Here is the example component that listens for open and close events:
`<zippy (open)="onOpen($event)" (close)="onClose($event)"></zippy>`
Here is the component definition:
<code-example path="observables-in-angular/src/main.ts" title="EventEmitter" region="eventemitter"></code-example>
## HTTP
Angulars `HttpClient` returns observables from HTTP method calls. For instance, `http.get(/api)` returns an observable. This provides several advantages over promise-based HTTP APIs:
* Observables do not mutate the server response (as can occur through chained `.then()` calls on promises). Instead, you can use a series of operators to transform values as needed.
* HTTP requests are cancellable through the `unsubscribe()` method.
* Requests can be configured to get progress event updates.
* Failed requests can be retried easily.
## Async pipe
The [AsyncPipe](https://angular.io/api/common/AsyncPipe) subscribes to an observable or promise and returns the latest value it has emitted. When a new value is emitted, the pipe marks the component to be checked for changes.
The following example binds the `time` observable to the component's view. The observable continuously updates the view with the current time.
<code-example path="observables-in-angular/src/main.ts" title="Using async pipe" region="pipe"></code-example>
## Router
[`Router.events`](https://angular.io/api/router/Router#events) provides events as observables. You can use the `filter()` operator from RxJS to look for events of interest, and subscribe to them in order to make decisions based on the sequence of events in the navigation process. Here's an example:
<code-example path="observables-in-angular/src/main.ts" title="Router events" region="router"></code-example>
The [ActivatedRoute](https://angular.io/api/router/ActivatedRoute) is an injected router service that makes use of observables to get information about a route path and parameters. For example, `ActivateRoute.url` contains an observable that reports the route path or paths. Here's an example:
<code-example path="observables-in-angular/src/main.ts" title="ActivatedRoute" region="activated_route"></code-example>
## Reactive forms
Reactive forms have properties that use observables to monitor form control values. The [`FormControl`](https://angular.io/api/forms/FormControl) properties `valueChanges` and `statusChanges` contain observables that raise change events. Subscribing to an observable form-control property is a way of triggering application logic within the component class. For example:
<code-example path="observables-in-angular/src/main.ts" title="Reactive forms" region="forms"></code-example>

View File

@ -0,0 +1,114 @@
# Observables
Observables provide support for passing messages between publishers and subscribers in your application. Observables offer significant benefits over other techniques for event handling, asynchronous programming, and handling multiple values.
Observables are declarative&mdash;that is, you define a function for publishing values, but it is not executed until a consumer subscribes to it. The subscribed consumer then receives notifications until the function completes, or until they unsubscribe.
An observable can deliver multiple values of any type&mdash;literals, messages, or events, depending on the context. The API for receiving values is the same whether the values are delivered synchronously or asynchronously. Because setup and teardown logic are both handled by the observable, your application code only needs to worry about subscribing to consume values, and when done, unsubscribing. Whether the stream was keystrokes, an HTTP response, or an interval timer, the interface for listening to values and stopping listening is the same.
Because of these advantages, observables are used extensively within Angular, and are recommended for app development as well.
## Basic usage and terms
As a publisher, you create an `Observable` instance that defines a *subscriber* function. This is the function that is executed when a consumer calls the `subscribe()` method. The subscriber function defines how to obtain or generate values or messages to be published.
To execute the observable you have created and begin receiving notifications, you call its `subscribe()` method, passing an *observer*. This is a JavaScript object that defines the handlers for the notifications you receive. The `subscribe()` call returns a `Subscription` object that has an `unsubscribe()` method, which you call to stop receiving notifications.
Here's an example that demonstrates the basic usage model by showing how an observable could be used to provide geolocation updates.
<code-example path="observables/src/geolocation.ts" title="Observe geolocation updates"></code-example>
## Defining observers
A handler for receiving observable notifications implements the `Observer` interface. It is an object that defines callback methods to handle the three types of notifications that an observable can send:
| Notification type | Description |
|:---------|:-------------------------------------------|
| `next` | Required. A handler for each delivered value. Called zero or more times after execution starts.|
| `error` | Optional. A handler for an error notification. An error halts execution of the observable instance.|
| `complete` | Optional. A handler for the execution-complete notification. Delayed values can continue to be delivered to the next handler after execution is complete.|
An observer object can define any combination of these handlers. If you don't supply a handler for a notification type, the observer ignores notifications of that type.
## Subscribing
An `Observable` instance begins publishing values only when someone subscribes to it. You subscribe by calling the `subscribe()` method of the instance, passing an observer object to receive the notifications.
<div class="l-sub-section">
In order to show how subscribing works, we need to create a new observable. There is a constructor that you use to create new instances, but for illustration, we can use some static methods on the `Observable` class that create simple observables of frequently used types:
* `Observable.of(...items)`&mdash;Returns an `Observable` instance that synchronously delivers the values provided as arguments.
* `Observable.from(iterable)`&mdash;Converts its argument to an `Observable` instance. This method is commonly used to convert an array to an observable.
</div>
Here's an example of creating and subscribing to a simple observable, with an observer that logs the received message to the console:
<code-example
path="observables/src/subscribing.ts"
region="observer"
title="Subscribe using observer"></code-example>
Alternatively, the `subscribe()` method can accept callback function definitions in line, for `next`, `error`, and `complete` handlers. For example, the following `subscribe()` call is the same as the one that specifies the predefined observer:
<code-example path="observables/src/subscribing.ts" region="sub_fn" title="Subscribe with positional arguments"></code-example>
In either case, a `next` handler is required. The `error` and `complete` handlers are optional.
Note that a `next()` function could receive, for instance, message strings, or event objects, numeric values, or stuctures, depending on context. As a general term, we refer to data published by an observable as a *stream*. Any type of value can be represented with an observable, and the values are published as a stream.
## Creating observables
Use the `Observable` constructor to create an observable stream of any type. The constructor takes as its argument the subscriber function to run when the observables `subscribe()` method executes. A subscriber function receives an `Observer` object, and can publish values to the observer's `next()` method.
For example, to create an observable equivalent to the `Observable.of(1, 2, 3)` above, you could do something like this:
<code-example path="observables/src/creating.ts" region="subscriber" title="Create observable with constructor"></code-example>
To take this example a little further, we can create an observable that publishes events. In this example, the subscriber function is defined inline.
<code-example path="observables/src/creating.ts" region="fromevent" title="Create with custom fromEvent function"></code-example>
Now you can use this function to create an observable that publishes keydown events:
<code-example path="observables/src/creating.ts" region="fromevent_use" title="Use custom fromEvent function"></code-example>
## Multicasting
A typical observable creates a new, independent execution for each subscribed observer. When an observer subscribes, the observable wires up an event handler and delivers values to that observer. When a second observer subscribes, the observable then wires up a new event handler and delivers values to that second observer in a separate execution.
Sometimes, instead of starting an independent execution for each subscriber, you want each subscription to get the same values&mdash;even if values have already started emitting. This might be the case with something like an observable of clicks on the document object.
*Multicasting* is the practice of broadcasting to a list of multiple subscribers in a single execution. With a multicasting observable, you don't register multiple listeners on the document, but instead re-use the first listener and send values out to each subscriber.
When creating an observable you should determine how you want that observable to be used and whether or not you want to multicast its values.
Lets look at an example that counts from 1 to 3, with a one-second delay after each number emitted.
<code-example path="observables/src/multicasting.ts" region="delay_sequence" title="Create a delayed sequence"></code-example>
Notice that if you subscribe twice, there will be two separate streams, each emitting values every second. It looks something like this:
<code-example path="observables/src/multicasting.ts" region="subscribe_twice" title="Two subscriptions"></code-example>
Changing the observable to be multicasting could look something like this:
<code-example path="observables/src/multicasting.ts" region="multicast_sequence" title="Create a multicast subscriber"></code-example>
<div class="l-sub-section">
Multicasting observables take a bit more setup, but they can be useful for certain applications. Later we will look at tools that simplify the process of multicasting, allowing you to take any observable and make it multicasting.
</div>
## Error handling
Because observables produce values asynchronously, try/catch will not effectively catch errors. Instead, you handle errors by specifying an `error` callback on the observer. Producing an error also causes the observable to clean up subscriptions and stop producing values. An observable can either produce values (calling the `next` callback), or it can complete, calling either the `complete` or `error` callback.
<code-example>
myObservable.subscribe({
next(num) { console.log('Next num: ' + num)},
error(err) { console.log('Received an errror: ' + err)}
});
</code-example>
Error handling (and specifically recovering from an error) is covered in more detail in a later section.

View File

@ -0,0 +1,23 @@
# Practical observable usage
Here are some examples of domains in which observables are particularly useful.
## Type-ahead suggestions
Observables can simplify the implementation of type-ahead suggestions. Typically, a type-ahead has to do a series of separate tasks:
* Listen for data from an input.
* Trim the value (remove whitespace) and make sure its a minimum length.
* Debounce (so as not to send off API requests for every keystroke, but instead wait for a break in keystrokes).
* Dont send a request if the value stays the same (rapidly hit a character, then backspace, for instance).
* Cancel ongoing AJAX requests if their results will be invalidated by the updated results.
Writing this in full JavaScript can be quite involved. With observables, you can use a simple series of RxJS operators:
<code-example path="practical-observable-usage/src/typeahead.ts" title="Typeahead"></code-example>
## Exponential backoff
Exponential backoff is a technique in which you retry an API after failure, making the time in between retries longer after each consecutive failure, with a maximum number of retries after which the request is considered to have failed. This can be quite complex to implement with promises and other methods of tracking AJAX calls. With observables, it is very easy:
<code-example path="practical-observable-usage/src/backoff.ts" title="Exponential backoff"></code-example>

View File

@ -0,0 +1,97 @@
# The RxJS library
Reactive programming is an asynchronous programming paradigm concerned with data streams and the propagation of change ([Wikipedia](https://en.wikipedia.org/wiki/Reactive_programming)). RxJS (Reactive Extensions for JavaScript) is a library for reactive programming using observables that makes it easier to compose asynchronous or callback-based code ([RxJS Docs](http://reactivex.io/rxjs/)).
RxJS provides an implementation of the `Observable` type, which is needed until the type becomes part of the language and until browsers support it. The library also provides utility functions for creating and working with observables. These utility functions can be used for:
* Converting existing code for async operations into observables
* Iterating through the values in a stream
* Mapping values to different types
* Filtering streams
* Composing multiple streams
## Observable creation functions
RxJS offers a number of functions that can be used to create new observables. These functions can simplify the process of creating observables from things such as events, timers, promises, and so on. For example:
<code-example path="rx-library/src/simple-creation.ts" region="promise" title="Create an observable from a promise"></code-example>
<code-example path="rx-library/src/simple-creation.ts" region="interval" title="Create an observable from a counter"></code-example>
<code-example path="rx-library/src/simple-creation.ts" region="event" title="Create an observable from an event"></code-example>
<code-example path="rx-library/src/simple-creation.ts" region="ajax" title="Create an observable that creates an AJAX request"></code-example>
## Operators
Operators are functions that build on the observables foundation to enable sophisticated manipulation of collections. For example, RxJS defines operators such as `map()`, `filter()`, `concat()`, and `flatMap()`.
Operators take configuration options, and they return a function that takes a source observable. When executing this returned function, the operator observes the source observables emitted values, transforms them, and returns a new observable of those transformed values. Here is a simple example:
<code-example path="rx-library/src/operators.ts" title="Map operator"></code-example>
You can use _pipes_ to link operators together. Pipes let you combine multiple functions into a single function. The `pipe()` function takes as its arguments the functions you want to combine, and returns a new function that, when executed, runs the composed functions in sequence.
A set of operators applied to an observable is a recipe&mdash;that is, a set of instructions for producing the values youre interested in. By itself, the recipe doesnt do anything. You need to call `subscribe()` to produce a result through the recipe.
Heres an example:
<code-example path="rx-library/src/operators.1.ts" title="Standalone pipe function"></code-example>
The `pipe()` function is also a method on the RxJS `Observable`, so you use this shorter form to define the same operation:
<code-example path="rx-library/src/operators.2.ts" title="Observable.pipe function"></code-example>
### Common operators
RxJS provides many operators (over 150 of them), but only a handful are used frequently. Here is a list of common operators; for usage examples, see [RxJS 5 Operators By Example](https://github.com/btroncone/learn-rxjs/blob/master/operators/complete.md) in RxJS documentation.
<div class="l-sub-section">
Note that, for Angular apps, we prefer combining operators with pipes, rather than chaining. Chaining is used in many RxJS examples.
</div>
| Area | Operators |
| :------------| :----------|
| Creation | `from`, `fromPromise`,`fromEvent`, `of` |
| Combination | `combineLatest`, `concat`, `merge`, `startWith` , `withLatestFrom`, `zip` |
| Filtering | `debounceTime`, `distinctUntilChanged`, `filter`, `take`, `takeUntil` |
| Transformation | `bufferTime`, `concatMap`, `map`, `mergeMap`, `scan`, `switchMap` |
| Utility | `tap` |
| Multicasting | `share` |
## Error handling
In addition to the `error()` handler that you provide on subscription, RxJS provides the `catchError` operator that lets you handle known errors in the observable recipe.
For instance, suppose you have an observable that makes an API request and maps to the response from the server. If the server returns an error or the value doesnt exist, an error is produced. If you catch this error and supply a default value, your stream continues to process values rather than erroring out.
Here's an example of using the `catchError` operator to do this:
<code-example path="rx-library/src/error-handling.ts" title="catchError operator"></code-example>
### Retry failed observable
Where the `catchError` operator provides a simple path of recovery, the `retry` operator lets you retry a failed request.
Use the `retry` operator before the `catchError` operator. It resubscribes to the original source observable, which can then re-run the full sequence of actions that resulted in the error. If this includes an HTTP request, it will retry that HTTP request.
The following converts the previous example to retry the request before catching the error:
<code-example path="rx-library/src/retry-on-error.ts" title="retry operator"></code-example>
<div class="l-sub-section">
Do not retry **authentication** requests, since these should only be initiated by user action. We don't want to lock out user accounts with repeated login requests that the user has not initiated.
</div>
## Naming conventions for observables
Because Angular applications are mostly written in TypeScript, you will typically know when a variable is an observable. Although the Angular framework does not enforce a naming convention for observables, you will often see observables named with a trailing “$” sign.
This can be useful when scanning through code and looking for observable values. Also, if you want a property to store the most recent value from an observable, it can be convenient to simply use the same name with or without the “$”.
For example:
<code-example path="rx-library/src/naming-convention.ts" title="Naming observables"></code-example>

View File

@ -0,0 +1,3 @@
# Testing
TBD. Original content [here](https://docs.google.com/document/d/1gGP5sqWNCHAWWV_GLdZQ1XyMO4K-CHksUxux0BFtVxk/edit#heading=h.ohqykkhzdhb2).

View File

@ -216,6 +216,38 @@
} }
] ]
}, },
{
"title": "Observables & RxJS",
"tooltip": "Observables & RxJS",
"children": [
{
"url": "guide/observables",
"title": "Observables",
"tooltip": ""
},
{
"url": "guide/rx-library",
"title": "The RxJS Library",
"tooltip": ""
},
{
"url": "guide/observables-in-angular",
"title": "Observables in Angular",
"tooltip": ""
},
{
"url": "guide/practical-observable-usage",
"title": "Practical Usage",
"tooltip": ""
},
{
"url": "guide/comparing-observables",
"title": "Compare to Other Techniques",
"tooltip": ""
}
]
},
{ {
"url": "guide/bootstrapping", "url": "guide/bootstrapping",
"title": "Bootstrapping", "title": "Bootstrapping",