feat(dialog): open dialog API improvements (#6289)

* 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.
This commit is contained in:
Kristiyan Kostadinov
2017-08-07 23:23:21 +02:00
committed by mmalerba
parent 667a4e4405
commit 8b54715053
5 changed files with 96 additions and 48 deletions
+3
View File
@@ -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';
+7 -1
View File
@@ -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<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)
+38 -15
View File
@@ -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, {
+47 -32
View File
@@ -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');
@@ -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,
@@ -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) {
@@ -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<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);
}
@@ -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<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) {
@@ -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;
}
@@ -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);
@@ -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);
}
@@ -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();
+1
View File
@@ -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',