diff --git a/aio/content/examples/observables-in-angular/example-config.json b/aio/content/examples/observables-in-angular/example-config.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aio/content/examples/observables-in-angular/src/main.ts b/aio/content/examples/observables-in-angular/src/main.ts new file mode 100644 index 0000000000..462dca8029 --- /dev/null +++ b/aio/content/examples/observables-in-angular/src/main.ts @@ -0,0 +1,131 @@ + +import { Component, Output, OnInit, EventEmitter, NgModule } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; + +// #docregion eventemitter + +@Component({ + selector: 'zippy', + template: ` +
+
Toggle
+
+ +
+
`}) + +export class ZippyComponent { + visible = true; + @Output() open = new EventEmitter(); + @Output() close = new EventEmitter(); + + 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: `
observable|async: + Time: {{ time | async }}
` +}) +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; + + 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; + } + + 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 { +} diff --git a/aio/content/examples/observables/example-config.json b/aio/content/examples/observables/example-config.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aio/content/examples/observables/src/creating.ts b/aio/content/examples/observables/src/creating.ts new file mode 100644 index 0000000000..ba77c4485c --- /dev/null +++ b/aio/content/examples/observables/src/creating.ts @@ -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 diff --git a/aio/content/examples/observables/src/geolocation.ts b/aio/content/examples/observables/src/geolocation.ts new file mode 100644 index 0000000000..c76a94e4f1 --- /dev/null +++ b/aio/content/examples/observables/src/geolocation.ts @@ -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 diff --git a/aio/content/examples/observables/src/main.ts b/aio/content/examples/observables/src/main.ts new file mode 100644 index 0000000000..31bacebf11 --- /dev/null +++ b/aio/content/examples/observables/src/main.ts @@ -0,0 +1,5 @@ + +import './geolocation'; +import './subscribing'; +import './creating'; +import './multicasting'; diff --git a/aio/content/examples/observables/src/multicasting.ts b/aio/content/examples/observables/src/multicasting.ts new file mode 100644 index 0000000000..ec40f27450 --- /dev/null +++ b/aio/content/examples/observables/src/multicasting.ts @@ -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 diff --git a/aio/content/examples/observables/src/subscribing.ts b/aio/content/examples/observables/src/subscribing.ts new file mode 100644 index 0000000000..9b6c9ca2f1 --- /dev/null +++ b/aio/content/examples/observables/src/subscribing.ts @@ -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 diff --git a/aio/content/examples/practical-observable-usage/example-config.json b/aio/content/examples/practical-observable-usage/example-config.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aio/content/examples/practical-observable-usage/src/backoff.ts b/aio/content/examples/practical-observable-usage/src/backoff.ts new file mode 100644 index 0000000000..a7174a200a --- /dev/null +++ b/aio/content/examples/practical-observable-usage/src/backoff.ts @@ -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) { + // ... +} diff --git a/aio/content/examples/practical-observable-usage/src/main.ts b/aio/content/examples/practical-observable-usage/src/main.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aio/content/examples/practical-observable-usage/src/typeahead.ts b/aio/content/examples/practical-observable-usage/src/typeahead.ts new file mode 100644 index 0000000000..5badfcab6b --- /dev/null +++ b/aio/content/examples/practical-observable-usage/src/typeahead.ts @@ -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 +}); diff --git a/aio/content/examples/rx-library/example-config.json b/aio/content/examples/rx-library/example-config.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aio/content/examples/rx-library/src/error-handling.ts b/aio/content/examples/rx-library/src/error-handling.ts new file mode 100644 index 0000000000..13af36d2cd --- /dev/null +++ b/aio/content/examples/rx-library/src/error-handling.ts @@ -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 diff --git a/aio/content/examples/rx-library/src/main.ts b/aio/content/examples/rx-library/src/main.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aio/content/examples/rx-library/src/naming-convention.ts b/aio/content/examples/rx-library/src/naming-convention.ts new file mode 100644 index 0000000000..a9510a5b73 --- /dev/null +++ b/aio/content/examples/rx-library/src/naming-convention.ts @@ -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; + + start() { + this.stopwatchValue$.subscribe(num => + this.stopwatchValue = num + ); + } +} diff --git a/aio/content/examples/rx-library/src/operators.1.ts b/aio/content/examples/rx-library/src/operators.1.ts new file mode 100644 index 0000000000..f33b88f2a8 --- /dev/null +++ b/aio/content/examples/rx-library/src/operators.1.ts @@ -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 + + diff --git a/aio/content/examples/rx-library/src/operators.2.ts b/aio/content/examples/rx-library/src/operators.2.ts new file mode 100644 index 0000000000..4e38c8eb6d --- /dev/null +++ b/aio/content/examples/rx-library/src/operators.2.ts @@ -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 diff --git a/aio/content/examples/rx-library/src/operators.ts b/aio/content/examples/rx-library/src/operators.ts new file mode 100644 index 0000000000..530c018e78 --- /dev/null +++ b/aio/content/examples/rx-library/src/operators.ts @@ -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 diff --git a/aio/content/examples/rx-library/src/retry-on-error.ts b/aio/content/examples/rx-library/src/retry-on-error.ts new file mode 100644 index 0000000000..aad18b761c --- /dev/null +++ b/aio/content/examples/rx-library/src/retry-on-error.ts @@ -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 diff --git a/aio/content/examples/rx-library/src/simple-creation.ts b/aio/content/examples/rx-library/src/simple-creation.ts new file mode 100644 index 0000000000..49da15fac5 --- /dev/null +++ b/aio/content/examples/rx-library/src/simple-creation.ts @@ -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 + + diff --git a/aio/content/guide/comparing-observables.md b/aio/content/guide/comparing-observables.md new file mode 100644 index 0000000000..9cb663cd56 --- /dev/null +++ b/aio/content/guide/comparing-observables.md @@ -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. + + +// declare a publishing operation +new Observable((observer) => { subscriber_fn }); +// initiate execution +observable.subscribe(() => { + // observer handles notifications + }); + + +* 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. + + +// initiate execution +new Promise((resolve, reject) => { executer_fn }); +// handle return value +promise.then((value) => { + // handle result here + }); + + +### Chaining + +* Observables differentiate between transformation function such as a map and subscription. Only subscription activates the subscriber function to start computing the values. + + +observable.map((v) => 2*v); + + +* Promises do not differentiate between the last `.then` clauses (equivalent to subscription) and intermediate `.then` clauses (equivalent to map). + + +promise.then((v) => 2*v); + + +### Cancellation + +* Observable subscriptions are cancellable. Unsubscribing removes the listener from receiving further values, and notifies the subscriber function to cancel work. + + +const sub = obs.subscribe(...); +sub.unsubscribe(); + + +* 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. + + +obs.subscribe(() => { + throw Error('my error'); +}); + + +* Promises push errors to the child promises. + + +promise.then(() => { + throw Error('my error'); +}); + + +### Cheat sheet + +The following code snippets illustrate how the same kind of operation is defined using observables and promises. + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ OperationObservablePromise
Creation +
new Observable((observer) => {
+  observer.next(123);
+});
+
+
new Promise((resolve, reject) => {
+  resolve(123);
+});
+
Transform
obs.map((value) => value * 2 );
promise.then((value) => value * 2);
Subscribe +
sub = obs.subscribe((value) => {
+  console.log(value)
+});
+
+
promise.then((value) => {
+  console.log(value);
+});
+
Unsubscribe
sub.unsubscribe();
Implied by promise resolution.
+ +## 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. + + + + + + + + + + + + + + + + + + + + +
+ ObservableEvents API
Creation & cancellation +
// Setup
+let clicks$ = fromEvent(buttonEl, ‘click’);
+// Begin listening
+let subscription = clicks$
+  .subscribe(e => console.log(‘Clicked’, e))
+// Stop listening
+subscription.unsubscribe();
+
+
function handler(e) {
+  console.log(‘Clicked’, e);
+}
+
+// Setup & begin listening
+button.addEventListener(‘click’, handler);
+// Stop listening
+button.removeEventListener(‘click’, handler);
+
+
Subscription +
observable.subscribe(() => {
+  // notification handlers here
+});
+
+
element.addEventListener(eventName, (event) => {
+  // notification handler here
+});
+
ConfigurationListen for keystrokes, but provide a stream representing the value in the input. +
fromEvent(inputEl, 'keydown').pipe(
+  map(e => e.target.value)
+);
+
Does not support configuration. +
element.addEventListener(eventName, (event) => {
+  // Cannot change the passed Event into another
+  // value before it gets to the handler
+});
+
+ + +## 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. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ObservableArray
Given +
obs: ➞1➞2➞3➞5➞7
+
obsB: ➞'a'➞'b'➞'c'
+
+
arr: [1, 2, 3, 5, 7]
+
arrB: ['a', 'b', 'c']
+
concat()
+
obs.concat(obsB)
+
➞1➞2➞3➞5➞7➞'a'➞'b'➞'c'
+
+
arr.concat(arrB)
+
[1,2,3,5,7,'a','b','c']
+
filter()
+
obs.filter((v) => v>3)
+
➞5➞7
+
+
arr.filter((v) => v>3)
+
[5, 7]
+
find()
+
obs.find((v) => v>3)
+
➞5
+
+
arr.find((v) => v>10)
+
5
+
findIndex()
+
obs.findIndex((v) => v>3)
+
➞3
+
+
arr.findIndex((v) => v>3)
+
3
+
forEach()
+
obs.forEach((v) => {
+  console.log(v);
+})
+1
+2
+3
+4
+5
+
+
arr.forEach((v) => {
+  console.log(v);
+})
+1
+2
+3
+4
+5
+
map()
+
obs.map((v) => -v)
+
➞-1➞-2➞-3➞-5➞-7
+
+
arr.map((v) => -v)
+
[-1, -2, -3, -5, -7]
+
reduce()
+
obs.scan((s,v)=> s+v, 0)
+
➞1➞3➞6➞11➞18
+
+
arr.reduce((s,v) => s+v, 0)
+
18
+
+ + + diff --git a/aio/content/guide/observables-in-angular.md b/aio/content/guide/observables-in-angular.md new file mode 100644 index 0000000000..858e9c8dc5 --- /dev/null +++ b/aio/content/guide/observables-in-angular.md @@ -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: + +`` + +Here is the component definition: + + + +## HTTP +Angular’s `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. + + + +## 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: + + + +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: + + + +## 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: + + diff --git a/aio/content/guide/observables.md b/aio/content/guide/observables.md new file mode 100644 index 0000000000..abc4bd005a --- /dev/null +++ b/aio/content/guide/observables.md @@ -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—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—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. + + + +## 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. + +
+ + 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)`—Returns an `Observable` instance that synchronously delivers the values provided as arguments. + * `Observable.from(iterable)`—Converts its argument to an `Observable` instance. This method is commonly used to convert an array to an observable. + +
+ +Here's an example of creating and subscribing to a simple observable, with an observer that logs the received message to the console: + + + +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: + + + +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 observable’s `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: + + + +To take this example a little further, we can create an observable that publishes events. In this example, the subscriber function is defined inline. + + + +Now you can use this function to create an observable that publishes keydown events: + + + +## 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—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. + +Let’s look at an example that counts from 1 to 3, with a one-second delay after each number emitted. + + + +Notice that if you subscribe twice, there will be two separate streams, each emitting values every second. It looks something like this: + + + + Changing the observable to be multicasting could look something like this: + + + +
+ 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. +
+ +## 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. + + +myObservable.subscribe({ + next(num) { console.log('Next num: ' + num)}, + error(err) { console.log('Received an errror: ' + err)} +}); + + +Error handling (and specifically recovering from an error) is covered in more detail in a later section. diff --git a/aio/content/guide/practical-observable-usage.md b/aio/content/guide/practical-observable-usage.md new file mode 100644 index 0000000000..9c0f4ba4ee --- /dev/null +++ b/aio/content/guide/practical-observable-usage.md @@ -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 it’s a minimum length. +* Debounce (so as not to send off API requests for every keystroke, but instead wait for a break in keystrokes). +* Don’t 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: + + + +## 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: + + diff --git a/aio/content/guide/rx-library.md b/aio/content/guide/rx-library.md new file mode 100644 index 0000000000..c0b1418663 --- /dev/null +++ b/aio/content/guide/rx-library.md @@ -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: + + + + + + + + + + +## 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 observable’s emitted values, transforms them, and returns a new observable of those transformed values. Here is a simple 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—that is, a set of instructions for producing the values you’re interested in. By itself, the recipe doesn’t do anything. You need to call `subscribe()` to produce a result through the recipe. + +Here’s an example: + + + +The `pipe()` function is also a method on the RxJS `Observable`, so you use this shorter form to define the same operation: + + + +### 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. + +
+ Note that, for Angular apps, we prefer combining operators with pipes, rather than chaining. Chaining is used in many RxJS examples. +
+ +| 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 doesn’t 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: + + + +### 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: + + + +
+ + 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. + +
+ +## 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: + + \ No newline at end of file diff --git a/aio/content/guide/testing-observables.md b/aio/content/guide/testing-observables.md new file mode 100644 index 0000000000..bb498d8736 --- /dev/null +++ b/aio/content/guide/testing-observables.md @@ -0,0 +1,3 @@ +# Testing + +TBD. Original content [here](https://docs.google.com/document/d/1gGP5sqWNCHAWWV_GLdZQ1XyMO4K-CHksUxux0BFtVxk/edit#heading=h.ohqykkhzdhb2). \ No newline at end of file diff --git a/aio/content/navigation.json b/aio/content/navigation.json index 0844940d75..243524326f 100644 --- a/aio/content/navigation.json +++ b/aio/content/navigation.json @@ -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", "title": "Bootstrapping",