Skip to content

Commit ff95263

Browse files
committed
feat(overlay): make overlays synchronous
1 parent b5e1e33 commit ff95263

File tree

16 files changed

+169
-311
lines changed

16 files changed

+169
-311
lines changed
Lines changed: 8 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
import {Component, ComponentRef, ViewChild, AfterViewInit} from '@angular/core';
1+
import {Component, ComponentRef, ViewChild} from '@angular/core';
22
import {
33
BasePortalHost,
44
ComponentPortal,
55
TemplatePortal
66
} from '@angular2-material/core/portal/portal';
77
import {PortalHostDirective} from '@angular2-material/core/portal/portal-directives';
8-
import {PromiseCompleter} from '@angular2-material/core/async/promise-completer';
98
import {MdDialogConfig} from './dialog-config';
109
import {MdDialogContentAlreadyAttachedError} from './dialog-errors';
1110

@@ -23,63 +22,23 @@ import {MdDialogContentAlreadyAttachedError} from './dialog-errors';
2322
'[attr.role]': 'dialogConfig?.role'
2423
}
2524
})
26-
export class MdDialogContainer extends BasePortalHost implements AfterViewInit {
25+
export class MdDialogContainer extends BasePortalHost {
2726
/** The portal host inside of this container into which the dialog content will be loaded. */
2827
@ViewChild(PortalHostDirective) _portalHost: PortalHostDirective;
2928

30-
/**
31-
* Completer used to resolve the promise for cases when a portal is attempted to be attached,
32-
* but AfterViewInit has not yet occured.
33-
*/
34-
private _deferredAttachCompleter: PromiseCompleter<ComponentRef<any>>;
35-
36-
/** Portal to be attached upon AfterViewInit. */
37-
private _deferredAttachPortal: ComponentPortal<any>;
38-
3929
/** The dialog configuration. */
4030
dialogConfig: MdDialogConfig;
4131

42-
/** TODO: internal */
43-
ngAfterViewInit() {
44-
// If there was an attempted call to `attachComponentPortal` before this lifecycle stage,
45-
// we actually perform the attachment now that the `@ViewChild` is resolved.
46-
if (this._deferredAttachCompleter) {
47-
this.attachComponentPortal(this._deferredAttachPortal).then(componentRef => {
48-
this._deferredAttachCompleter.resolve(componentRef);
49-
50-
this._deferredAttachPortal = null;
51-
this._deferredAttachCompleter = null;
52-
}, () => {
53-
this._deferredAttachCompleter.reject();
54-
this._deferredAttachCompleter = null;
55-
this._deferredAttachPortal = null;
56-
});
57-
}
58-
}
59-
6032
/** Attach a portal as content to this dialog container. */
61-
attachComponentPortal<T>(portal: ComponentPortal<T>): Promise<ComponentRef<T>> {
62-
if (this._portalHost) {
63-
if (this._portalHost.hasAttached()) {
64-
throw new MdDialogContentAlreadyAttachedError();
65-
}
66-
67-
return this._portalHost.attachComponentPortal(portal);
68-
} else {
69-
// The @ViewChild query for the portalHost is not resolved until AfterViewInit, but this
70-
// function may be called before this lifecycle event. As such, we defer the attachment of
71-
// the portal until AfterViewInit.
72-
if (this._deferredAttachCompleter) {
73-
throw new MdDialogContentAlreadyAttachedError();
74-
}
75-
76-
this._deferredAttachPortal = portal;
77-
this._deferredAttachCompleter = new PromiseCompleter();
78-
return this._deferredAttachCompleter.promise;
33+
attachComponentPortal<T>(portal: ComponentPortal<T>): ComponentRef<T> {
34+
if (this._portalHost.hasAttached()) {
35+
throw new MdDialogContentAlreadyAttachedError();
7936
}
37+
38+
return this._portalHost.attachComponentPortal(portal);
8039
}
8140

82-
attachTemplatePortal(portal: TemplatePortal): Promise<Map<string, any>> {
41+
attachTemplatePortal(portal: TemplatePortal): Map<string, any> {
8342
throw Error('Not yet implemented');
8443
}
8544
}

src/components/dialog/dialog.spec.ts

Lines changed: 33 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {inject, fakeAsync, async, ComponentFixture, TestBed} from '@angular/core/testing';
1+
import {inject, async, ComponentFixture, TestBed} from '@angular/core/testing';
22
import {NgModule, Component, Directive, ViewChild, ViewContainerRef} from '@angular/core';
33
import {MdDialog, MdDialogModule} from './dialog';
44
import {OverlayContainer} from '@angular2-material/core/overlay/overlay-container';
@@ -27,9 +27,9 @@ describe('MdDialog', () => {
2727
TestBed.compileComponents();
2828
}));
2929

30-
beforeEach(inject([MdDialog], fakeAsync((d: MdDialog) => {
30+
beforeEach(inject([MdDialog], (d: MdDialog) => {
3131
dialog = d;
32-
})));
32+
}));
3333

3434
beforeEach(() => {
3535
viewContainerFixture = TestBed.createComponent(ComponentWithChildViewContainer);
@@ -38,72 +38,58 @@ describe('MdDialog', () => {
3838
testViewContainerRef = viewContainerFixture.componentInstance.childViewContainer;
3939
});
4040

41-
it('should open a dialog with a component', async(() => {
41+
it('should open a dialog with a component', () => {
4242
let config = new MdDialogConfig();
4343
config.viewContainerRef = testViewContainerRef;
4444

45-
dialog.open(PizzaMsg, config).then(dialogRef => {
46-
expect(overlayContainerElement.textContent).toContain('Pizza');
47-
expect(dialogRef.componentInstance).toEqual(jasmine.any(PizzaMsg));
48-
expect(dialogRef.componentInstance.dialogRef).toBe(dialogRef);
45+
let dialogRef = dialog.open(PizzaMsg, config);
4946

50-
viewContainerFixture.detectChanges();
51-
let dialogContainerElement = overlayContainerElement.querySelector('md-dialog-container');
52-
expect(dialogContainerElement.getAttribute('role')).toBe('dialog');
53-
});
47+
viewContainerFixture.detectChanges();
5448

55-
detectChangesForDialogOpen(viewContainerFixture);
56-
}));
49+
expect(overlayContainerElement.textContent).toContain('Pizza');
50+
expect(dialogRef.componentInstance).toEqual(jasmine.any(PizzaMsg));
51+
expect(dialogRef.componentInstance.dialogRef).toBe(dialogRef);
52+
53+
viewContainerFixture.detectChanges();
54+
let dialogContainerElement = overlayContainerElement.querySelector('md-dialog-container');
55+
expect(dialogContainerElement.getAttribute('role')).toBe('dialog');
56+
});
5757

58-
it('should apply the configured role to the dialog element', async(() => {
58+
it('should apply the configured role to the dialog element', () => {
5959
let config = new MdDialogConfig();
6060
config.viewContainerRef = testViewContainerRef;
6161
config.role = 'alertdialog';
6262

63-
dialog.open(PizzaMsg, config).then(dialogRef => {
64-
viewContainerFixture.detectChanges();
63+
dialog.open(PizzaMsg, config);
6564

66-
let dialogContainerElement = overlayContainerElement.querySelector('md-dialog-container');
67-
expect(dialogContainerElement.getAttribute('role')).toBe('alertdialog');
68-
});
65+
viewContainerFixture.detectChanges();
6966

70-
detectChangesForDialogOpen(viewContainerFixture);
71-
}));
67+
let dialogContainerElement = overlayContainerElement.querySelector('md-dialog-container');
68+
expect(dialogContainerElement.getAttribute('role')).toBe('alertdialog');
69+
});
7270

73-
it('should close a dialog and get back a result', async(() => {
71+
it('should close a dialog and get back a result', () => {
7472
let config = new MdDialogConfig();
7573
config.viewContainerRef = testViewContainerRef;
7674

77-
dialog.open(PizzaMsg, config).then(dialogRef => {
78-
viewContainerFixture.detectChanges();
75+
let dialogRef = dialog.open(PizzaMsg, config);
7976

80-
let afterCloseResult: string;
81-
dialogRef.afterClosed().subscribe(result => {
82-
afterCloseResult = result;
83-
});
77+
viewContainerFixture.detectChanges();
8478

85-
dialogRef.close('Charmander');
79+
viewContainerFixture.detectChanges();
8680

87-
viewContainerFixture.whenStable().then(() => {
88-
expect(afterCloseResult).toBe('Charmander');
89-
expect(overlayContainerElement.childNodes.length).toBe(0);
90-
});
81+
let afterCloseResult: string;
82+
dialogRef.afterClosed().subscribe(result => {
83+
afterCloseResult = result;
9184
});
9285

93-
detectChangesForDialogOpen(viewContainerFixture);
94-
}));
95-
});
86+
dialogRef.close('Charmander');
9687

88+
expect(afterCloseResult).toBe('Charmander');
89+
expect(overlayContainerElement.querySelector('md-dialog-container')).toBeNull();
90+
});
91+
});
9792

98-
/** Runs the necessary detectChanges for a dialog to complete its opening. */
99-
function detectChangesForDialogOpen(fixture: ComponentFixture<ComponentWithChildViewContainer>) {
100-
// TODO(jelbourn): figure out why the test zone is "stable" when there are still pending
101-
// tasks, such that we have to use `setTimeout` to run the second round of change detection.
102-
// Two rounds of change detection are necessary: one to *create* the dialog container, and
103-
// another to cause the lifecycle events of the container to run and load the dialog content.
104-
fixture.detectChanges();
105-
setTimeout(() => fixture.detectChanges(), 50);
106-
}
10793

10894
@Directive({selector: 'dir-with-view-container'})
10995
class DirectiveWithViewContainer {
@@ -123,10 +109,7 @@ class ComponentWithChildViewContainer {
123109
}
124110

125111
/** Simple component for testing ComponentPortal. */
126-
@Component({
127-
selector: 'pizza-msg',
128-
template: '<p>Pizza</p>',
129-
})
112+
@Component({template: '<p>Pizza</p>'})
130113
class PizzaMsg {
131114
constructor(public dialogRef: MdDialogRef<PizzaMsg>) { }
132115
}

src/components/dialog/dialog.ts

Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -39,21 +39,21 @@ export class MdDialog {
3939
* @param component Type of the component to load into the load.
4040
* @param config
4141
*/
42-
open<T>(component: ComponentType<T>, config: MdDialogConfig): Promise<MdDialogRef<T>> {
43-
let overlayRef: OverlayRef;
42+
open<T>(component: ComponentType<T>, config: MdDialogConfig): MdDialogRef<T> {
43+
let overlayRef = this._createOverlay(config);
44+
let dialogContainer = this._attachDialogContainer(overlayRef, config);
4445

45-
return this._createOverlay(config)
46-
.then(overlay => overlayRef = overlay)
47-
.then(overlay => this._attachDialogContainer(overlay, config))
48-
.then(containerRef => this._attachDialogContent(component, containerRef, overlayRef));
46+
// TODO: probably need to wait for dialogContainer ngOnInit before attaching.
47+
48+
return this._attachDialogContent(component, dialogContainer, overlayRef);
4949
}
5050

5151
/**
5252
* Creates the overlay into which the dialog will be loaded.
5353
* @param dialogConfig The dialog configuration.
5454
* @returns A promise resolving to the OverlayRef for the created overlay.
5555
*/
56-
private _createOverlay(dialogConfig: MdDialogConfig): Promise<OverlayRef> {
56+
private _createOverlay(dialogConfig: MdDialogConfig): OverlayRef {
5757
let overlayState = this._getOverlayState(dialogConfig);
5858
return this._overlay.create(overlayState);
5959
}
@@ -64,43 +64,41 @@ export class MdDialog {
6464
* @param config The dialog configuration.
6565
* @returns A promise resolving to a ComponentRef for the attached container.
6666
*/
67-
private _attachDialogContainer(overlay: OverlayRef, config: MdDialogConfig):
68-
Promise<ComponentRef<MdDialogContainer>> {
67+
private _attachDialogContainer(overlay: OverlayRef, config: MdDialogConfig): MdDialogContainer {
6968
let containerPortal = new ComponentPortal(MdDialogContainer, config.viewContainerRef);
70-
return overlay.attach(containerPortal).then((containerRef: ComponentRef<MdDialogContainer>) => {
71-
// Pass the config directly to the container so that it can consume any relevant settings.
72-
containerRef.instance.dialogConfig = config;
73-
return containerRef;
74-
});
69+
70+
let containerRef: ComponentRef<MdDialogContainer> = overlay.attach(containerPortal);
71+
containerRef.instance.dialogConfig = config;
72+
73+
return containerRef.instance;
7574
}
7675

7776
/**
7877
* Attaches the user-provided component to the already-created MdDialogContainer.
7978
* @param component The type of component being loaded into the dialog.
80-
* @param containerRef Reference to the wrapping MdDialogContainer.
79+
* @param dialogContainer Reference to the wrapping MdDialogContainer.
8180
* @param overlayRef Reference to the overlay in which the dialog resides.
8281
* @returns A promise resolving to the MdDialogRef that should be returned to the user.
8382
*/
8483
private _attachDialogContent<T>(
8584
component: ComponentType<T>,
86-
containerRef: ComponentRef<MdDialogContainer>,
87-
overlayRef: OverlayRef): Promise<MdDialogRef<T>> {
88-
let dialogContainer = containerRef.instance;
89-
85+
dialogContainer: MdDialogContainer,
86+
overlayRef: OverlayRef): MdDialogRef<T> {
9087
// Create a reference to the dialog we're creating in order to give the user a handle
9188
// to modify and close it.
92-
let dialogRef = new MdDialogRef(overlayRef);
89+
let dialogRef = <MdDialogRef<T>> new MdDialogRef(overlayRef);
9390

9491
// We create an injector specifically for the component we're instantiating so that it can
9592
// inject the MdDialogRef. This allows a component loaded inside of a dialog to close itself
9693
// and, optionally, to return a value.
9794
let dialogInjector = new DialogInjector(dialogRef, this._injector);
9895

9996
let contentPortal = new ComponentPortal(component, null, dialogInjector);
100-
return dialogContainer.attachComponentPortal(contentPortal).then(contentRef => {
101-
dialogRef.componentInstance = contentRef.instance;
102-
return dialogRef;
103-
});
97+
98+
let contentRef = dialogContainer.attachComponentPortal(contentPortal);
99+
dialogRef.componentInstance = contentRef.instance;
100+
101+
return dialogRef;
104102
}
105103

106104
/**

src/components/menu/menu-trigger.ts

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -58,21 +58,21 @@ export class MdMenuTrigger implements AfterViewInit, OnDestroy {
5858
get menuOpen(): boolean { return this._menuOpen; }
5959

6060
@HostListener('click')
61-
toggleMenu(): Promise<void> {
61+
toggleMenu(): void {
6262
return this._menuOpen ? this.closeMenu() : this.openMenu();
6363
}
6464

65-
openMenu(): Promise<void> {
66-
return this._createOverlay()
67-
.then(() => this._overlayRef.attach(this._portal))
68-
.then(() => this._setIsMenuOpen(true));
65+
openMenu(): void {
66+
this._createOverlay();
67+
this._overlayRef.attach(this._portal);
68+
this._setIsMenuOpen(true);
6969
}
7070

71-
closeMenu(): Promise<void> {
72-
if (!this._overlayRef) { return Promise.resolve(); }
73-
74-
return this._overlayRef.detach()
75-
.then(() => this._setIsMenuOpen(false));
71+
closeMenu(): void {
72+
if (this._overlayRef) {
73+
this._overlayRef.detach();
74+
this._setIsMenuOpen(false);
75+
}
7676
}
7777

7878
destroyMenu(): void {
@@ -103,12 +103,11 @@ export class MdMenuTrigger implements AfterViewInit, OnDestroy {
103103
* This method creates the overlay from the provided menu's template and saves its
104104
* OverlayRef so that it can be attached to the DOM when openMenu is called.
105105
*/
106-
private _createOverlay(): Promise<any> {
107-
if (this._overlayRef) { return Promise.resolve(); }
108-
109-
this._portal = new TemplatePortal(this.menu.templateRef, this._viewContainerRef);
110-
return this._overlay.create(this._getOverlayConfig())
111-
.then(overlay => this._overlayRef = overlay);
106+
private _createOverlay(): void {
107+
if (!this._overlayRef) {
108+
this._portal = new TemplatePortal(this.menu.templateRef, this._viewContainerRef);
109+
this._overlayRef = this._overlay.create(this._getOverlayConfig());
110+
}
112111
}
113112

114113
/**

0 commit comments

Comments
 (0)