Skip to content

Commit

Permalink
feat(dialog): open dialog API improvements (#6289)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
crisbeto authored and mmalerba committed Aug 7, 2017
1 parent 667a4e4 commit 8b54715
Show file tree
Hide file tree
Showing 5 changed files with 96 additions and 48 deletions.
3 changes: 3 additions & 0 deletions src/lib/dialog/dialog-config.ts
Expand Up @@ -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';

Expand Down
8 changes: 7 additions & 1 deletion src/lib/dialog/dialog-ref.ts
Expand Up @@ -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.
Expand All @@ -34,7 +36,11 @@ export class MdDialogRef<T> {
/** 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)
Expand Down
53 changes: 38 additions & 15 deletions src/lib/dialog/dialog.spec.ts
Expand Up @@ -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'
Expand Down Expand Up @@ -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, {
Expand Down
79 changes: 47 additions & 32 deletions src/lib/dialog/dialog.ts
Expand Up @@ -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<any>('MdDialogData');

Expand Down Expand Up @@ -70,26 +72,27 @@ export class MdDialog {
private _boundKeydown = this._handleKeydown.bind(this);

/** Keeps track of the currently-open dialogs. */
get _openDialogs(): MdDialogRef<any>[] {
return this._parentDialog ? this._parentDialog._openDialogs : this._openDialogsAtThisLevel;
get openDialogs(): MdDialogRef<any>[] {
return this._parentDialog ? this._parentDialog.openDialogs : this._openDialogsAtThisLevel;
}

/** Subject for notifying the user that a dialog has opened. */
get _afterOpen(): Subject<MdDialogRef<any>> {
return this._parentDialog ? this._parentDialog._afterOpen : this._afterOpenAtThisLevel;
/** Stream that emits when a dialog has been opened. */
get afterOpen(): Subject<MdDialogRef<any>> {
return this._parentDialog ? this._parentDialog.afterOpen : this._afterOpenAtThisLevel;
}

/** Subject for notifying the user that all open dialogs have finished closing. */
get _afterAllClosed(): Subject<void> {
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<MdDialogRef<any>> = this._afterOpen.asObservable();

/** Gets an observable that is notified when all open dialog have finished closing. */
afterAllClosed: Observable<void> = 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<void> = defer<void>(() => this.openDialogs.length ?
this._afterAllClosed :
startWith.call(this._afterAllClosed, undefined));

constructor(
private _overlay: Overlay,
Expand All @@ -116,7 +119,7 @@ export class MdDialog {
open<T>(componentOrTemplateRef: ComponentType<T> | TemplateRef<T>,
config?: MdDialogConfig): MdDialogRef<T> {

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) {
Expand All @@ -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;
}
Expand All @@ -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<any> | 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);
}

Expand All @@ -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();
Expand Down Expand Up @@ -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<T>(overlayRef, dialogContainer);
const dialogRef = new MdDialogRef<T>(overlayRef, dialogContainer, config.id);

// When the dialog backdrop is clicked, we want to close it.
if (config.hasBackdrop) {
Expand All @@ -230,8 +245,8 @@ export class MdDialog {
if (componentOrTemplateRef instanceof TemplateRef) {
dialogContainer.attachTemplatePortal(new TemplatePortal(componentOrTemplateRef, null!));
} else {
let injector = this._createInjector<T>(config, dialogRef, dialogContainer);
let contentRef = dialogContainer.attachComponentPortal(
const injector = this._createInjector<T>(config, dialogRef, dialogContainer);
const contentRef = dialogContainer.attachComponentPortal(
new ComponentPortal(componentOrTemplateRef, undefined, injector));
dialogRef.componentInstance = contentRef.instance;
}
Expand All @@ -256,8 +271,8 @@ export class MdDialog {
dialogRef: MdDialogRef<T>,
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);
Expand All @@ -271,13 +286,13 @@ export class MdDialog {
* @param dialogRef Dialog to be removed.
*/
private _removeOpenDialog(dialogRef: MdDialogRef<any>) {
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);
}
Expand All @@ -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();
Expand Down
1 change: 1 addition & 0 deletions tools/package-tools/rollup-globals.ts
Expand Up @@ -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',
Expand Down

0 comments on commit 8b54715

Please sign in to comment.