diff --git a/CODING_STANDARDS.md b/CODING_STANDARDS.md index 5009fbee2b7f..f7bb8d1b1491 100644 --- a/CODING_STANDARDS.md +++ b/CODING_STANDARDS.md @@ -12,12 +12,12 @@ ES6 or TypeScript. ### General #### Write useful comments -Comments that explain what some block of code does are nice; they can tell you something in less +Comments that explain what some block of code does are nice; they can tell you something in less time than it would take to follow through the code itself. -Comments that explain why some block of code exists at all, or does something the way it does, -are _invaluable_. The "why" is difficult, or sometimes impossible, to track down without seeking out -the original author. When collaborators are in the same room, this hurts productivity. +Comments that explain why some block of code exists at all, or does something the way it does, +are _invaluable_. The "why" is difficult, or sometimes impossible, to track down without seeking out +the original author. When collaborators are in the same room, this hurts productivity. When collaborators are in different timezones, this can be devastating to productivity. For example, this is a not-very-useful comment: @@ -55,22 +55,22 @@ do this: ``` #### Prefer small, focused modules -Keeping modules to a single responsibility makes the code easier to test, consume, and maintain. -ES6 modules offer a straightforward way to organize code into logical, granular units. +Keeping modules to a single responsibility makes the code easier to test, consume, and maintain. +ES6 modules offer a straightforward way to organize code into logical, granular units. Ideally, individual files are 200 - 300 lines of code. -As a rule of thumb, once a file draws near 400 lines (barring abnormally long constants / comments), -start considering how to refactor into smaller pieces. +As a rule of thumb, once a file draws near 400 lines (barring abnormally long constants / comments), +start considering how to refactor into smaller pieces. #### Less is more -Once a feature is released, it never goes away. We should avoid adding features that don't offer -high user value for price we pay both in maintenance, complexity, and payload size. When in doubt, -leave it out. +Once a feature is released, it never goes away. We should avoid adding features that don't offer +high user value for price we pay both in maintenance, complexity, and payload size. When in doubt, +leave it out. -This applies especially so to providing two different APIs to accomplish the same thing. Always +This applies especially so to providing two different APIs to accomplish the same thing. Always prefer sticking to a _single_ API for accomplishing something. -### 100 column limit +### 100 column limit All code and docs in the repo should be 100 columns or fewer. This applies to TypeScript, SCSS, HTML, bash scripts, and markdown files. @@ -81,12 +81,12 @@ Avoid `any` where possible. If you find yourself using `any`, consider whether a appropriate in your case. For methods and properties that are part of a component's public API, all types must be explicitly -specified because our documentation tooling cannot currently infer types in places where TypeScript +specified because our documentation tooling cannot currently infer types in places where TypeScript can. #### Fluent APIs When creating a fluent or builder-pattern style API, use the `this` return type for methods: -``` +```ts class ConfigBuilder { withName(name: string): this { this.config.name = name; @@ -95,13 +95,39 @@ class ConfigBuilder { } ``` +#### RxJS +When dealing with RxJS operators, import the operator functions directly (e.g. +`import "rxjs/operator/map"`), as opposed to using the "patch" imports which pollute the user's +global Observable object (e.g. `import "rxjs/add/operator/map"`): + +```ts +// NO +import 'rxjs/add/operator/map'; +someObservable.map(...).subscribe(...); + +// YES +import {map} from 'rxjs/operator/map'; +map.call(someObservable, ...).subscribe(...); +``` + +Note that this approach can be inflexible when dealing with long chains of operators. You can use +the `FunctionChain` class to help with it: + +```ts +// Before +someObservable.filter(...).map(...).do(...); + +// After +new FunctionChain(someObservable).call(filter, ...).call(map, ...).call(do, ...).execute(); +``` + #### Access modifiers * Omit the `public` keyword as it is the default behavior. * Use `private` when appropriate and possible, prefixing the name with an underscore. * Use `protected` when appropriate and possible with no prefix. -* Prefix *library-internal* properties and methods with an underscore without using the `private` +* Prefix *library-internal* properties and methods with an underscore without using the `private` keyword. This is necessary for anything that must be public (to be used by Angular), but should not -be part of the user-facing API. This typically applies to symbols used in template expressions, +be part of the user-facing API. This typically applies to symbols used in template expressions, `@ViewChildren` / `@ContentChildren` properties, host bindings, and `@Input` / `@Output` properties (when using an alias). @@ -114,14 +140,14 @@ All public APIs must have user-facing comments. These are extracted and shown in on [material.angular.io](https://material.angular.io). Private and internal APIs should have JsDoc when they are not obvious. Ultimately it is the purview -of the code reviwer as to what is "obvious", but the rule of thumb is that *most* classes, -properties, and methods should have a JsDoc description. +of the code reviwer as to what is "obvious", but the rule of thumb is that *most* classes, +properties, and methods should have a JsDoc description. Properties should have a concise description of what the property means: ```ts /** The label position relative to the checkbox. Defaults to 'after' */ @Input() labelPosition: 'before' | 'after' = 'after'; -``` +``` Methods blocks should describe what the function does and provide a description for each parameter and the return value: @@ -145,7 +171,7 @@ Boolean properties and return values should use "Whether..." as opposed to "True ##### General * Prefer writing out words instead of using abbreviations. -* Prefer *exact* names over short names (within reason). E.g., `labelPosition` is better than +* Prefer *exact* names over short names (within reason). E.g., `labelPosition` is better than `align` because the former much more exactly communicates what the property means. * Except for `@Input` properties, use `is` and `has` prefixes for boolean properties / methods. @@ -169,25 +195,25 @@ The name of a method should capture the action that is performed *by* that metho ### Angular #### Host bindings -Prefer using the `host` object in the directive configuration instead of `@HostBinding` and -`@HostListener`. We do this because TypeScript preserves the type information of methods with +Prefer using the `host` object in the directive configuration instead of `@HostBinding` and +`@HostListener`. We do this because TypeScript preserves the type information of methods with decorators, and when one of the arguments for the method is a native `Event` type, this preserved -type information can lead to runtime errors in non-browser environments (e.g., server-side -pre-rendering). +type information can lead to runtime errors in non-browser environments (e.g., server-side +pre-rendering). ### CSS #### Be cautious with use of `display: flex` -* The [baseline calculation for flex elements](http://www.w3.org/TR/css-flexbox-1/#flex-baselines) -is different than other display values, making it difficult to align flex elements with standard +* The [baseline calculation for flex elements](http://www.w3.org/TR/css-flexbox-1/#flex-baselines) +is different than other display values, making it difficult to align flex elements with standard elements like input and button. * Component outermost elements are never flex (block or inline-block) * Don't use `display: flex` on elements that will contain projected content. #### Use lowest specificity possible -Always prioritize lower specificity over other factors. Most style definitions should consist of a -single element or css selector plus necessary state modifiers. **Avoid SCSS nesting for the sake of +Always prioritize lower specificity over other factors. Most style definitions should consist of a +single element or css selector plus necessary state modifiers. **Avoid SCSS nesting for the sake of code organization.** This will allow users to much more easily override styles. For example, rather than doing this: @@ -224,12 +250,12 @@ md-calendar { The end-user of a component should be the one to decide how much margin a component has around it. #### Prefer styling the host element vs. elements inside the template (where possible). -This makes it easier to override styles when necessary. For example, rather than +This makes it easier to override styles when necessary. For example, rather than ```scss the-host-element { // ... - + .some-child-element { color: red; } @@ -267,4 +293,4 @@ When it is not super obvious, include a brief description of what a class repres // Portion of the floating panel that sits, invisibly, on top of the input. .md-datepicker-input-mask { } -``` +``` diff --git a/src/lib/autocomplete/autocomplete-trigger.ts b/src/lib/autocomplete/autocomplete-trigger.ts index e15e902a54d9..a68b3c319635 100644 --- a/src/lib/autocomplete/autocomplete-trigger.ts +++ b/src/lib/autocomplete/autocomplete-trigger.ts @@ -16,18 +16,21 @@ import {Overlay, OverlayRef, OverlayState, TemplatePortal} from '../core'; import {MdAutocomplete} from './autocomplete'; import {PositionStrategy} from '../core/overlay/position/position-strategy'; import {ConnectedPositionStrategy} from '../core/overlay/position/connected-position-strategy'; -import {Observable} from 'rxjs/Observable'; import {MdOptionSelectEvent, MdOption} from '../core/option/option'; import {ActiveDescendantKeyManager} from '../core/a11y/activedescendant-key-manager'; import {ENTER, UP_ARROW, DOWN_ARROW} from '../core/keyboard/keycodes'; +import {MdInputContainer, FloatPlaceholderType} from '../input/input-container'; import {Dir} from '../core/rtl/dir'; import {Subscription} from 'rxjs/Subscription'; +import {Observable} from 'rxjs/Observable'; import {Subject} from 'rxjs/Subject'; -import 'rxjs/add/observable/of'; -import 'rxjs/add/observable/merge'; -import 'rxjs/add/operator/startWith'; -import 'rxjs/add/operator/switchMap'; -import {MdInputContainer, FloatPlaceholderType} from '../input/input-container'; +import {FunctionChain} from '../core/util/function-chain'; +import {of as observableOf} from 'rxjs/observable/of'; +import {merge} from 'rxjs/observable/merge'; +import {first} from 'rxjs/operator/first'; +import {map} from 'rxjs/operator/map'; +import {switchMap} from 'rxjs/operator/switchMap'; + /** * The following style constants are necessary to save here in order @@ -144,11 +147,10 @@ export class MdAutocompleteTrigger implements AfterContentInit, ControlValueAcce * when an option is selected, on blur, and when TAB is pressed. */ get panelClosingActions(): Observable { - return Observable.merge( - ...this.optionSelections, - this._blurStream.asObservable(), - this._keyManager.tabOut - ); + return merge.call(Observable, + ...this.optionSelections, + this._blurStream.asObservable(), + this._keyManager.tabOut); } /** Stream of autocomplete option selections. */ @@ -251,19 +253,24 @@ export class MdAutocompleteTrigger implements AfterContentInit, ControlValueAcce const initialOptions = this._getStableOptions(); // When the zone is stable initially, and when the option list changes... - Observable.merge(initialOptions, this.autocomplete.options.changes) - // create a new stream of panelClosingActions, replacing any previous streams - // that were created, and flatten it so our stream only emits closing events... - .switchMap(options => { - this._resetPanel(); - // If the options list is empty, emit close event immediately. - // Otherwise, listen for panel closing actions... - return options.length ? this.panelClosingActions : Observable.of(null); - }) - // when the first closing event occurs... - .first() - // set the value, close the panel, and complete. - .subscribe(event => this._setValueAndClose(event)); + const observable = merge.call(Observable, + initialOptions, this.autocomplete.options.changes); + + new FunctionChain>() + .context(observable) + // create a new stream of panelClosingActions, replacing any previous streams + // that were created, and flatten it so our stream only emits closing events... + .call(switchMap, (options: MdOption[]) => { + this._resetPanel(); + // If the options list is empty, emit close event immediately. + // Otherwise, listen for panel closing actions... + return options.length ? this.panelClosingActions : observableOf.call(Observable, null); + }) + // when the first closing event occurs... + .call(first) + .execute() + // set the value, close the panel, and complete. + .subscribe(event => this._setValueAndClose(event)); } /** @@ -272,7 +279,11 @@ export class MdAutocompleteTrigger implements AfterContentInit, ControlValueAcce * with the options available under the current filter. */ private _getStableOptions(): Observable> { - return this._zone.onStable.first().map(() => this.autocomplete.options); + return new FunctionChain>>() + .context(this._zone.onStable) + .call(first) + .call(map, () => this.autocomplete.options) + .execute(); } /** Destroys the autocomplete suggestion panel. */ diff --git a/src/lib/autocomplete/autocomplete.spec.ts b/src/lib/autocomplete/autocomplete.spec.ts index 57a5e4ca4fb0..303a85f9abdf 100644 --- a/src/lib/autocomplete/autocomplete.spec.ts +++ b/src/lib/autocomplete/autocomplete.spec.ts @@ -797,7 +797,7 @@ describe('MdAutocomplete', () => { - + {{ state.code }}: {{ state.name }} diff --git a/src/lib/core/a11y/focus-trap.ts b/src/lib/core/a11y/focus-trap.ts index 714b7f6d3610..8e07a667b4ba 100644 --- a/src/lib/core/a11y/focus-trap.ts +++ b/src/lib/core/a11y/focus-trap.ts @@ -1,4 +1,5 @@ import {Component, ViewEncapsulation, ViewChild, ElementRef, Input, NgZone} from '@angular/core'; +import {first} from 'rxjs/operator/first'; import {InteractivityChecker} from './interactivity-checker'; import {coerceBooleanProperty} from '../coercion/boolean-property'; @@ -33,7 +34,7 @@ export class FocusTrap { * trap region. */ focusFirstTabbableElementWhenReady() { - this._ngZone.onMicrotaskEmpty.first().subscribe(() => { + first.call(this._ngZone.onMicrotaskEmpty).subscribe(() => { this.focusFirstTabbableElement(); }); } @@ -43,7 +44,7 @@ export class FocusTrap { * trap region. */ focusLastTabbableElementWhenReady() { - this._ngZone.onMicrotaskEmpty.first().subscribe(() => { + first.call(this._ngZone.onMicrotaskEmpty).subscribe(() => { this.focusLastTabbableElement(); }); } diff --git a/src/lib/core/a11y/list-key-manager.spec.ts b/src/lib/core/a11y/list-key-manager.spec.ts index 2f6bd8657f8e..c983dda16972 100644 --- a/src/lib/core/a11y/list-key-manager.spec.ts +++ b/src/lib/core/a11y/list-key-manager.spec.ts @@ -3,6 +3,7 @@ import {FocusKeyManager} from './focus-key-manager'; import {DOWN_ARROW, UP_ARROW, TAB, HOME, END} from '../keyboard/keycodes'; import {ListKeyManager} from './list-key-manager'; import {ActiveDescendantKeyManager} from './activedescendant-key-manager'; +import {first} from 'rxjs/operator/first'; class FakeFocusable { disabled = false; @@ -218,7 +219,7 @@ describe('Key managers', () => { it('should emit tabOut when the tab key is pressed', () => { let tabOutEmitted = false; - keyManager.tabOut.first().subscribe(() => tabOutEmitted = true); + first.call(keyManager.tabOut).subscribe(() => tabOutEmitted = true); keyManager.onKeydown(TAB_EVENT); expect(tabOutEmitted).toBe(true); diff --git a/src/lib/core/overlay/scroll/scroll-dispatcher.ts b/src/lib/core/overlay/scroll/scroll-dispatcher.ts index e4f8b3864713..315200d3fd76 100644 --- a/src/lib/core/overlay/scroll/scroll-dispatcher.ts +++ b/src/lib/core/overlay/scroll/scroll-dispatcher.ts @@ -3,7 +3,7 @@ import {Scrollable} from './scrollable'; import {Subject} from 'rxjs/Subject'; import {Observable} from 'rxjs/Observable'; import {Subscription} from 'rxjs/Subscription'; -import 'rxjs/add/observable/fromEvent'; +import {fromEvent} from 'rxjs/observable/fromEvent'; /** @@ -23,8 +23,8 @@ export class ScrollDispatcher { constructor() { // By default, notify a scroll event when the document is scrolled or the window is resized. - Observable.fromEvent(window.document, 'scroll').subscribe(() => this._notify()); - Observable.fromEvent(window, 'resize').subscribe(() => this._notify()); + fromEvent.call(Observable, window.document, 'scroll').subscribe(() => this._notify()); + fromEvent.call(Observable, window, 'resize').subscribe(() => this._notify()); } /** diff --git a/src/lib/core/overlay/scroll/scrollable.ts b/src/lib/core/overlay/scroll/scrollable.ts index 3e5f2bb03616..66ee4be1be22 100644 --- a/src/lib/core/overlay/scroll/scrollable.ts +++ b/src/lib/core/overlay/scroll/scrollable.ts @@ -1,7 +1,7 @@ import {Directive, ElementRef, OnInit, OnDestroy} from '@angular/core'; import {Observable} from 'rxjs/Observable'; import {ScrollDispatcher} from './scroll-dispatcher'; -import 'rxjs/add/observable/fromEvent'; +import {fromEvent} from 'rxjs/observable/fromEvent'; /** @@ -28,7 +28,7 @@ export class Scrollable implements OnInit, OnDestroy { * Returns observable that emits when a scroll event is fired on the host element. */ elementScrolled(): Observable { - return Observable.fromEvent(this._elementRef.nativeElement, 'scroll'); + return fromEvent.call(Observable, this._elementRef.nativeElement, 'scroll'); } getElementRef(): ElementRef { diff --git a/src/lib/core/util/function-chain.spec.ts b/src/lib/core/util/function-chain.spec.ts new file mode 100644 index 000000000000..5eb720721a18 --- /dev/null +++ b/src/lib/core/util/function-chain.spec.ts @@ -0,0 +1,46 @@ +import {FunctionChain} from './function-chain'; + +describe('FunctionChain', () => { + it('should chain functions', () => { + let first = jasmine.createSpy('First function'); + let second = jasmine.createSpy('Second function'); + let third = jasmine.createSpy('Third function'); + + new FunctionChain().call(first).call(second).call(third).execute(); + + expect(first).toHaveBeenCalled(); + expect(second).toHaveBeenCalled(); + expect(third).toHaveBeenCalled(); + }); + + it('should pass in arguments to the functions', () => { + let first = jasmine.createSpy('First function'); + let second = jasmine.createSpy('Second function'); + + new FunctionChain().call(first, 1, 2).call(second, 3, 4).execute(); + + expect(first).toHaveBeenCalledWith(1, 2); + expect(second).toHaveBeenCalledWith(3, 4); + }); + + it('should clear the chain once it has been executed', () => { + let fn = jasmine.createSpy('Spy function'); + let chain = new FunctionChain().call(fn); + + chain.execute(); + expect(fn).toHaveBeenCalledTimes(1); + + chain.execute(); + expect(fn).toHaveBeenCalledTimes(1); + }); + + it('should return the final function result', () => { + let result = new FunctionChain() + .context([1, 2, 3, 4, 5]) + .call(Array.prototype.map, (current: number) => current * 2) + .call(Array.prototype.filter, (current: number) => current > 5) + .execute(); + + expect(result).toEqual([6, 8, 10]); + }); +}); diff --git a/src/lib/core/util/function-chain.ts b/src/lib/core/util/function-chain.ts new file mode 100644 index 000000000000..2ec440c11f06 --- /dev/null +++ b/src/lib/core/util/function-chain.ts @@ -0,0 +1,57 @@ +/** + * Collects and executes a chain of functions. Useful in cases like RxJS, where we can't chain + * the methods directly, but rather have to call them individually. + * @docs-private + * + * @example + * // Standard way + * someObservable.filter(...).map(...).do(...); + * + * // Function chain + * new FunctionChain() + * .context(someObservable) + * .call(filter, ...) + * .call(map, ...) + * .call(do, ...) + * .execute(); + */ +export class FunctionChain { + /** Tracks the currently-chained functions. */ + private _chainedCalls: any[] = []; + + constructor(private _initialContext?: any) { } + + /** + * Adds a function to the chain. + * @param fn Functions to be added to the chain. + * @param ...args Arguments with which to call the function. + */ + call(fn: Function, ...args: any[]): this { + this._chainedCalls.push([fn, ...args]); + return this; + } + + /** + * Executes all of the functions in the chain and clears it. + * @returns The return value of the final function in the chain. + */ + execute(): T { + let result = this._chainedCalls.reduce((currentValue, currentFunction) => { + return (currentFunction.shift() as Function).apply(currentValue, currentFunction); + }, this._initialContext); + + this._chainedCalls.length = 0; + + return result as T; + } + + /** + * Allows setting the initial context in a separate call from the constructor. + * This helps with readability where the line with the constructor could be too long. + * @param initialContext The new initial context. + */ + context(initialContext: any): this { + this._initialContext = initialContext; + return this; + } +} diff --git a/src/lib/dialog/dialog-container.ts b/src/lib/dialog/dialog-container.ts index 415c6d31c1bb..3ad9768d5f83 100644 --- a/src/lib/dialog/dialog-container.ts +++ b/src/lib/dialog/dialog-container.ts @@ -12,7 +12,7 @@ import {MdDialogConfig} from './dialog-config'; import {MdDialogRef} from './dialog-ref'; import {MdDialogContentAlreadyAttachedError} from './dialog-errors'; import {FocusTrap} from '../core/a11y/focus-trap'; -import 'rxjs/add/operator/first'; +import {first} from 'rxjs/operator/first'; /** @@ -65,7 +65,7 @@ export class MdDialogContainer extends BasePortalHost implements OnDestroy { // If were to attempt to focus immediately, then the content of the dialog would not yet be // ready in instances where change detection has to run first. To deal with this, we simply // wait for the microtask queue to be empty. - this._ngZone.onMicrotaskEmpty.first().subscribe(() => { + first.call(this._ngZone.onMicrotaskEmpty).subscribe(() => { this._elementFocusedBeforeDialogWasOpened = document.activeElement; this._focusTrap.focusFirstTabbableElement(); }); @@ -94,7 +94,7 @@ export class MdDialogContainer extends BasePortalHost implements OnDestroy { // that it doesn't end up back on the . Also note that we need the extra check, because // IE can set the `activeElement` to null in some cases. if (this._elementFocusedBeforeDialogWasOpened) { - this._ngZone.onMicrotaskEmpty.first().subscribe(() => { + first.call(this._ngZone.onMicrotaskEmpty).subscribe(() => { this._renderer.invokeElementMethod(this._elementFocusedBeforeDialogWasOpened, 'focus'); }); } diff --git a/src/lib/dialog/dialog.ts b/src/lib/dialog/dialog.ts index 93e48cd6b2fc..c7b563b74a4a 100644 --- a/src/lib/dialog/dialog.ts +++ b/src/lib/dialog/dialog.ts @@ -1,6 +1,7 @@ import {Injector, ComponentRef, Injectable, Optional, SkipSelf} from '@angular/core'; import {Observable} from 'rxjs/Observable'; import {Subject} from 'rxjs/Subject'; +import {first} from 'rxjs/operator/first'; import {Overlay, OverlayRef, ComponentType, OverlayState, ComponentPortal} from '../core'; import {extendObject} from '../core/util/object-extend'; @@ -127,11 +128,11 @@ export class MdDialog { config?: MdDialogConfig): MdDialogRef { // Create a reference to the dialog we're creating in order to give the user a handle // to modify and close it. - let dialogRef = > new MdDialogRef(overlayRef); + let dialogRef = new MdDialogRef(overlayRef) as MdDialogRef; if (!dialogContainer.dialogConfig.disableClose) { // When the dialog backdrop is clicked, we want to close it. - overlayRef.backdropClick().first().subscribe(() => dialogRef.close()); + first.call(overlayRef.backdropClick()).subscribe(() => dialogRef.close()); } // Set the dialogRef to the container so that it can use the ref to close the dialog. diff --git a/src/lib/icon/icon-registry.ts b/src/lib/icon/icon-registry.ts index bea95e419fa7..8ec2a5c59859 100644 --- a/src/lib/icon/icon-registry.ts +++ b/src/lib/icon/icon-registry.ts @@ -1,17 +1,19 @@ import {Injectable, SecurityContext} from '@angular/core'; import {SafeResourceUrl, DomSanitizer} from '@angular/platform-browser'; -import {Http} from '@angular/http'; -import {MdError} from '../core'; +import {Http, Response} from '@angular/http'; + import {Observable} from 'rxjs/Observable'; -import 'rxjs/add/observable/forkJoin'; -import 'rxjs/add/observable/of'; -import 'rxjs/add/operator/map'; -import 'rxjs/add/operator/filter'; -import 'rxjs/add/operator/do'; -import 'rxjs/add/operator/share'; -import 'rxjs/add/operator/finally'; -import 'rxjs/add/operator/catch'; +import {of as observableOf} from 'rxjs/observable/of'; +import {forkJoin} from 'rxjs/observable/forkJoin'; +import {map} from 'rxjs/operator/map'; +import {share} from 'rxjs/operator/share'; +import {_throw} from 'rxjs/observable/throw'; +import {_finally} from 'rxjs/operator/finally'; +import {_catch} from 'rxjs/operator/catch'; +import {_do} from 'rxjs/operator/do'; +import {MdError} from '../core'; +import {FunctionChain} from '../core/util/function-chain'; /** * Exception thrown when attempting to load an icon with a name that cannot be found. @@ -180,11 +182,14 @@ export class MdIconRegistry { let url = this._sanitizer.sanitize(SecurityContext.RESOURCE_URL, safeUrl); if (this._cachedIconsByUrl.has(url)) { - return Observable.of(cloneSvg(this._cachedIconsByUrl.get(url))); + return observableOf.call(Observable, cloneSvg(this._cachedIconsByUrl.get(url))); } - return this._loadSvgIconFromConfig(new SvgIconConfig(url)) - .do(svg => this._cachedIconsByUrl.set(url, svg)) - .map(svg => cloneSvg(svg)); + + return new FunctionChain>() + .context(this._loadSvgIconFromConfig(new SvgIconConfig(url))) + .call(_do, (svg: SVGElement) => this._cachedIconsByUrl.set(url, svg)) + .call(map, (svg: SVGElement) => cloneSvg(svg)) + .execute(); } /** @@ -206,7 +211,7 @@ export class MdIconRegistry { if (iconSetConfigs) { return this._getSvgFromIconSetConfigs(name, iconSetConfigs); } - return Observable.throw(new MdIconNameNotFoundError(key)); + return _throw.call(Observable, new MdIconNameNotFoundError(key)); } /** @@ -215,12 +220,14 @@ export class MdIconRegistry { private _getSvgFromConfig(config: SvgIconConfig): Observable { if (config.svgElement) { // We already have the SVG element for this icon, return a copy. - return Observable.of(cloneSvg(config.svgElement)); + return observableOf.call(Observable, cloneSvg(config.svgElement)); } else { // Fetch the icon from the config's URL, cache it, and return a copy. - return this._loadSvgIconFromConfig(config) - .do(svg => config.svgElement = svg) - .map(svg => cloneSvg(svg)); + return new FunctionChain>() + .context(this._loadSvgIconFromConfig(config)) + .call(_do, (svg: SVGElement) => config.svgElement = svg) + .call(map, (svg: SVGElement) => cloneSvg(svg)) + .execute(); } } @@ -241,39 +248,48 @@ export class MdIconRegistry { // We could cache namedIcon in _svgIconConfigs, but since we have to make a copy every // time anyway, there's probably not much advantage compared to just always extracting // it from the icon set. - return Observable.of(namedIcon); + return observableOf.call(Observable, namedIcon); } // Not found in any cached icon sets. If there are icon sets with URLs that we haven't // fetched, fetch them now and look for iconName in the results. const iconSetFetchRequests: Observable[] = iconSetConfigs .filter(iconSetConfig => !iconSetConfig.svgElement) - .map(iconSetConfig => - this._loadSvgIconSetFromConfig(iconSetConfig) - .catch((err: any, caught: Observable): Observable => { - let url = - this._sanitizer.sanitize(SecurityContext.RESOURCE_URL, iconSetConfig.url); - - // Swallow errors fetching individual URLs so the combined Observable won't - // necessarily fail. - console.log(`Loading icon set URL: ${url} failed: ${err}`); - return Observable.of(null); - }) - .do(svg => { - // Cache SVG element. - if (svg) { - iconSetConfig.svgElement = svg; - } - })); + .map(iconSetConfig => { + let chain = new FunctionChain>(); + + chain + .context(this._loadSvgIconSetFromConfig(iconSetConfig)) + .call(_catch, (err: any, caught: Observable) => { + let url = this._sanitizer.sanitize(SecurityContext.RESOURCE_URL, iconSetConfig.url); + + // Swallow errors fetching individual URLs so the combined Observable won't + // necessarily fail. + console.log(`Loading icon set URL: ${url} failed: ${err}`); + return observableOf.call(Observable, null); + }) + .call(_do, (svg: SVGElement) => { + // Cache SVG element. + if (svg) { + iconSetConfig.svgElement = svg; + } + }); + + return chain.execute(); + }); + // Fetch all the icon set URLs. When the requests complete, every IconSet should have a // cached SVG element (unless the request failed), and we can check again for the icon. - return Observable.forkJoin(iconSetFetchRequests) - .map((ignoredResults: any) => { - const foundIcon = this._extractIconWithNameFromAnySet(name, iconSetConfigs); - if (!foundIcon) { - throw new MdIconNameNotFoundError(name); - } - return foundIcon; - }); + return new FunctionChain>() + .context(Observable) + .call(forkJoin, iconSetFetchRequests) + .call(map, (ignoredResults: any) => { + const foundIcon = this._extractIconWithNameFromAnySet(name, iconSetConfigs); + if (!foundIcon) { + throw new MdIconNameNotFoundError(name); + } + return foundIcon; + }) + .execute(); } /** @@ -301,8 +317,8 @@ export class MdIconRegistry { * from it. */ private _loadSvgIconFromConfig(config: SvgIconConfig): Observable { - return this._fetchUrl(config.url) - .map(svgText => this._createSvgElementForSingleIcon(svgText)); + return map.call(this._fetchUrl(config.url), + (svgText: string) => this._createSvgElementForSingleIcon(svgText)); } /** @@ -310,9 +326,9 @@ export class MdIconRegistry { * from it. */ private _loadSvgIconSetFromConfig(config: SvgIconConfig): Observable { - // TODO: Document that icons should only be loaded from trusted sources. - return this._fetchUrl(config.url) - .map(svgText => this._svgElementFromString(svgText)); + // TODO: Document that icons should only be loaded from trusted sources. + return map.call(this._fetchUrl(config.url), + (svgText: string) => this._svgElementFromString(svgText)); } /** @@ -396,12 +412,13 @@ export class MdIconRegistry { // TODO(jelbourn): for some reason, the `finally` operator "loses" the generic type on the // Observable. Figure out why and fix it. - const req = > this._http.get(url) - .map(response => response.text()) - .finally(() => { - this._inProgressUrlFetches.delete(url); - }) - .share(); + let req = new FunctionChain>() + .context(this._http.get(url)) + .call(map, (response: Response) => response.text()) + .call(_finally, () => { this._inProgressUrlFetches.delete(url); }) + .call(share) + .execute(); + this._inProgressUrlFetches.set(url, req); return req; } diff --git a/src/lib/icon/icon.ts b/src/lib/icon/icon.ts index 73de166725e5..441e9e305e83 100644 --- a/src/lib/icon/icon.ts +++ b/src/lib/icon/icon.ts @@ -16,8 +16,10 @@ import { } from '@angular/core'; import {HttpModule, Http} from '@angular/http'; import {DomSanitizer} from '@angular/platform-browser'; +import {first} from 'rxjs/operator/first'; import {MdError, CompatibilityModule} from '../core'; import {MdIconRegistry} from './icon-registry'; + export {MdIconRegistry} from './icon-registry'; /** Exception thrown when an invalid icon name is passed to an md-icon component. */ @@ -150,8 +152,9 @@ export class MdIcon implements OnChanges, OnInit, AfterViewChecked { if (changedInputs.indexOf('svgIcon') != -1 || changedInputs.indexOf('svgSrc') != -1) { if (this.svgIcon) { const [namespace, iconName] = this._splitIconName(this.svgIcon); - this._mdIconRegistry.getNamedSvgIcon(iconName, namespace).first().subscribe( - svg => this._setSvgElement(svg), + + first.call(this._mdIconRegistry.getNamedSvgIcon(iconName, namespace)).subscribe( + (svg: SVGElement) => this._setSvgElement(svg), (err: any) => console.log(`Error retrieving icon: ${err}`)); } } diff --git a/src/lib/snack-bar/snack-bar-container.ts b/src/lib/snack-bar/snack-bar-container.ts index d191067a0914..08a0932c8d06 100644 --- a/src/lib/snack-bar/snack-bar-container.ts +++ b/src/lib/snack-bar/snack-bar-container.ts @@ -23,7 +23,7 @@ import {MdSnackBarConfig} from './snack-bar-config'; import {MdSnackBarContentAlreadyAttached} from './snack-bar-errors'; import {Observable} from 'rxjs/Observable'; import {Subject} from 'rxjs/Subject'; - +import {first} from 'rxjs/operator/first'; export type SnackBarState = 'initial' | 'visible' | 'complete' | 'void'; @@ -150,7 +150,7 @@ export class MdSnackBarContainer extends BasePortalHost implements OnDestroy { * errors where we end up removing an element which is in the middle of an animation. */ private _completeExit() { - this._ngZone.onMicrotaskEmpty.first().subscribe(() => { + first.call(this._ngZone.onMicrotaskEmpty).subscribe(() => { this.onExit.next(); this.onExit.complete(); }); diff --git a/src/lib/tabs/tab-body.ts b/src/lib/tabs/tab-body.ts index 1de1100616e5..277a59652c30 100644 --- a/src/lib/tabs/tab-body.ts +++ b/src/lib/tabs/tab-body.ts @@ -18,7 +18,6 @@ import { AfterContentChecked, } from '@angular/core'; import {TemplatePortal, PortalHostDirective, Dir, LayoutDirection} from '../core'; -import 'rxjs/add/operator/map'; /** * These position states are used internally as animation states for the tab body. Setting the diff --git a/src/lib/tabs/tab-group.ts b/src/lib/tabs/tab-group.ts index 2b8b6cd47724..d69650032e9d 100644 --- a/src/lib/tabs/tab-group.ts +++ b/src/lib/tabs/tab-group.ts @@ -21,7 +21,7 @@ import {MdTabLabelWrapper} from './tab-label-wrapper'; import {MdTabNavBar, MdTabLink, MdTabLinkRipple} from './tab-nav-bar/tab-nav-bar'; import {MdInkBar} from './ink-bar'; import {Observable} from 'rxjs/Observable'; -import 'rxjs/add/operator/map'; +import {map} from 'rxjs/operator/map'; import {MdRippleModule} from '../core/ripple/ripple'; import {ObserveContentModule} from '../core/observe-content/observe-content'; import {MdTab} from './tab'; @@ -96,7 +96,7 @@ export class MdTabGroup { /** Output to enable support for two-way binding on `selectedIndex`. */ @Output() get selectedIndexChange(): Observable { - return this.selectChange.map(event => event.index); + return map.call(this.selectChange, (event: MdTabChangeEvent) => event.index); } private _onFocusChange: EventEmitter = new EventEmitter(); diff --git a/src/lib/tabs/tab-header.ts b/src/lib/tabs/tab-header.ts index 7a0abd9b8228..548e173b4278 100644 --- a/src/lib/tabs/tab-header.ts +++ b/src/lib/tabs/tab-header.ts @@ -16,7 +16,6 @@ import { import {RIGHT_ARROW, LEFT_ARROW, ENTER, Dir, LayoutDirection} from '../core'; import {MdTabLabelWrapper} from './tab-label-wrapper'; import {MdInkBar} from './ink-bar'; -import 'rxjs/add/operator/map'; import {applyCssTransform} from '../core/style/apply-transform'; /** diff --git a/src/lib/tooltip/tooltip.ts b/src/lib/tooltip/tooltip.ts index 94dc7780062a..fdfd157f97b6 100644 --- a/src/lib/tooltip/tooltip.ts +++ b/src/lib/tooltip/tooltip.ts @@ -31,7 +31,7 @@ import {MdTooltipInvalidPositionError} from './tooltip-errors'; import {Observable} from 'rxjs/Observable'; import {Subject} from 'rxjs/Subject'; import {Dir} from '../core/rtl/dir'; -import 'rxjs/add/operator/first'; +import {first} from 'rxjs/operator/first'; export type TooltipPosition = 'left' | 'right' | 'above' | 'below' | 'before' | 'after'; @@ -252,7 +252,8 @@ export class MdTooltip implements OnDestroy { // Must wait for the message to be painted to the tooltip so that the overlay can properly // calculate the correct positioning based on the size of the text. this._tooltipInstance.message = message; - this._ngZone.onMicrotaskEmpty.first().subscribe(() => { + + first.call(this._ngZone.onMicrotaskEmpty).subscribe(() => { if (this._tooltipInstance) { this._overlayRef.updatePosition(); } diff --git a/tools/gulp/tasks/components.ts b/tools/gulp/tasks/components.ts index bf00567ebef9..53e112f0b8a2 100644 --- a/tools/gulp/tasks/components.ts +++ b/tools/gulp/tasks/components.ts @@ -72,21 +72,21 @@ task(':build:components:rollup', () => { // Rxjs dependencies 'rxjs/Subject': 'Rx', - 'rxjs/add/observable/fromEvent': 'Rx.Observable', - 'rxjs/add/observable/forkJoin': 'Rx.Observable', - 'rxjs/add/observable/of': 'Rx.Observable', - 'rxjs/add/observable/merge': 'Rx.Observable', - 'rxjs/add/observable/throw': 'Rx.Observable', - 'rxjs/add/operator/toPromise': 'Rx.Observable.prototype', - 'rxjs/add/operator/map': 'Rx.Observable.prototype', - 'rxjs/add/operator/filter': 'Rx.Observable.prototype', - 'rxjs/add/operator/do': 'Rx.Observable.prototype', - 'rxjs/add/operator/share': 'Rx.Observable.prototype', - 'rxjs/add/operator/finally': 'Rx.Observable.prototype', - 'rxjs/add/operator/catch': 'Rx.Observable.prototype', - 'rxjs/add/operator/first': 'Rx.Observable.prototype', - 'rxjs/add/operator/startWith': 'Rx.Observable.prototype', - 'rxjs/add/operator/switchMap': 'Rx.Observable.prototype', + 'rxjs/observable/fromEvent': 'Rx.Observable', + 'rxjs/observable/forkJoin': 'Rx.Observable', + 'rxjs/observable/of': 'Rx.Observable', + 'rxjs/observable/merge': 'Rx.Observable', + 'rxjs/observable/throw': 'Rx.Observable', + 'rxjs/operator/toPromise': 'Rx.Observable.prototype', + 'rxjs/operator/map': 'Rx.Observable.prototype', + 'rxjs/operator/filter': 'Rx.Observable.prototype', + 'rxjs/operator/do': 'Rx.Observable.prototype', + 'rxjs/operator/share': 'Rx.Observable.prototype', + 'rxjs/operator/finally': 'Rx.Observable.prototype', + 'rxjs/operator/catch': 'Rx.Observable.prototype', + 'rxjs/operator/first': 'Rx.Observable.prototype', + 'rxjs/operator/startWith': 'Rx.Observable.prototype', + 'rxjs/operator/switchMap': 'Rx.Observable.prototype', 'rxjs/Observable': 'Rx' };