From c4f29a307e256bac130a01f47a1752954d5a6fde Mon Sep 17 00:00:00 2001 From: crisbeto Date: Sat, 5 Aug 2017 12:55:07 +0200 Subject: [PATCH] feat(dialog): open dialog API improvements * Makes the `openDialog` public. * Adds a unique id to each `MdDialogRef`, as well as the option to override the id and to look dialogs by id. * The `afterAllClosed` stream now emits on subscribe if there are no open dialogs. Fixes #6272. --- src/lib/dialog/dialog-config.ts | 3 + src/lib/dialog/dialog-ref.ts | 8 ++- src/lib/dialog/dialog.spec.ts | 53 +++++++++++++----- src/lib/dialog/dialog.ts | 79 ++++++++++++++++----------- tools/package-tools/rollup-globals.ts | 1 + 5 files changed, 96 insertions(+), 48 deletions(-) diff --git a/src/lib/dialog/dialog-config.ts b/src/lib/dialog/dialog-config.ts index 190631277d2b..dfe5ce0525f4 100644 --- a/src/lib/dialog/dialog-config.ts +++ b/src/lib/dialog/dialog-config.ts @@ -33,6 +33,9 @@ export class MdDialogConfig { */ viewContainerRef?: ViewContainerRef; + /** ID for the dialog. If omitted, a unique one will be generated. */ + id?: string; + /** The ARIA role of the dialog element. */ role?: DialogRole = 'dialog'; diff --git a/src/lib/dialog/dialog-ref.ts b/src/lib/dialog/dialog-ref.ts index 0bfe795dde1e..1d8faeea3fc6 100644 --- a/src/lib/dialog/dialog-ref.ts +++ b/src/lib/dialog/dialog-ref.ts @@ -17,6 +17,8 @@ import {RxChain, first, filter} from '../core/rxjs/index'; // TODO(jelbourn): resizing // TODO(jelbourn): afterOpen and beforeClose +// Counter for unique dialog ids. +let uniqueId = 0; /** * Reference to a dialog opened via the MdDialog service. @@ -34,7 +36,11 @@ export class MdDialogRef { /** Result to be passed to afterClosed. */ private _result: any; - constructor(private _overlayRef: OverlayRef, private _containerInstance: MdDialogContainer) { + constructor( + private _overlayRef: OverlayRef, + private _containerInstance: MdDialogContainer, + public readonly id: string = `md-dialog-${uniqueId++}`) { + RxChain.from(_containerInstance._animationStateChanged) .call(filter, event => event.phaseName === 'done' && event.toState === 'exit') .call(first) diff --git a/src/lib/dialog/dialog.spec.ts b/src/lib/dialog/dialog.spec.ts index c006827824a3..597732465d66 100644 --- a/src/lib/dialog/dialog.spec.ts +++ b/src/lib/dialog/dialog.spec.ts @@ -205,33 +205,32 @@ describe('MdDialog', () => { }); it('should notify the observers if all open dialogs have finished closing', async(() => { - const ref1 = dialog.open(PizzaMsg, { - viewContainerRef: testViewContainerRef - }); - const ref2 = dialog.open(ContentElementDialog, { - viewContainerRef: testViewContainerRef - }); - let allClosed = false; + const ref1 = dialog.open(PizzaMsg, { viewContainerRef: testViewContainerRef }); + const ref2 = dialog.open(ContentElementDialog, { viewContainerRef: testViewContainerRef }); + const spy = jasmine.createSpy('afterAllClosed spy'); - dialog.afterAllClosed.subscribe(() => { - allClosed = true; - }); + dialog.afterAllClosed.subscribe(spy); ref1.close(); viewContainerFixture.detectChanges(); viewContainerFixture.whenStable().then(() => { - expect(allClosed).toBeFalsy(); + expect(spy).not.toHaveBeenCalled(); ref2.close(); viewContainerFixture.detectChanges(); - - viewContainerFixture.whenStable().then(() => { - expect(allClosed).toBeTruthy(); - }); + viewContainerFixture.whenStable().then(() => expect(spy).toHaveBeenCalled()); }); })); + it('should emit the afterAllClosed stream on subscribe if there are no open dialogs', () => { + const spy = jasmine.createSpy('afterAllClosed spy'); + + dialog.afterAllClosed.subscribe(spy); + + expect(spy).toHaveBeenCalled(); + }); + it('should should override the width of the overlay pane', () => { dialog.open(PizzaMsg, { width: '500px' @@ -468,6 +467,30 @@ describe('MdDialog', () => { }); })); + it('should assign a unique id to each dialog', () => { + const one = dialog.open(PizzaMsg); + const two = dialog.open(PizzaMsg); + + expect(one.id).toBeTruthy(); + expect(two.id).toBeTruthy(); + expect(one.id).not.toBe(two.id); + }); + + it('should allow for the id to be overwritten', () => { + const dialogRef = dialog.open(PizzaMsg, { id: 'pizza' }); + expect(dialogRef.id).toBe('pizza'); + }); + + it('should throw when trying to open a dialog with the same id as another dialog', () => { + dialog.open(PizzaMsg, { id: 'pizza' }); + expect(() => dialog.open(PizzaMsg, { id: 'pizza' })).toThrowError(/must be unique/g); + }); + + it('should be able to find a dialog by id', () => { + const dialogRef = dialog.open(PizzaMsg, { id: 'pizza' }); + expect(dialog.getDialogById('pizza')).toBe(dialogRef); + }); + describe('disableClose option', () => { it('should prevent closing via clicks on the backdrop', () => { dialog.open(PizzaMsg, { diff --git a/src/lib/dialog/dialog.ts b/src/lib/dialog/dialog.ts index 26ba124eb4e9..6c9364a02ee4 100644 --- a/src/lib/dialog/dialog.ts +++ b/src/lib/dialog/dialog.ts @@ -38,6 +38,8 @@ import {MdDialogConfig} from './dialog-config'; import {MdDialogRef} from './dialog-ref'; import {MdDialogContainer} from './dialog-container'; import {TemplatePortal} from '../core/portal/portal'; +import {defer} from 'rxjs/observable/defer'; +import {startWith} from '../core/rxjs/index'; export const MD_DIALOG_DATA = new InjectionToken('MdDialogData'); @@ -70,26 +72,27 @@ export class MdDialog { private _boundKeydown = this._handleKeydown.bind(this); /** Keeps track of the currently-open dialogs. */ - get _openDialogs(): MdDialogRef[] { - return this._parentDialog ? this._parentDialog._openDialogs : this._openDialogsAtThisLevel; + get openDialogs(): MdDialogRef[] { + return this._parentDialog ? this._parentDialog.openDialogs : this._openDialogsAtThisLevel; } - /** Subject for notifying the user that a dialog has opened. */ - get _afterOpen(): Subject> { - return this._parentDialog ? this._parentDialog._afterOpen : this._afterOpenAtThisLevel; + /** Stream that emits when a dialog has been opened. */ + get afterOpen(): Subject> { + return this._parentDialog ? this._parentDialog.afterOpen : this._afterOpenAtThisLevel; } - /** Subject for notifying the user that all open dialogs have finished closing. */ - get _afterAllClosed(): Subject { - return this._parentDialog ? - this._parentDialog._afterAllClosed : this._afterAllClosedAtThisLevel; + get _afterAllClosed() { + const parent = this._parentDialog; + return parent ? parent._afterAllClosed : this._afterAllClosedAtThisLevel; } - /** Gets an observable that is notified when a dialog has been opened. */ - afterOpen: Observable> = this._afterOpen.asObservable(); - - /** Gets an observable that is notified when all open dialog have finished closing. */ - afterAllClosed: Observable = this._afterAllClosed.asObservable(); + /** + * Stream that emits when all open dialog have finished closing. + * Will emit on subscribe if there are no open dialogs to begin with. + */ + afterAllClosed: Observable = defer(() => this.openDialogs.length ? + this._afterAllClosed : + startWith.call(this._afterAllClosed, undefined)); constructor( private _overlay: Overlay, @@ -116,7 +119,7 @@ export class MdDialog { open(componentOrTemplateRef: ComponentType | TemplateRef, config?: MdDialogConfig): MdDialogRef { - const inProgressDialog = this._openDialogs.find(dialog => dialog._isAnimating()); + const inProgressDialog = this.openDialogs.find(dialog => dialog._isAnimating()); // If there's a dialog that is in the process of being opened, return it instead. if (inProgressDialog) { @@ -125,18 +128,22 @@ export class MdDialog { config = _applyConfigDefaults(config); + if (config.id && this.getDialogById(config.id)) { + throw Error(`Dialog with id "${config.id}" exists already. The dialog id must be unique.`); + } + const overlayRef = this._createOverlay(config); const dialogContainer = this._attachDialogContainer(overlayRef, config); const dialogRef = this._attachDialogContent(componentOrTemplateRef, dialogContainer, overlayRef, config); - if (!this._openDialogs.length) { + if (!this.openDialogs.length) { document.addEventListener('keydown', this._boundKeydown); } - this._openDialogs.push(dialogRef); + this.openDialogs.push(dialogRef); dialogRef.afterClosed().subscribe(() => this._removeOpenDialog(dialogRef)); - this._afterOpen.next(dialogRef); + this.afterOpen.next(dialogRef); return dialogRef; } @@ -145,24 +152,32 @@ export class MdDialog { * Closes all of the currently-open dialogs. */ closeAll(): void { - let i = this._openDialogs.length; + let i = this.openDialogs.length; while (i--) { // The `_openDialogs` property isn't updated after close until the rxjs subscription // runs on the next microtask, in addition to modifying the array as we're going // through it. We loop through all of them and call close without assuming that // they'll be removed from the list instantaneously. - this._openDialogs[i].close(); + this.openDialogs[i].close(); } } + /** + * Finds an open dialog by its id. + * @param id ID to use when looking up the dialog. + */ + getDialogById(id: string): MdDialogRef | undefined { + return this.openDialogs.find(dialog => dialog.id === id); + } + /** * Creates the overlay into which the dialog will be loaded. * @param config The dialog configuration. * @returns A promise resolving to the OverlayRef for the created overlay. */ private _createOverlay(config: MdDialogConfig): OverlayRef { - let overlayState = this._getOverlayState(config); + const overlayState = this._getOverlayState(config); return this._overlay.create(overlayState); } @@ -172,7 +187,7 @@ export class MdDialog { * @returns The overlay configuration. */ private _getOverlayState(dialogConfig: MdDialogConfig): OverlayState { - let overlayState = new OverlayState(); + const overlayState = new OverlayState(); overlayState.panelClass = dialogConfig.panelClass; overlayState.hasBackdrop = dialogConfig.hasBackdrop; overlayState.scrollStrategy = this._scrollStrategy(); @@ -216,7 +231,7 @@ export class MdDialog { // 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, dialogContainer); + const dialogRef = new MdDialogRef(overlayRef, dialogContainer, config.id); // When the dialog backdrop is clicked, we want to close it. if (config.hasBackdrop) { @@ -230,8 +245,8 @@ export class MdDialog { if (componentOrTemplateRef instanceof TemplateRef) { dialogContainer.attachTemplatePortal(new TemplatePortal(componentOrTemplateRef, null!)); } else { - let injector = this._createInjector(config, dialogRef, dialogContainer); - let contentRef = dialogContainer.attachComponentPortal( + const injector = this._createInjector(config, dialogRef, dialogContainer); + const contentRef = dialogContainer.attachComponentPortal( new ComponentPortal(componentOrTemplateRef, undefined, injector)); dialogRef.componentInstance = contentRef.instance; } @@ -256,8 +271,8 @@ export class MdDialog { dialogRef: MdDialogRef, dialogContainer: MdDialogContainer): PortalInjector { - let userInjector = config && config.viewContainerRef && config.viewContainerRef.injector; - let injectionTokens = new WeakMap(); + const userInjector = config && config.viewContainerRef && config.viewContainerRef.injector; + const injectionTokens = new WeakMap(); injectionTokens.set(MdDialogRef, dialogRef); injectionTokens.set(MdDialogContainer, dialogContainer); @@ -271,13 +286,13 @@ export class MdDialog { * @param dialogRef Dialog to be removed. */ private _removeOpenDialog(dialogRef: MdDialogRef) { - let index = this._openDialogs.indexOf(dialogRef); + const index = this.openDialogs.indexOf(dialogRef); if (index > -1) { - this._openDialogs.splice(index, 1); + this.openDialogs.splice(index, 1); // no open dialogs are left, call next on afterAllClosed Subject - if (!this._openDialogs.length) { + if (!this.openDialogs.length) { this._afterAllClosed.next(); document.removeEventListener('keydown', this._boundKeydown); } @@ -289,8 +304,8 @@ export class MdDialog { * top dialog when the user presses escape. */ private _handleKeydown(event: KeyboardEvent): void { - let topDialog = this._openDialogs[this._openDialogs.length - 1]; - let canClose = topDialog ? !topDialog.disableClose : false; + const topDialog = this.openDialogs[this.openDialogs.length - 1]; + const canClose = topDialog ? !topDialog.disableClose : false; if (event.keyCode === ESCAPE && canClose) { topDialog.close(); diff --git a/tools/package-tools/rollup-globals.ts b/tools/package-tools/rollup-globals.ts index aec25e1ae6f3..55e513bb9537 100644 --- a/tools/package-tools/rollup-globals.ts +++ b/tools/package-tools/rollup-globals.ts @@ -56,6 +56,7 @@ export const rollupGlobals = { 'rxjs/observable/merge': 'Rx.Observable', 'rxjs/observable/of': 'Rx.Observable', 'rxjs/observable/throw': 'Rx.Observable', + 'rxjs/observable/defer': 'Rx.Observable', 'rxjs/operator/auditTime': 'Rx.Observable.prototype', 'rxjs/operator/catch': 'Rx.Observable.prototype', 'rxjs/operator/debounceTime': 'Rx.Observable.prototype',