Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(dialog): open dialog API improvements #6289

Merged
merged 1 commit into from
Aug 7, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/lib/dialog/dialog-config.ts
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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