mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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:
committed by
mmalerba
parent
667a4e4405
commit
8b54715053
@@ -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';
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user