Skip to content

Commit 8b54715

Browse files
crisbetommalerba
authored andcommitted
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.
1 parent 667a4e4 commit 8b54715

File tree

5 files changed

+96
-48
lines changed

5 files changed

+96
-48
lines changed

src/lib/dialog/dialog-config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ export class MdDialogConfig {
3333
*/
3434
viewContainerRef?: ViewContainerRef;
3535

36+
/** ID for the dialog. If omitted, a unique one will be generated. */
37+
id?: string;
38+
3639
/** The ARIA role of the dialog element. */
3740
role?: DialogRole = 'dialog';
3841

src/lib/dialog/dialog-ref.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import {RxChain, first, filter} from '../core/rxjs/index';
1717
// TODO(jelbourn): resizing
1818
// TODO(jelbourn): afterOpen and beforeClose
1919

20+
// Counter for unique dialog ids.
21+
let uniqueId = 0;
2022

2123
/**
2224
* Reference to a dialog opened via the MdDialog service.
@@ -34,7 +36,11 @@ export class MdDialogRef<T> {
3436
/** Result to be passed to afterClosed. */
3537
private _result: any;
3638

37-
constructor(private _overlayRef: OverlayRef, private _containerInstance: MdDialogContainer) {
39+
constructor(
40+
private _overlayRef: OverlayRef,
41+
private _containerInstance: MdDialogContainer,
42+
public readonly id: string = `md-dialog-${uniqueId++}`) {
43+
3844
RxChain.from(_containerInstance._animationStateChanged)
3945
.call(filter, event => event.phaseName === 'done' && event.toState === 'exit')
4046
.call(first)

src/lib/dialog/dialog.spec.ts

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -205,33 +205,32 @@ describe('MdDialog', () => {
205205
});
206206

207207
it('should notify the observers if all open dialogs have finished closing', async(() => {
208-
const ref1 = dialog.open(PizzaMsg, {
209-
viewContainerRef: testViewContainerRef
210-
});
211-
const ref2 = dialog.open(ContentElementDialog, {
212-
viewContainerRef: testViewContainerRef
213-
});
214-
let allClosed = false;
208+
const ref1 = dialog.open(PizzaMsg, { viewContainerRef: testViewContainerRef });
209+
const ref2 = dialog.open(ContentElementDialog, { viewContainerRef: testViewContainerRef });
210+
const spy = jasmine.createSpy('afterAllClosed spy');
215211

216-
dialog.afterAllClosed.subscribe(() => {
217-
allClosed = true;
218-
});
212+
dialog.afterAllClosed.subscribe(spy);
219213

220214
ref1.close();
221215
viewContainerFixture.detectChanges();
222216

223217
viewContainerFixture.whenStable().then(() => {
224-
expect(allClosed).toBeFalsy();
218+
expect(spy).not.toHaveBeenCalled();
225219

226220
ref2.close();
227221
viewContainerFixture.detectChanges();
228-
229-
viewContainerFixture.whenStable().then(() => {
230-
expect(allClosed).toBeTruthy();
231-
});
222+
viewContainerFixture.whenStable().then(() => expect(spy).toHaveBeenCalled());
232223
});
233224
}));
234225

226+
it('should emit the afterAllClosed stream on subscribe if there are no open dialogs', () => {
227+
const spy = jasmine.createSpy('afterAllClosed spy');
228+
229+
dialog.afterAllClosed.subscribe(spy);
230+
231+
expect(spy).toHaveBeenCalled();
232+
});
233+
235234
it('should should override the width of the overlay pane', () => {
236235
dialog.open(PizzaMsg, {
237236
width: '500px'
@@ -468,6 +467,30 @@ describe('MdDialog', () => {
468467
});
469468
}));
470469

470+
it('should assign a unique id to each dialog', () => {
471+
const one = dialog.open(PizzaMsg);
472+
const two = dialog.open(PizzaMsg);
473+
474+
expect(one.id).toBeTruthy();
475+
expect(two.id).toBeTruthy();
476+
expect(one.id).not.toBe(two.id);
477+
});
478+
479+
it('should allow for the id to be overwritten', () => {
480+
const dialogRef = dialog.open(PizzaMsg, { id: 'pizza' });
481+
expect(dialogRef.id).toBe('pizza');
482+
});
483+
484+
it('should throw when trying to open a dialog with the same id as another dialog', () => {
485+
dialog.open(PizzaMsg, { id: 'pizza' });
486+
expect(() => dialog.open(PizzaMsg, { id: 'pizza' })).toThrowError(/must be unique/g);
487+
});
488+
489+
it('should be able to find a dialog by id', () => {
490+
const dialogRef = dialog.open(PizzaMsg, { id: 'pizza' });
491+
expect(dialog.getDialogById('pizza')).toBe(dialogRef);
492+
});
493+
471494
describe('disableClose option', () => {
472495
it('should prevent closing via clicks on the backdrop', () => {
473496
dialog.open(PizzaMsg, {

src/lib/dialog/dialog.ts

Lines changed: 47 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ import {MdDialogConfig} from './dialog-config';
3838
import {MdDialogRef} from './dialog-ref';
3939
import {MdDialogContainer} from './dialog-container';
4040
import {TemplatePortal} from '../core/portal/portal';
41+
import {defer} from 'rxjs/observable/defer';
42+
import {startWith} from '../core/rxjs/index';
4143

4244
export const MD_DIALOG_DATA = new InjectionToken<any>('MdDialogData');
4345

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

7274
/** Keeps track of the currently-open dialogs. */
73-
get _openDialogs(): MdDialogRef<any>[] {
74-
return this._parentDialog ? this._parentDialog._openDialogs : this._openDialogsAtThisLevel;
75+
get openDialogs(): MdDialogRef<any>[] {
76+
return this._parentDialog ? this._parentDialog.openDialogs : this._openDialogsAtThisLevel;
7577
}
7678

77-
/** Subject for notifying the user that a dialog has opened. */
78-
get _afterOpen(): Subject<MdDialogRef<any>> {
79-
return this._parentDialog ? this._parentDialog._afterOpen : this._afterOpenAtThisLevel;
79+
/** Stream that emits when a dialog has been opened. */
80+
get afterOpen(): Subject<MdDialogRef<any>> {
81+
return this._parentDialog ? this._parentDialog.afterOpen : this._afterOpenAtThisLevel;
8082
}
8183

82-
/** Subject for notifying the user that all open dialogs have finished closing. */
83-
get _afterAllClosed(): Subject<void> {
84-
return this._parentDialog ?
85-
this._parentDialog._afterAllClosed : this._afterAllClosedAtThisLevel;
84+
get _afterAllClosed() {
85+
const parent = this._parentDialog;
86+
return parent ? parent._afterAllClosed : this._afterAllClosedAtThisLevel;
8687
}
8788

88-
/** Gets an observable that is notified when a dialog has been opened. */
89-
afterOpen: Observable<MdDialogRef<any>> = this._afterOpen.asObservable();
90-
91-
/** Gets an observable that is notified when all open dialog have finished closing. */
92-
afterAllClosed: Observable<void> = this._afterAllClosed.asObservable();
89+
/**
90+
* Stream that emits when all open dialog have finished closing.
91+
* Will emit on subscribe if there are no open dialogs to begin with.
92+
*/
93+
afterAllClosed: Observable<void> = defer<void>(() => this.openDialogs.length ?
94+
this._afterAllClosed :
95+
startWith.call(this._afterAllClosed, undefined));
9396

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

119-
const inProgressDialog = this._openDialogs.find(dialog => dialog._isAnimating());
122+
const inProgressDialog = this.openDialogs.find(dialog => dialog._isAnimating());
120123

121124
// If there's a dialog that is in the process of being opened, return it instead.
122125
if (inProgressDialog) {
@@ -125,18 +128,22 @@ export class MdDialog {
125128

126129
config = _applyConfigDefaults(config);
127130

131+
if (config.id && this.getDialogById(config.id)) {
132+
throw Error(`Dialog with id "${config.id}" exists already. The dialog id must be unique.`);
133+
}
134+
128135
const overlayRef = this._createOverlay(config);
129136
const dialogContainer = this._attachDialogContainer(overlayRef, config);
130137
const dialogRef =
131138
this._attachDialogContent(componentOrTemplateRef, dialogContainer, overlayRef, config);
132139

133-
if (!this._openDialogs.length) {
140+
if (!this.openDialogs.length) {
134141
document.addEventListener('keydown', this._boundKeydown);
135142
}
136143

137-
this._openDialogs.push(dialogRef);
144+
this.openDialogs.push(dialogRef);
138145
dialogRef.afterClosed().subscribe(() => this._removeOpenDialog(dialogRef));
139-
this._afterOpen.next(dialogRef);
146+
this.afterOpen.next(dialogRef);
140147

141148
return dialogRef;
142149
}
@@ -145,24 +152,32 @@ export class MdDialog {
145152
* Closes all of the currently-open dialogs.
146153
*/
147154
closeAll(): void {
148-
let i = this._openDialogs.length;
155+
let i = this.openDialogs.length;
149156

150157
while (i--) {
151158
// The `_openDialogs` property isn't updated after close until the rxjs subscription
152159
// runs on the next microtask, in addition to modifying the array as we're going
153160
// through it. We loop through all of them and call close without assuming that
154161
// they'll be removed from the list instantaneously.
155-
this._openDialogs[i].close();
162+
this.openDialogs[i].close();
156163
}
157164
}
158165

166+
/**
167+
* Finds an open dialog by its id.
168+
* @param id ID to use when looking up the dialog.
169+
*/
170+
getDialogById(id: string): MdDialogRef<any> | undefined {
171+
return this.openDialogs.find(dialog => dialog.id === id);
172+
}
173+
159174
/**
160175
* Creates the overlay into which the dialog will be loaded.
161176
* @param config The dialog configuration.
162177
* @returns A promise resolving to the OverlayRef for the created overlay.
163178
*/
164179
private _createOverlay(config: MdDialogConfig): OverlayRef {
165-
let overlayState = this._getOverlayState(config);
180+
const overlayState = this._getOverlayState(config);
166181
return this._overlay.create(overlayState);
167182
}
168183

@@ -172,7 +187,7 @@ export class MdDialog {
172187
* @returns The overlay configuration.
173188
*/
174189
private _getOverlayState(dialogConfig: MdDialogConfig): OverlayState {
175-
let overlayState = new OverlayState();
190+
const overlayState = new OverlayState();
176191
overlayState.panelClass = dialogConfig.panelClass;
177192
overlayState.hasBackdrop = dialogConfig.hasBackdrop;
178193
overlayState.scrollStrategy = this._scrollStrategy();
@@ -216,7 +231,7 @@ export class MdDialog {
216231

217232
// Create a reference to the dialog we're creating in order to give the user a handle
218233
// to modify and close it.
219-
let dialogRef = new MdDialogRef<T>(overlayRef, dialogContainer);
234+
const dialogRef = new MdDialogRef<T>(overlayRef, dialogContainer, config.id);
220235

221236
// When the dialog backdrop is clicked, we want to close it.
222237
if (config.hasBackdrop) {
@@ -230,8 +245,8 @@ export class MdDialog {
230245
if (componentOrTemplateRef instanceof TemplateRef) {
231246
dialogContainer.attachTemplatePortal(new TemplatePortal(componentOrTemplateRef, null!));
232247
} else {
233-
let injector = this._createInjector<T>(config, dialogRef, dialogContainer);
234-
let contentRef = dialogContainer.attachComponentPortal(
248+
const injector = this._createInjector<T>(config, dialogRef, dialogContainer);
249+
const contentRef = dialogContainer.attachComponentPortal(
235250
new ComponentPortal(componentOrTemplateRef, undefined, injector));
236251
dialogRef.componentInstance = contentRef.instance;
237252
}
@@ -256,8 +271,8 @@ export class MdDialog {
256271
dialogRef: MdDialogRef<T>,
257272
dialogContainer: MdDialogContainer): PortalInjector {
258273

259-
let userInjector = config && config.viewContainerRef && config.viewContainerRef.injector;
260-
let injectionTokens = new WeakMap();
274+
const userInjector = config && config.viewContainerRef && config.viewContainerRef.injector;
275+
const injectionTokens = new WeakMap();
261276

262277
injectionTokens.set(MdDialogRef, dialogRef);
263278
injectionTokens.set(MdDialogContainer, dialogContainer);
@@ -271,13 +286,13 @@ export class MdDialog {
271286
* @param dialogRef Dialog to be removed.
272287
*/
273288
private _removeOpenDialog(dialogRef: MdDialogRef<any>) {
274-
let index = this._openDialogs.indexOf(dialogRef);
289+
const index = this.openDialogs.indexOf(dialogRef);
275290

276291
if (index > -1) {
277-
this._openDialogs.splice(index, 1);
292+
this.openDialogs.splice(index, 1);
278293

279294
// no open dialogs are left, call next on afterAllClosed Subject
280-
if (!this._openDialogs.length) {
295+
if (!this.openDialogs.length) {
281296
this._afterAllClosed.next();
282297
document.removeEventListener('keydown', this._boundKeydown);
283298
}
@@ -289,8 +304,8 @@ export class MdDialog {
289304
* top dialog when the user presses escape.
290305
*/
291306
private _handleKeydown(event: KeyboardEvent): void {
292-
let topDialog = this._openDialogs[this._openDialogs.length - 1];
293-
let canClose = topDialog ? !topDialog.disableClose : false;
307+
const topDialog = this.openDialogs[this.openDialogs.length - 1];
308+
const canClose = topDialog ? !topDialog.disableClose : false;
294309

295310
if (event.keyCode === ESCAPE && canClose) {
296311
topDialog.close();

tools/package-tools/rollup-globals.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export const rollupGlobals = {
5656
'rxjs/observable/merge': 'Rx.Observable',
5757
'rxjs/observable/of': 'Rx.Observable',
5858
'rxjs/observable/throw': 'Rx.Observable',
59+
'rxjs/observable/defer': 'Rx.Observable',
5960
'rxjs/operator/auditTime': 'Rx.Observable.prototype',
6061
'rxjs/operator/catch': 'Rx.Observable.prototype',
6162
'rxjs/operator/debounceTime': 'Rx.Observable.prototype',

0 commit comments

Comments
 (0)