refactor(aio): switch to pipeable RxJS operators (#22872)

PR Close #22872
This commit is contained in:
George Kalpakas 2018-03-21 03:40:23 +02:00 committed by Matias Niemelä
parent da76db9601
commit 01d2dd2a3a
24 changed files with 196 additions and 192 deletions

View File

@ -3,7 +3,7 @@
"master": { "master": {
"uncompressed": { "uncompressed": {
"inline": 1971, "inline": 1971,
"main": 595662, "main": 584136,
"polyfills": 40272, "polyfills": 40272,
"prettify": 14888 "prettify": 14888
} }

View File

@ -7,7 +7,7 @@ import { MatProgressBar, MatSidenav } from '@angular/material';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { Observable, timer } from 'rxjs'; import { Observable, timer } from 'rxjs';
import 'rxjs/add/operator/mapTo'; import { mapTo } from 'rxjs/operators';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
import { AppModule } from './app.module'; import { AppModule } from './app.module';
@ -1371,6 +1371,6 @@ class TestHttpClient {
} }
// Preserve async nature of `HttpClient`. // Preserve async nature of `HttpClient`.
return timer(1).mapTo(data); return timer(1).pipe(mapTo(data));
} }
} }

View File

@ -14,7 +14,7 @@ import { SearchService } from 'app/search/search.service';
import { TocService } from 'app/shared/toc.service'; import { TocService } from 'app/shared/toc.service';
import { BehaviorSubject, combineLatest, Observable } from 'rxjs'; import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
import 'rxjs/add/operator/first'; import { first, map } from 'rxjs/operators';
const sideNavView = 'SideNav'; const sideNavView = 'SideNav';
@ -143,7 +143,7 @@ export class AppComponent implements OnInit {
// Compute the version picker list from the current version and the versions in the navigation map // Compute the version picker list from the current version and the versions in the navigation map
combineLatest( combineLatest(
this.navigationService.versionInfo, this.navigationService.versionInfo,
this.navigationService.navigationViews.map(views => views['docVersions'])) this.navigationService.navigationViews.pipe(map(views => views['docVersions'])))
.subscribe(([versionInfo, versions]) => { .subscribe(([versionInfo, versions]) => {
// TODO(pbd): consider whether we can lookup the stable and next versions from the internet // TODO(pbd): consider whether we can lookup the stable and next versions from the internet
const computedVersions: NavigationNode[] = [ const computedVersions: NavigationNode[] = [
@ -171,7 +171,7 @@ export class AppComponent implements OnInit {
this.navigationService.versionInfo.subscribe(vi => this.versionInfo = vi); this.navigationService.versionInfo.subscribe(vi => this.versionInfo = vi);
const hasNonEmptyToc = this.tocService.tocList.map(tocList => tocList.length > 0); const hasNonEmptyToc = this.tocService.tocList.pipe(map(tocList => tocList.length > 0));
combineLatest(hasNonEmptyToc, this.showFloatingToc) combineLatest(hasNonEmptyToc, this.showFloatingToc)
.subscribe(([hasToc, showFloatingToc]) => this.hasFloatingToc = hasToc && showFloatingToc); .subscribe(([hasToc, showFloatingToc]) => this.hasFloatingToc = hasToc && showFloatingToc);
@ -183,7 +183,7 @@ export class AppComponent implements OnInit {
combineLatest( combineLatest(
this.documentService.currentDocument, // ...needed to determine host classes this.documentService.currentDocument, // ...needed to determine host classes
this.navigationService.currentNodes) // ...needed to determine `sidenav` state this.navigationService.currentNodes) // ...needed to determine `sidenav` state
.first() .pipe(first())
.subscribe(() => this.updateShell()); .subscribe(() => this.updateShell());
} }

View File

@ -1,5 +1,6 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { catchError, map } from 'rxjs/operators';
import { Logger } from 'app/shared/logger.service'; import { Logger } from 'app/shared/logger.service';
import { CONTENT_URL_PREFIX } from 'app/documents/document.service'; import { CONTENT_URL_PREFIX } from 'app/documents/document.service';
const announcementsPath = CONTENT_URL_PREFIX + 'announcements.json'; const announcementsPath = CONTENT_URL_PREFIX + 'announcements.json';
@ -58,15 +59,17 @@ export class AnnouncementBarComponent implements OnInit {
ngOnInit() { ngOnInit() {
this.http.get<Announcement[]>(announcementsPath) this.http.get<Announcement[]>(announcementsPath)
.catch(error => { .pipe(
this.logger.error(new Error(`${announcementsPath} request failed: ${error.message}`)); catchError(error => {
return []; this.logger.error(new Error(`${announcementsPath} request failed: ${error.message}`));
}) return [];
.map(announcements => this.findCurrentAnnouncement(announcements)) }),
.catch(error => { map(announcements => this.findCurrentAnnouncement(announcements)),
this.logger.error(new Error(`${announcementsPath} contains invalid data: ${error.message}`)); catchError(error => {
return []; this.logger.error(new Error(`${announcementsPath} contains invalid data: ${error.message}`));
}) return [];
}),
)
.subscribe(announcement => this.announcement = announcement); .subscribe(announcement => this.announcement = announcement);
} }

View File

@ -2,9 +2,7 @@ import { Injectable, OnDestroy } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http'; import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { ReplaySubject, Subject } from 'rxjs'; import { ReplaySubject, Subject } from 'rxjs';
import 'rxjs/add/operator/do'; import { takeUntil, tap } from 'rxjs/operators';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/takeUntil';
import { Logger } from 'app/shared/logger.service'; import { Logger } from 'app/shared/logger.service';
import { DOC_CONTENT_URL_PREFIX } from 'app/documents/document.service'; import { DOC_CONTENT_URL_PREFIX } from 'app/documents/document.service';
@ -34,7 +32,7 @@ export class ApiService implements OnDestroy {
private firstTime = true; private firstTime = true;
private onDestroy = new Subject(); private onDestroy = new Subject();
private sectionsSubject = new ReplaySubject<ApiSection[]>(1); private sectionsSubject = new ReplaySubject<ApiSection[]>(1);
private _sections = this.sectionsSubject.takeUntil(this.onDestroy); private _sections = this.sectionsSubject.pipe(takeUntil(this.onDestroy));
/** /**
* Return a cached observable of API sections from a JSON file. * Return a cached observable of API sections from a JSON file.
@ -70,8 +68,10 @@ export class ApiService implements OnDestroy {
// TODO: get URL by configuration? // TODO: get URL by configuration?
const url = this.apiBase + (src || this.apiListJsonDefault); const url = this.apiBase + (src || this.apiListJsonDefault);
this.http.get<ApiSection[]>(url) this.http.get<ApiSection[]>(url)
.takeUntil(this.onDestroy) .pipe(
.do(() => this.logger.log(`Got API sections from ${url}`)) takeUntil(this.onDestroy),
tap(() => this.logger.log(`Got API sections from ${url}`)),
)
.subscribe( .subscribe(
sections => this.sectionsSubject.next(sections), sections => this.sectionsSubject.next(sections),
(err: HttpErrorResponse) => { (err: HttpErrorResponse) => {

View File

@ -3,8 +3,7 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { MatSnackBar } from '@angular/material'; import { MatSnackBar } from '@angular/material';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import 'rxjs/add/operator/first'; import { first } from 'rxjs/operators';
import 'rxjs/add/operator/toPromise';
import { CodeComponent } from './code.component'; import { CodeComponent } from './code.component';
import { CodeModule } from './code.module'; import { CodeModule } from './code.module';
@ -65,7 +64,7 @@ describe('CodeComponent', () => {
describe('pretty printing', () => { describe('pretty printing', () => {
const untilCodeFormatted = () => { const untilCodeFormatted = () => {
const emitter = hostComponent.codeComponent.codeFormatted; const emitter = hostComponent.codeComponent.codeFormatted;
return emitter.first().toPromise(); return emitter.pipe(first()).toPromise();
}; };
const hasLineNumbers = async () => { const hasLineNumbers = async () => {
// presence of `<li>`s are a tell-tale for line numbers // presence of `<li>`s are a tell-tale for line numbers

View File

@ -3,7 +3,7 @@ import { Logger } from 'app/shared/logger.service';
import { PrettyPrinter } from './pretty-printer.service'; import { PrettyPrinter } from './pretty-printer.service';
import { CopierService } from 'app/shared/copier.service'; import { CopierService } from 'app/shared/copier.service';
import { MatSnackBar } from '@angular/material/snack-bar'; import { MatSnackBar } from '@angular/material/snack-bar';
import 'rxjs/add/operator/do'; import { tap } from 'rxjs/operators';
/** /**
* If linenums is not set, this is the default maximum number of lines that * If linenums is not set, this is the default maximum number of lines that
@ -120,7 +120,7 @@ export class CodeComponent implements OnChanges {
this.pretty this.pretty
.formatCode(leftAlignedCode, this.language, this.getLinenums(leftAlignedCode)) .formatCode(leftAlignedCode, this.language, this.getLinenums(leftAlignedCode))
.do(() => this.codeFormatted.emit()) .pipe(tap(() => this.codeFormatted.emit()))
.subscribe(c => this.setCodeHtml(c), err => { /* ignore failure to format */ } .subscribe(c => this.setCodeHtml(c), err => { /* ignore failure to format */ }
); );
} }

View File

@ -1,8 +1,7 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { from as fromPromise, Observable } from 'rxjs'; import { from as fromPromise, Observable } from 'rxjs';
import 'rxjs/add/operator/map'; import { first, map, share } from 'rxjs/operators';
import 'rxjs/add/operator/first';
import { Logger } from 'app/shared/logger.service'; import { Logger } from 'app/shared/logger.service';
@ -21,7 +20,7 @@ export class PrettyPrinter {
private prettyPrintOne: Observable<PrettyPrintOne>; private prettyPrintOne: Observable<PrettyPrintOne>;
constructor(private logger: Logger) { constructor(private logger: Logger) {
this.prettyPrintOne = fromPromise(this.getPrettyPrintOne()).share(); this.prettyPrintOne = fromPromise(this.getPrettyPrintOne()).pipe(share());
} }
private getPrettyPrintOne(): Promise<PrettyPrintOne> { private getPrettyPrintOne(): Promise<PrettyPrintOne> {
@ -50,15 +49,17 @@ export class PrettyPrinter {
* @returns Observable<string> - Observable of formatted code * @returns Observable<string> - Observable of formatted code
*/ */
formatCode(code: string, language?: string, linenums?: number | boolean) { formatCode(code: string, language?: string, linenums?: number | boolean) {
return this.prettyPrintOne.map(ppo => { return this.prettyPrintOne.pipe(
try { map(ppo => {
return ppo(code, language, linenums); try {
} catch (err) { return ppo(code, language, linenums);
const msg = `Could not format code that begins '${code.substr(0, 50)}...'.`; } catch (err) {
console.error(msg, err); const msg = `Could not format code that begins '${code.substr(0, 50)}...'.`;
throw new Error(msg); console.error(msg, err);
} throw new Error(msg);
}) }
.first(); // complete immediately }),
first(), // complete immediately
);
} }
} }

View File

@ -1,9 +1,8 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs'; import { ConnectableObservable, Observable } from 'rxjs';
import 'rxjs/add/operator/map'; import { map, publishLast } from 'rxjs/operators';
import 'rxjs/add/operator/publishLast';
import { Contributor, ContributorGroup } from './contributors.model'; import { Contributor, ContributorGroup } from './contributors.model';
@ -22,9 +21,9 @@ export class ContributorService {
} }
private getContributors() { private getContributors() {
const contributors = this.http.get<{[key: string]: Contributor}>(contributorsPath) const contributors = this.http.get<{[key: string]: Contributor}>(contributorsPath).pipe(
// Create group map // Create group map
.map(contribs => { map(contribs => {
const contribMap: { [name: string]: Contributor[]} = {}; const contribMap: { [name: string]: Contributor[]} = {};
Object.keys(contribs).forEach(key => { Object.keys(contribs).forEach(key => {
const contributor = contribs[key]; const contributor = contribs[key];
@ -38,10 +37,10 @@ export class ContributorService {
}); });
return contribMap; return contribMap;
}) }),
// Flatten group map into sorted group array of sorted contributors // Flatten group map into sorted group array of sorted contributors
.map(cmap => { map(cmap => {
return Object.keys(cmap).map(key => { return Object.keys(cmap).map(key => {
const order = knownGroups.indexOf(key); const order = knownGroups.indexOf(key);
return { return {
@ -51,10 +50,12 @@ export class ContributorService {
} as ContributorGroup; } as ContributorGroup;
}) })
.sort(compareGroups); .sort(compareGroups);
}) }),
.publishLast();
contributors.connect(); publishLast(),
);
(contributors as ConnectableObservable<ContributorGroup[]>).connect();
return contributors; return contributors;
} }
} }

View File

@ -1,9 +1,8 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs'; import { ConnectableObservable, Observable } from 'rxjs';
import 'rxjs/add/operator/map'; import { map, publishLast } from 'rxjs/operators';
import 'rxjs/add/operator/publishLast';
import { Category, Resource, SubCategory } from './resource.model'; import { Category, Resource, SubCategory } from './resource.model';
import { CONTENT_URL_PREFIX } from 'app/documents/document.service'; import { CONTENT_URL_PREFIX } from 'app/documents/document.service';
@ -20,11 +19,12 @@ export class ResourceService {
private getCategories(): Observable<Category[]> { private getCategories(): Observable<Category[]> {
const categories = this.http.get<any>(resourcesPath) const categories = this.http.get<any>(resourcesPath).pipe(
.map(data => mkCategories(data)) map(data => mkCategories(data)),
.publishLast(); publishLast(),
);
categories.connect(); (categories as ConnectableObservable<Category[]>).connect();
return categories; return categories;
}; };
} }

View File

@ -1,5 +1,6 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { LocationService } from 'app/shared/location.service'; import { LocationService } from 'app/shared/location.service';
import { SearchResults } from 'app/search/interfaces'; import { SearchResults } from 'app/search/interfaces';
import { SearchService } from 'app/search/search.service'; import { SearchService } from 'app/search/search.service';
@ -15,9 +16,9 @@ export class FileNotFoundSearchComponent implements OnInit {
constructor(private location: LocationService, private search: SearchService) {} constructor(private location: LocationService, private search: SearchService) {}
ngOnInit() { ngOnInit() {
this.searchResults = this.location.currentPath.switchMap(path => { this.searchResults = this.location.currentPath.pipe(switchMap(path => {
const query = path.split(/\W+/).join(' '); const query = path.split(/\W+/).join(' ');
return this.search.search(query); return this.search.search(query);
}); }));
} }
} }

View File

@ -2,8 +2,7 @@ import { Injectable } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http'; import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { AsyncSubject, Observable, of } from 'rxjs'; import { AsyncSubject, Observable, of } from 'rxjs';
import 'rxjs/add/operator/catch'; import { catchError, switchMap, tap } from 'rxjs/operators';
import 'rxjs/add/operator/switchMap';
import { DocumentContents } from './document-contents'; import { DocumentContents } from './document-contents';
export { DocumentContents } from './document-contents'; export { DocumentContents } from './document-contents';
@ -41,7 +40,7 @@ export class DocumentService {
private http: HttpClient, private http: HttpClient,
location: LocationService) { location: LocationService) {
// Whenever the URL changes we try to get the appropriate doc // Whenever the URL changes we try to get the appropriate doc
this.currentDocument = location.currentPath.switchMap(path => this.getDocument(path)); this.currentDocument = location.currentPath.pipe(switchMap(path => this.getDocument(path)));
} }
private getDocument(url: string) { private getDocument(url: string) {
@ -60,15 +59,17 @@ export class DocumentService {
this.logger.log('fetching document from', requestPath); this.logger.log('fetching document from', requestPath);
this.http this.http
.get<DocumentContents>(requestPath, {responseType: 'json'}) .get<DocumentContents>(requestPath, {responseType: 'json'})
.do(data => { .pipe(
if (!data || typeof data !== 'object') { tap(data => {
this.logger.log('received invalid data:', data); if (!data || typeof data !== 'object') {
throw Error('Invalid data'); this.logger.log('received invalid data:', data);
} throw Error('Invalid data');
}) }
.catch((error: HttpErrorResponse) => { }),
return error.status === 404 ? this.getFileNotFoundDoc(id) : this.getErrorDoc(id, error); catchError((error: HttpErrorResponse) => {
}) return error.status === 404 ? this.getFileNotFoundDoc(id) : this.getErrorDoc(id, error);
}),
)
.subscribe(subject); .subscribe(subject);
return subject.asObservable(); return subject.asObservable();
@ -90,7 +91,7 @@ export class DocumentService {
private getErrorDoc(id: string, error: HttpErrorResponse): Observable<DocumentContents> { private getErrorDoc(id: string, error: HttpErrorResponse): Observable<DocumentContents> {
this.logger.error(new Error(`Error fetching document '${id}': (${error.message})`)); this.logger.error(new Error(`Error fetching document '${id}': (${error.message})`));
this.cache.delete(id); this.cache.delete(id);
return Observable.of({ return of({
id: FETCHING_ERROR_ID, id: FETCHING_ERROR_ID,
contents: FETCHING_ERROR_CONTENTS contents: FETCHING_ERROR_CONTENTS
}); });

View File

@ -2,10 +2,7 @@ import { Component, ElementRef, EventEmitter, Input, OnDestroy, Output } from '@
import { Title, Meta } from '@angular/platform-browser'; import { Title, Meta } from '@angular/platform-browser';
import { Observable, of, timer } from 'rxjs'; import { Observable, of, timer } from 'rxjs';
import 'rxjs/add/operator/catch'; import { catchError, switchMap, takeUntil, tap } from 'rxjs/operators';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/switchMap';
import 'rxjs/add/operator/takeUntil';
import { DocumentContents, FILE_NOT_FOUND_ID, FETCHING_ERROR_ID } from 'app/documents/document.service'; import { DocumentContents, FILE_NOT_FOUND_ID, FETCHING_ERROR_ID } from 'app/documents/document.service';
import { Logger } from 'app/shared/logger.service'; import { Logger } from 'app/shared/logger.service';
@ -80,8 +77,10 @@ export class DocViewerComponent implements OnDestroy {
} }
this.docContents$ this.docContents$
.switchMap(newDoc => this.render(newDoc)) .pipe(
.takeUntil(this.onDestroy$) switchMap(newDoc => this.render(newDoc)),
takeUntil(this.onDestroy$),
)
.subscribe(); .subscribe();
} }
@ -132,22 +131,23 @@ export class DocViewerComponent implements OnDestroy {
this.setNoIndex(doc.id === FILE_NOT_FOUND_ID || doc.id === FETCHING_ERROR_ID); this.setNoIndex(doc.id === FILE_NOT_FOUND_ID || doc.id === FETCHING_ERROR_ID);
return this.void$ return this.void$.pipe(
// Security: `doc.contents` is always authored by the documentation team // Security: `doc.contents` is always authored by the documentation team
// and is considered to be safe. // and is considered to be safe.
.do(() => this.nextViewContainer.innerHTML = doc.contents || '') tap(() => this.nextViewContainer.innerHTML = doc.contents || ''),
.do(() => addTitleAndToc = this.prepareTitleAndToc(this.nextViewContainer, doc.id)) tap(() => addTitleAndToc = this.prepareTitleAndToc(this.nextViewContainer, doc.id)),
.switchMap(() => this.elementsLoader.loadContainingCustomElements(this.nextViewContainer)) switchMap(() => this.elementsLoader.loadContainingCustomElements(this.nextViewContainer)),
.do(() => this.docReady.emit()) tap(() => this.docReady.emit()),
.switchMap(() => this.swapViews(addTitleAndToc)) switchMap(() => this.swapViews(addTitleAndToc)),
.do(() => this.docRendered.emit()) tap(() => this.docRendered.emit()),
.catch(err => { catchError(err => {
const errorMessage = (err instanceof Error) ? err.stack : err; const errorMessage = (err instanceof Error) ? err.stack : err;
this.logger.error(new Error(`[DocViewer] Error preparing document '${doc.id}': ${errorMessage}`)); this.logger.error(new Error(`[DocViewer] Error preparing document '${doc.id}': ${errorMessage}`));
this.nextViewContainer.innerHTML = ''; this.nextViewContainer.innerHTML = '';
this.setNoIndex(true); this.setNoIndex(true);
return this.void$; return this.void$;
}); }),
);
} }
/** /**
@ -199,16 +199,17 @@ export class DocViewerComponent implements OnDestroy {
} }
elem.style.transition = ''; elem.style.transition = '';
return animationsDisabled return animationsDisabled
? this.void$.do(() => elem.style[prop] = to) ? this.void$.pipe(tap(() => elem.style[prop] = to))
: this.void$ : this.void$.pipe(
// In order to ensure that the `from` value will be applied immediately (i.e. // In order to ensure that the `from` value will be applied immediately (i.e.
// without transition) and that the `to` value will be affected by the // without transition) and that the `to` value will be affected by the
// `transition` style, we need to ensure an animation frame has passed between // `transition` style, we need to ensure an animation frame has passed between
// setting each style. // setting each style.
.switchMap(() => raf$).do(() => elem.style[prop] = from) switchMap(() => raf$), tap(() => elem.style[prop] = from),
.switchMap(() => raf$).do(() => elem.style.transition = `all ${duration}ms ease-in-out`) switchMap(() => raf$), tap(() => elem.style.transition = `all ${duration}ms ease-in-out`),
.switchMap(() => raf$).do(() => (elem.style as any)[prop] = to) switchMap(() => raf$), tap(() => (elem.style as any)[prop] = to),
.switchMap(() => timer(getActualDuration(elem))).switchMap(() => this.void$); switchMap(() => timer(getActualDuration(elem))), switchMap(() => this.void$),
);
}; };
const animateLeave = (elem: HTMLElement) => animateProp(elem, 'opacity', '1', '0.1'); const animateLeave = (elem: HTMLElement) => animateProp(elem, 'opacity', '1', '0.1');
@ -217,25 +218,27 @@ export class DocViewerComponent implements OnDestroy {
let done$ = this.void$; let done$ = this.void$;
if (this.currViewContainer.parentElement) { if (this.currViewContainer.parentElement) {
done$ = done$ done$ = done$.pipe(
// Remove the current view from the viewer. // Remove the current view from the viewer.
.switchMap(() => animateLeave(this.currViewContainer)) switchMap(() => animateLeave(this.currViewContainer)),
.do(() => this.currViewContainer.parentElement!.removeChild(this.currViewContainer)) tap(() => this.currViewContainer.parentElement!.removeChild(this.currViewContainer)),
.do(() => this.docRemoved.emit()); tap(() => this.docRemoved.emit()),
);
} }
return done$ return done$.pipe(
// Insert the next view into the viewer. // Insert the next view into the viewer.
.do(() => this.hostElement.appendChild(this.nextViewContainer)) tap(() => this.hostElement.appendChild(this.nextViewContainer)),
.do(() => onInsertedCb()) tap(() => onInsertedCb()),
.do(() => this.docInserted.emit()) tap(() => this.docInserted.emit()),
.switchMap(() => animateEnter(this.nextViewContainer)) switchMap(() => animateEnter(this.nextViewContainer)),
// Update the view references and clean up unused nodes. // Update the view references and clean up unused nodes.
.do(() => { tap(() => {
const prevViewContainer = this.currViewContainer; const prevViewContainer = this.currViewContainer;
this.currViewContainer = this.nextViewContainer; this.currViewContainer = this.nextViewContainer;
this.nextViewContainer = prevViewContainer; this.nextViewContainer = prevViewContainer;
this.nextViewContainer.innerHTML = ''; // Empty to release memory. this.nextViewContainer.innerHTML = ''; // Empty to release memory.
}); }),
);
} }
} }

View File

@ -1,8 +1,6 @@
import { AfterViewInit, Component, ElementRef, OnDestroy, OnInit, QueryList, ViewChildren } from '@angular/core'; import { AfterViewInit, Component, ElementRef, OnDestroy, OnInit, QueryList, ViewChildren } from '@angular/core';
import { asapScheduler as asap, Observable, Subject } from 'rxjs'; import { asapScheduler as asap, combineLatest, Subject } from 'rxjs';
import 'rxjs/add/observable/combineLatest'; import { startWith, subscribeOn, takeUntil } from 'rxjs/operators';
import 'rxjs/add/operator/subscribeOn';
import 'rxjs/add/operator/takeUntil';
import { ScrollService } from 'app/shared/scroll.service'; import { ScrollService } from 'app/shared/scroll.service';
import { TocItem, TocService } from 'app/shared/toc.service'; import { TocItem, TocService } from 'app/shared/toc.service';
@ -34,7 +32,7 @@ export class TocComponent implements OnInit, AfterViewInit, OnDestroy {
ngOnInit() { ngOnInit() {
this.tocService.tocList this.tocService.tocList
.takeUntil(this.onDestroy) .pipe(takeUntil(this.onDestroy))
.subscribe(tocList => { .subscribe(tocList => {
this.tocList = tocList; this.tocList = tocList;
const itemCount = count(this.tocList, item => item.level !== 'h1'); const itemCount = count(this.tocList, item => item.level !== 'h1');
@ -54,8 +52,8 @@ export class TocComponent implements OnInit, AfterViewInit, OnDestroy {
// We use the `asap` scheduler because updates to `activeItemIndex` are triggered by DOM changes, // We use the `asap` scheduler because updates to `activeItemIndex` are triggered by DOM changes,
// which, in turn, are caused by the rendering that happened due to a ChangeDetection. // which, in turn, are caused by the rendering that happened due to a ChangeDetection.
// Without asap, we would be updating the model while still in a ChangeDetection handler, which is disallowed by Angular. // Without asap, we would be updating the model while still in a ChangeDetection handler, which is disallowed by Angular.
Observable.combineLatest(this.tocService.activeItemIndex.subscribeOn(asap), this.items.changes.startWith(this.items)) combineLatest(this.tocService.activeItemIndex.pipe(subscribeOn(asap)), this.items.changes.pipe(startWith(this.items)))
.takeUntil(this.onDestroy) .pipe(takeUntil(this.onDestroy))
.subscribe(([index, items]) => { .subscribe(([index, items]) => {
this.activeIndex = index; this.activeIndex = index;
if (index === null || index >= items.length) { if (index === null || index >= items.length) {

View File

@ -1,10 +1,8 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { combineLatest, Observable } from 'rxjs'; import { combineLatest, ConnectableObservable, Observable } from 'rxjs';
import 'rxjs/add/operator/map'; import { map, publishLast, publishReplay } from 'rxjs/operators';
import 'rxjs/add/operator/publishLast';
import 'rxjs/add/operator/publishReplay';
import { LocationService } from 'app/shared/location.service'; import { LocationService } from 'app/shared/location.service';
import { CONTENT_URL_PREFIX } from 'app/documents/document.service'; import { CONTENT_URL_PREFIX } from 'app/documents/document.service';
@ -56,30 +54,32 @@ export class NavigationService {
*/ */
private fetchNavigationInfo(): Observable<NavigationResponse> { private fetchNavigationInfo(): Observable<NavigationResponse> {
const navigationInfo = this.http.get<NavigationResponse>(navigationPath) const navigationInfo = this.http.get<NavigationResponse>(navigationPath)
.publishLast(); .pipe(publishLast());
navigationInfo.connect(); (navigationInfo as ConnectableObservable<NavigationResponse>).connect();
return navigationInfo; return navigationInfo;
} }
private getVersionInfo(navigationInfo: Observable<NavigationResponse>) { private getVersionInfo(navigationInfo: Observable<NavigationResponse>) {
const versionInfo = navigationInfo const versionInfo = navigationInfo.pipe(
.map(response => response.__versionInfo) map(response => response.__versionInfo),
.publishLast(); publishLast(),
versionInfo.connect(); );
(versionInfo as ConnectableObservable<VersionInfo>).connect();
return versionInfo; return versionInfo;
} }
private getNavigationViews(navigationInfo: Observable<NavigationResponse>): Observable<NavigationViews> { private getNavigationViews(navigationInfo: Observable<NavigationResponse>): Observable<NavigationViews> {
const navigationViews = navigationInfo const navigationViews = navigationInfo.pipe(
.map(response => { map(response => {
const views = Object.assign({}, response); const views = Object.assign({}, response);
Object.keys(views).forEach(key => { Object.keys(views).forEach(key => {
if (key[0] === '_') { delete views[key]; } if (key[0] === '_') { delete views[key]; }
}); });
return views as NavigationViews; return views as NavigationViews;
}) }),
.publishLast(); publishLast(),
navigationViews.connect(); );
(navigationViews as ConnectableObservable<NavigationViews>).connect();
return navigationViews; return navigationViews;
} }
@ -91,15 +91,15 @@ export class NavigationService {
*/ */
private getCurrentNodes(navigationViews: Observable<NavigationViews>): Observable<CurrentNodes> { private getCurrentNodes(navigationViews: Observable<NavigationViews>): Observable<CurrentNodes> {
const currentNodes = combineLatest( const currentNodes = combineLatest(
navigationViews.map(views => this.computeUrlToNavNodesMap(views)), navigationViews.pipe(map(views => this.computeUrlToNavNodesMap(views))),
this.location.currentPath, this.location.currentPath,
(navMap, url) => { (navMap, url) => {
const urlKey = url.startsWith('api/') ? 'api' : url; const urlKey = url.startsWith('api/') ? 'api' : url;
return navMap.get(urlKey) || { '' : { view: '', url: urlKey, nodes: [] }}; return navMap.get(urlKey) || { '' : { view: '', url: urlKey, nodes: [] }};
}) })
.publishReplay(1); .pipe(publishReplay(1));
currentNodes.connect(); (currentNodes as ConnectableObservable<CurrentNodes>).connect();
return currentNodes; return currentNodes;
} }

View File

@ -1,7 +1,7 @@
import { Component, OnInit, ViewChild, ElementRef, EventEmitter, Output } from '@angular/core'; import { Component, OnInit, ViewChild, ElementRef, EventEmitter, Output } from '@angular/core';
import { LocationService } from 'app/shared/location.service'; import { LocationService } from 'app/shared/location.service';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import 'rxjs/add/operator/distinctUntilChanged'; import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
/** /**
* This component provides a text box to type a search query that will be sent to the SearchService. * This component provides a text box to type a search query that will be sent to the SearchService.
@ -30,7 +30,7 @@ export class SearchBoxComponent implements OnInit {
private searchSubject = new Subject<string>(); private searchSubject = new Subject<string>();
@ViewChild('searchBox') searchBox: ElementRef; @ViewChild('searchBox') searchBox: ElementRef;
@Output() onSearch = this.searchSubject.distinctUntilChanged().debounceTime(this.searchDebounce); @Output() onSearch = this.searchSubject.pipe(distinctUntilChanged(), debounceTime(this.searchDebounce));
@Output() onFocus = new EventEmitter<string>(); @Output() onFocus = new EventEmitter<string>();
constructor(private locationService: LocationService) { } constructor(private locationService: LocationService) { }

View File

@ -1,7 +1,6 @@
import { ReflectiveInjector, NgZone } from '@angular/core'; import { ReflectiveInjector, NgZone } from '@angular/core';
import { fakeAsync, tick } from '@angular/core/testing'; import { fakeAsync, tick } from '@angular/core/testing';
import { Observable } from 'rxjs'; import { of } from 'rxjs';
import 'rxjs/add/observable/of';
import { SearchService } from './search.service'; import { SearchService } from './search.service';
import { WebWorkerClient } from 'app/shared/web-worker'; import { WebWorkerClient } from 'app/shared/web-worker';
@ -13,7 +12,7 @@ describe('SearchService', () => {
let mockWorker: WebWorkerClient; let mockWorker: WebWorkerClient;
beforeEach(() => { beforeEach(() => {
sendMessageSpy = jasmine.createSpy('sendMessage').and.returnValue(Observable.of({})); sendMessageSpy = jasmine.createSpy('sendMessage').and.returnValue(of({}));
mockWorker = { sendMessage: sendMessageSpy } as any; mockWorker = { sendMessage: sendMessageSpy } as any;
spyOn(WebWorkerClient, 'create').and.returnValue(mockWorker); spyOn(WebWorkerClient, 'create').and.returnValue(mockWorker);
@ -40,7 +39,7 @@ describe('SearchService', () => {
// We must initialize the service before calling connectSearches // We must initialize the service before calling connectSearches
service.initWorker('some/url', 1000); service.initWorker('some/url', 1000);
// Simulate the index being ready so that searches get sent to the worker // Simulate the index being ready so that searches get sent to the worker
(service as any).ready = Observable.of(true); (service as any).ready = of(true);
}); });
it('should trigger a `loadIndex` synchronously (not waiting for the delay)', () => { it('should trigger a `loadIndex` synchronously (not waiting for the delay)', () => {
@ -57,7 +56,7 @@ describe('SearchService', () => {
it('should push the response to the returned observable', () => { it('should push the response to the returned observable', () => {
const mockSearchResults = { results: ['a', 'b'] }; const mockSearchResults = { results: ['a', 'b'] };
let actualSearchResults: any; let actualSearchResults: any;
(mockWorker.sendMessage as jasmine.Spy).and.returnValue(Observable.of(mockSearchResults)); (mockWorker.sendMessage as jasmine.Spy).and.returnValue(of(mockSearchResults));
service.search('some query').subscribe(results => actualSearchResults = results); service.search('some query').subscribe(results => actualSearchResults = results);
expect(actualSearchResults).toEqual(mockSearchResults); expect(actualSearchResults).toEqual(mockSearchResults);
}); });

View File

@ -5,10 +5,8 @@ can be found in the LICENSE file at http://angular.io/license
*/ */
import { NgZone, Injectable } from '@angular/core'; import { NgZone, Injectable } from '@angular/core';
import { Observable, race, ReplaySubject, timer } from 'rxjs'; import { ConnectableObservable, Observable, race, ReplaySubject, timer } from 'rxjs';
import 'rxjs/add/operator/concatMap'; import { concatMap, first, publishReplay } from 'rxjs/operators';
import 'rxjs/add/operator/first';
import 'rxjs/add/operator/publishReplay';
import { WebWorkerClient } from 'app/shared/web-worker'; import { WebWorkerClient } from 'app/shared/web-worker';
import { SearchResults } from 'app/search/interfaces'; import { SearchResults } from 'app/search/interfaces';
@ -31,16 +29,19 @@ export class SearchService {
// Wait for the initDelay or the first search // Wait for the initDelay or the first search
const ready = this.ready = race<any>( const ready = this.ready = race<any>(
timer(initDelay), timer(initDelay),
(this.searchesSubject.asObservable()).first() this.searchesSubject.asObservable().pipe(first()),
) )
.concatMap(() => { .pipe(
// Create the worker and load the index concatMap(() => {
this.worker = WebWorkerClient.create(workerUrl, this.zone); // Create the worker and load the index
return this.worker.sendMessage<boolean>('load-index'); this.worker = WebWorkerClient.create(workerUrl, this.zone);
}).publishReplay(1); return this.worker.sendMessage<boolean>('load-index');
}),
publishReplay(1),
);
// Connect to the observable to kick off the timer // Connect to the observable to kick off the timer
ready.connect(); (ready as ConnectableObservable<boolean>).connect();
return ready; return ready;
} }
@ -53,6 +54,6 @@ export class SearchService {
// Trigger the searches subject to override the init delay timer // Trigger the searches subject to override the init delay timer
this.searchesSubject.next(query); this.searchesSubject.next(query);
// Once the index has loaded, switch to listening to the searches coming in. // Once the index has loaded, switch to listening to the searches coming in.
return this.ready.concatMap(() => this.worker.sendMessage<SearchResults>('query-index', query)); return this.ready.pipe(concatMap(() => this.worker.sendMessage<SearchResults>('query-index', query)));
} }
} }

View File

@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
import { Location, PlatformLocation } from '@angular/common'; import { Location, PlatformLocation } from '@angular/common';
import { ReplaySubject } from 'rxjs'; import { ReplaySubject } from 'rxjs';
import 'rxjs/add/operator/do'; import { map, tap } from 'rxjs/operators';
import { GaService } from 'app/shared/ga.service'; import { GaService } from 'app/shared/ga.service';
import { SwUpdatesService } from 'app/sw-updates/sw-updates.service'; import { SwUpdatesService } from 'app/sw-updates/sw-updates.service';
@ -15,11 +15,12 @@ export class LocationService {
private swUpdateActivated = false; private swUpdateActivated = false;
currentUrl = this.urlSubject currentUrl = this.urlSubject
.map(url => this.stripSlashes(url)); .pipe(map(url => this.stripSlashes(url)));
currentPath = this.currentUrl currentPath = this.currentUrl.pipe(
.map(url => (url.match(/[^?#]*/) || [])[0]) // strip query and hash map(url => (url.match(/[^?#]*/) || [])[0]), // strip query and hash
.do(path => this.gaService.locationChanged(path)); tap(path => this.gaService.locationChanged(path)),
);
constructor( constructor(
private gaService: GaService, private gaService: GaService,

View File

@ -1,10 +1,7 @@
import { Inject, Injectable } from '@angular/core'; import { Inject, Injectable } from '@angular/core';
import { DOCUMENT } from '@angular/platform-browser'; import { DOCUMENT } from '@angular/platform-browser';
import { Observable, ReplaySubject, Subject } from 'rxjs'; import { fromEvent, Observable, ReplaySubject, Subject } from 'rxjs';
import 'rxjs/add/observable/fromEvent'; import { auditTime, distinctUntilChanged, takeUntil } from 'rxjs/operators';
import 'rxjs/add/operator/auditTime';
import 'rxjs/add/operator/distinctUntilChanged';
import 'rxjs/add/operator/takeUntil';
import { ScrollService } from 'app/shared/scroll.service'; import { ScrollService } from 'app/shared/scroll.service';
@ -122,8 +119,8 @@ export class ScrollSpiedElementGroup {
export class ScrollSpyService { export class ScrollSpyService {
private spiedElementGroups: ScrollSpiedElementGroup[] = []; private spiedElementGroups: ScrollSpiedElementGroup[] = [];
private onStopListening = new Subject(); private onStopListening = new Subject();
private resizeEvents = Observable.fromEvent(window, 'resize').auditTime(300).takeUntil(this.onStopListening); private resizeEvents = fromEvent(window, 'resize').pipe(auditTime(300), takeUntil(this.onStopListening));
private scrollEvents = Observable.fromEvent(window, 'scroll').auditTime(10).takeUntil(this.onStopListening); private scrollEvents = fromEvent(window, 'scroll').pipe(auditTime(10), takeUntil(this.onStopListening));
private lastContentHeight: number; private lastContentHeight: number;
private lastMaxScrollTop: number; private lastMaxScrollTop: number;
@ -159,7 +156,7 @@ export class ScrollSpyService {
this.spiedElementGroups.push(spiedGroup); this.spiedElementGroups.push(spiedGroup);
return { return {
active: spiedGroup.activeScrollItem.asObservable().distinctUntilChanged(), active: spiedGroup.activeScrollItem.asObservable().pipe(distinctUntilChanged()),
unspy: () => this.unspy(spiedGroup) unspy: () => this.unspy(spiedGroup)
}; };
} }

View File

@ -2,7 +2,7 @@ import { ReflectiveInjector } from '@angular/core';
import { fakeAsync, tick } from '@angular/core/testing'; import { fakeAsync, tick } from '@angular/core/testing';
import { NgServiceWorker } from '@angular/service-worker'; import { NgServiceWorker } from '@angular/service-worker';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import 'rxjs/add/operator/take'; import { take } from 'rxjs/operators';
import { Logger } from 'app/shared/logger.service'; import { Logger } from 'app/shared/logger.service';
import { SwUpdatesService } from './sw-updates.service'; import { SwUpdatesService } from './sw-updates.service';
@ -153,8 +153,8 @@ class MockNgServiceWorker {
updates = this.$$updatesSubj.asObservable(); updates = this.$$updatesSubj.asObservable();
activateUpdate = jasmine.createSpy('MockNgServiceWorker.activateUpdate') activateUpdate = jasmine.createSpy('MockNgServiceWorker.activateUpdate')
.and.callFake(() => this.$$activateUpdateSubj.take(1)); .and.callFake(() => this.$$activateUpdateSubj.pipe(take(1)));
checkForUpdate = jasmine.createSpy('MockNgServiceWorker.checkForUpdate') checkForUpdate = jasmine.createSpy('MockNgServiceWorker.checkForUpdate')
.and.callFake(() => this.$$checkForUpdateSubj.take(1)); .and.callFake(() => this.$$checkForUpdateSubj.pipe(take(1)));
} }

View File

@ -1,15 +1,7 @@
import { Injectable, OnDestroy } from '@angular/core'; import { Injectable, OnDestroy } from '@angular/core';
import { NgServiceWorker } from '@angular/service-worker'; import { NgServiceWorker } from '@angular/service-worker';
import { Observable, Subject } from 'rxjs'; import { concat, of, Subject } from 'rxjs';
import 'rxjs/add/observable/of'; import { debounceTime, filter, map, startWith, take, takeUntil, tap } from 'rxjs/operators';
import 'rxjs/add/operator/concat';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/filter';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/startWith';
import 'rxjs/add/operator/take';
import 'rxjs/add/operator/takeUntil';
import { Logger } from 'app/shared/logger.service'; import { Logger } from 'app/shared/logger.service';
@ -28,19 +20,22 @@ import { Logger } from 'app/shared/logger.service';
@Injectable() @Injectable()
export class SwUpdatesService implements OnDestroy { export class SwUpdatesService implements OnDestroy {
private checkInterval = 1000 * 60 * 60 * 6; // 6 hours private checkInterval = 1000 * 60 * 60 * 6; // 6 hours
private onDestroy = new Subject(); private onDestroy = new Subject<void>();
private checkForUpdateSubj = new Subject(); private checkForUpdateSubj = new Subject<void>();
updateActivated = this.sw.updates updateActivated = this.sw.updates.pipe(
.takeUntil(this.onDestroy) takeUntil(this.onDestroy),
.do(evt => this.log(`Update event: ${JSON.stringify(evt)}`)) tap(evt => this.log(`Update event: ${JSON.stringify(evt)}`)),
.filter(({type}) => type === 'activation') filter(({type}) => type === 'activation'),
.map(({version}) => version); map(({version}) => version),
);
constructor(private logger: Logger, private sw: NgServiceWorker) { constructor(private logger: Logger, private sw: NgServiceWorker) {
this.checkForUpdateSubj this.checkForUpdateSubj
.debounceTime(this.checkInterval) .pipe(
.startWith(null) debounceTime(this.checkInterval),
.takeUntil(this.onDestroy) startWith<void>(undefined),
takeUntil(this.onDestroy),
)
.subscribe(() => this.checkForUpdate()); .subscribe(() => this.checkForUpdate());
} }
@ -56,11 +51,13 @@ export class SwUpdatesService implements OnDestroy {
private checkForUpdate() { private checkForUpdate() {
this.log('Checking for update...'); this.log('Checking for update...');
this.sw.checkForUpdate() // Temp workaround for https://github.com/angular/mobile-toolkit/pull/137.
// Temp workaround for https://github.com/angular/mobile-toolkit/pull/137. // TODO (gkalpak): Remove once #137 is fixed.
// TODO (gkalpak): Remove once #137 is fixed. concat(this.sw.checkForUpdate(), of(false))
.concat(Observable.of(false)).take(1) .pipe(
.do(v => this.log(`Update available: ${v}`)) take(1),
tap(v => this.log(`Update available: ${v}`)),
)
.subscribe(v => v ? this.activateUpdate() : this.scheduleCheckForUpdate()); .subscribe(v => v ? this.activateUpdate() : this.scheduleCheckForUpdate());
} }

View File

@ -1,5 +1,6 @@
import { enableProdMode, ApplicationRef } from '@angular/core'; import { enableProdMode, ApplicationRef } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { first } from 'rxjs/operators';
import { AppModule } from './app/app.module'; import { AppModule } from './app/app.module';
import { environment } from './environments/environment'; import { environment } from './environments/environment';
@ -11,7 +12,7 @@ if (environment.production) {
platformBrowserDynamic().bootstrapModule(AppModule).then(ref => { platformBrowserDynamic().bootstrapModule(AppModule).then(ref => {
if (environment.production && 'serviceWorker' in (navigator as any)) { if (environment.production && 'serviceWorker' in (navigator as any)) {
const appRef: ApplicationRef = ref.injector.get(ApplicationRef); const appRef: ApplicationRef = ref.injector.get(ApplicationRef);
appRef.isStable.first().subscribe(() => { appRef.isStable.pipe(first()).subscribe(() => {
(navigator as any).serviceWorker.register('/worker-basic.min.js'); (navigator as any).serviceWorker.register('/worker-basic.min.js');
}); });
} }

View File

@ -1,10 +1,11 @@
import { BehaviorSubject } from 'rxjs'; import { BehaviorSubject } from 'rxjs';
import { map } from 'rxjs/operators';
export class MockLocationService { export class MockLocationService {
urlSubject = new BehaviorSubject<string>(this.initialUrl); urlSubject = new BehaviorSubject<string>(this.initialUrl);
currentUrl = this.urlSubject.asObservable().map(url => this.stripSlashes(url)); currentUrl = this.urlSubject.asObservable().pipe(map(url => this.stripSlashes(url)));
// strip off query and hash // strip off query and hash
currentPath = this.currentUrl.map(url => url.match(/[^?#]*/)![0]); currentPath = this.currentUrl.pipe(map(url => url.match(/[^?#]*/)![0]));
search = jasmine.createSpy('search').and.returnValue({}); search = jasmine.createSpy('search').and.returnValue({});
setSearch = jasmine.createSpy('setSearch'); setSearch = jasmine.createSpy('setSearch');
go = jasmine.createSpy('Location.go').and go = jasmine.createSpy('Location.go').and