diff --git a/src/demo-app/demo-app-module.ts b/src/demo-app/demo-app-module.ts index 47b54dbb1036..8454f65ddd5f 100644 --- a/src/demo-app/demo-app-module.ts +++ b/src/demo-app/demo-app-module.ts @@ -8,7 +8,14 @@ import {MaterialModule, OverlayContainer, FullscreenOverlayContainer} from '@angular/material'; import {DEMO_APP_ROUTES} from './demo-app/routes'; import {ProgressBarDemo} from './progress-bar/progress-bar-demo'; -import {JazzDialog, ContentElementDialog, DialogDemo, IFrameDialog} from './dialog/dialog-demo'; +import { + JazzDialog, + JazzDialogTemplateRef, + ContentElementDialog, + ContentElementTemplateRefDialog, + DialogDemo, + IFrameDialog +} from './dialog/dialog-demo'; import {RippleDemo} from './ripple/ripple-demo'; import {IconDemo} from './icon/icon-demo'; import {GesturesDemo} from './gestures/gestures-demo'; @@ -64,7 +71,9 @@ import {InputContainerDemo} from './input/input-container-demo'; IconDemo, InputContainerDemo, JazzDialog, + JazzDialogTemplateRef, ContentElementDialog, + ContentElementTemplateRefDialog, IFrameDialog, ListDemo, LiveAnnouncerDemo, @@ -100,7 +109,9 @@ import {InputContainerDemo} from './input/input-container-demo'; entryComponents: [ DemoApp, JazzDialog, + JazzDialogTemplateRef, ContentElementDialog, + ContentElementTemplateRefDialog, IFrameDialog, RotiniPanel, ScienceJoke, diff --git a/src/demo-app/dialog/dialog-demo.html b/src/demo-app/dialog/dialog-demo.html index 53734bbbfdff..ec6cf69cb0e5 100644 --- a/src/demo-app/dialog/dialog-demo.html +++ b/src/demo-app/dialog/dialog-demo.html @@ -1,7 +1,15 @@

Dialog demo

- - +
+ + +
+ +
+ + +
+ @@ -51,3 +59,12 @@

Other options

Last close result: {{lastCloseResult}}

+ + + + + diff --git a/src/demo-app/dialog/dialog-demo.scss b/src/demo-app/dialog/dialog-demo.scss index 6f7f4cac254a..8d1f33274c67 100644 --- a/src/demo-app/dialog/dialog-demo.scss +++ b/src/demo-app/dialog/dialog-demo.scss @@ -6,3 +6,7 @@ max-width: 350px; margin: 20px 0; } + +.demo-button-group { + margin-bottom: 20px; +} diff --git a/src/demo-app/dialog/dialog-demo.ts b/src/demo-app/dialog/dialog-demo.ts index 5e91c097f9aa..623e41b5b28c 100644 --- a/src/demo-app/dialog/dialog-demo.ts +++ b/src/demo-app/dialog/dialog-demo.ts @@ -1,4 +1,12 @@ -import {Component, Inject} from '@angular/core'; +import { + Component, + Inject, + Input, + Output, + ViewChild, + TemplateRef, + EventEmitter +} from '@angular/core'; import {DOCUMENT} from '@angular/platform-browser'; import {MdDialog, MdDialogRef, MdDialogConfig} from '@angular/material'; @@ -9,7 +17,15 @@ import {MdDialog, MdDialogRef, MdDialogConfig} from '@angular/material'; styleUrls: ['dialog-demo.css'], }) export class DialogDemo { + @ViewChild('jazzDialogRef') + jazzDialogRef: TemplateRef; + + @ViewChild('contentElementRef') + contentElementRef: TemplateRef; + dialogRef: MdDialogRef; + dialogTemplateRef: MdDialogRef; + dialogContentTemplateRef: MdDialogRef; lastCloseResult: string; actionsAlignment: string; config: MdDialogConfig = { @@ -41,16 +57,43 @@ export class DialogDemo { openJazz() { this.dialogRef = this.dialog.open(JazzDialog, this.config); - this.dialogRef.afterClosed().subscribe(result => { + this.dialogRef.afterClosed().first().subscribe(result => { this.lastCloseResult = result; this.dialogRef = null; }); } + openJazzUsingTemplateRef() { + this.dialogTemplateRef = this.dialog.openFromTemplate(this.jazzDialogRef, this.config); + + this.dialogTemplateRef.afterClosed().first().subscribe(() => { + this.dialogTemplateRef = null; + }); + } + + closeJazzUsingTemplateRef(result: string) { + this.lastCloseResult = result; + + this.dialogTemplateRef.close(); + } + openContentElement() { let dialogRef = this.dialog.open(ContentElementDialog, this.config); dialogRef.componentInstance.actionsAlignment = this.actionsAlignment; } + + openContentElementUsingTemplateRef() { + this.dialogContentTemplateRef = this.dialog.openFromTemplate( + this.contentElementRef, + this.config + ); + } + + closeContentElementUsingTemplateRef() { + if (this.dialogContentTemplateRef) { + this.dialogContentTemplateRef.close(); + } + } } @@ -69,6 +112,24 @@ export class JazzDialog { } +@Component({ + selector: 'demo-jazz-dialog-template-ref', + template: ` +

It's Jazz!

+

+

{{ jazzMessage }}

+ ` +}) +export class JazzDialogTemplateRef { + jazzMessage = 'Jazzy jazz jazz'; + + @Output() + close = new EventEmitter(false); + + constructor() { } +} + + @Component({ selector: 'demo-content-element-dialog', styles: [ @@ -98,13 +159,12 @@ export class JazzDialog { md-raised-button color="primary" md-dialog-close>Close - Read more on Wikipedia - + + Read more on Wikipedia + + + + ` +}) +export class ContentElementTemplateRefDialog { + @Input() + actionsAlignment: string; + + @Output() + close = new EventEmitter(); + + constructor(public dialog: MdDialog) { } + + showInStackedDialog() { + this.dialog.open(IFrameDialog); + } +} + @Component({ selector: 'demo-iframe-dialog', styles: [ diff --git a/src/lib/dialog/dialog-container.ts b/src/lib/dialog/dialog-container.ts index 6004809e8e05..76d3d72de3a9 100644 --- a/src/lib/dialog/dialog-container.ts +++ b/src/lib/dialog/dialog-container.ts @@ -5,6 +5,7 @@ import { ViewEncapsulation, NgZone, OnDestroy, + ViewContainerRef, } from '@angular/core'; import {BasePortalHost, ComponentPortal, PortalHostDirective, TemplatePortal} from '../core'; import {MdDialogConfig} from './dialog-config'; @@ -46,8 +47,13 @@ export class MdDialogContainer extends BasePortalHost implements OnDestroy { /** Reference to the open dialog. */ dialogRef: MdDialogRef; - constructor(private _ngZone: NgZone) { + /** Exposes ViewContainerRef */ + containerRef: ViewContainerRef; + + constructor(private _viewContainerRef: ViewContainerRef, private _ngZone: NgZone) { super(); + + this.containerRef = this._viewContainerRef; } /** @@ -61,20 +67,21 @@ export class MdDialogContainer extends BasePortalHost implements OnDestroy { let attachResult = this._portalHost.attachComponentPortal(portal); - // If were to attempt to focus immediately, then the content of the dialog would not yet be - // ready in instances where change detection has to run first. To deal with this, we simply - // wait for the microtask queue to be empty. - this._ngZone.onMicrotaskEmpty.first().subscribe(() => { - this._elementFocusedBeforeDialogWasOpened = document.activeElement; - this._focusTrap.focusFirstTabbableElement(); - }); + this._focusFirstTabbableElement(); return attachResult; } - /** @docs-private */ attachTemplatePortal(portal: TemplatePortal): Map { - throw Error('Not yet implemented'); + if (this._portalHost.hasAttached()) { + throw new MdDialogContentAlreadyAttachedError(); + } + + let attachResult = this._portalHost.attachTemplatePortal(portal); + + this._focusFirstTabbableElement(); + + return attachResult; } /** @@ -85,6 +92,8 @@ export class MdDialogContainer extends BasePortalHost implements OnDestroy { if (!this.dialogConfig.disableClose) { this.dialogRef.close(); } + + this.dialogRef.escapePress.next(); } ngOnDestroy() { @@ -95,4 +104,14 @@ export class MdDialogContainer extends BasePortalHost implements OnDestroy { (this._elementFocusedBeforeDialogWasOpened as HTMLElement).focus(); }); } + + private _focusFirstTabbableElement() { + // If were to attempt to focus immediately, then the content of the dialog would not yet be + // ready in instances where change detection has to run first. To deal with this, we simply + // wait for the microtask queue to be empty. + this._ngZone.onMicrotaskEmpty.first().subscribe(() => { + this._elementFocusedBeforeDialogWasOpened = document.activeElement; + this._focusTrap.focusFirstTabbableElement(); + }); + } } diff --git a/src/lib/dialog/dialog-ref.ts b/src/lib/dialog/dialog-ref.ts index bd9a45df84ac..01409530bf0f 100644 --- a/src/lib/dialog/dialog-ref.ts +++ b/src/lib/dialog/dialog-ref.ts @@ -14,10 +14,18 @@ export class MdDialogRef { /** The instance of component opened into the dialog. */ componentInstance: T; + /** Expose overlay backdrop click event */ + backdropClick: Observable; + + /** Subject for notifying the user that esc key way pressed */ + escapePress = new Subject(); + /** Subject for notifying the user that the dialog has finished closing. */ private _afterClosed: Subject = new Subject(); - constructor(private _overlayRef: OverlayRef) { } + constructor(private _overlayRef: OverlayRef) { + this.backdropClick = this._overlayRef.backdropClick(); + } /** * Close the dialog. diff --git a/src/lib/dialog/dialog.spec.ts b/src/lib/dialog/dialog.spec.ts index e4196924deaa..3d09ba007dda 100644 --- a/src/lib/dialog/dialog.spec.ts +++ b/src/lib/dialog/dialog.spec.ts @@ -8,7 +8,15 @@ import { tick, } from '@angular/core/testing'; import {By} from '@angular/platform-browser'; -import {NgModule, Component, Directive, ViewChild, ViewContainerRef, Injector} from '@angular/core'; +import { + NgModule, + Component, + Directive, + ViewChild, + ViewContainerRef, + Injector, + TemplateRef +} from '@angular/core'; import {MdDialogModule} from './index'; import {MdDialog} from './dialog'; import {OverlayContainer} from '../core'; @@ -64,6 +72,23 @@ describe('MdDialog', () => { expect(dialogContainerElement.getAttribute('role')).toBe('dialog'); }); + it('should open a dialog with a templateRef', () => { + const catDialogContainer = TestBed.createComponent(CatDialogContainer); + + catDialogContainer.detectChanges(); + + dialog.openFromTemplate(catDialogContainer.componentInstance.catRef); + + viewContainerFixture.detectChanges(); + + expect(overlayContainerElement.textContent).toContain('Cat'); + + viewContainerFixture.detectChanges(); + let dialogContainerElement = overlayContainerElement.querySelector('md-dialog-container'); + expect(dialogContainerElement.getAttribute('role')).toBe('dialog'); + }); + + it('should use injector from viewContainerRef for DialogInjector', () => { let dialogRef = dialog.open(PizzaMsg, { viewContainerRef: testViewContainerRef @@ -120,7 +145,6 @@ describe('MdDialog', () => { expect(overlayContainerElement.querySelector('md-dialog-container')).toBeNull(); }); - it('should close a dialog via the escape key', () => { dialog.open(PizzaMsg, { viewContainerRef: testViewContainerRef @@ -137,6 +161,26 @@ describe('MdDialog', () => { expect(overlayContainerElement.querySelector('md-dialog-container')).toBeNull(); }); + it('should notify the user that escape key was pressed', () => { + let dialogRef = dialog.open(PizzaMsg, { + viewContainerRef: testViewContainerRef + }); + + viewContainerFixture.detectChanges(); + + let dialogContainer: MdDialogContainer = + viewContainerFixture.debugElement.query(By.directive(MdDialogContainer)).componentInstance; + + let pressedSpy = jasmine.createSpy('clicked callback'); + + dialogRef.escapePress.subscribe(pressedSpy); + + // Fake the user pressing the escape key by calling the handler directly. + dialogContainer.handleEscapeKey(); + + expect(pressedSpy).toHaveBeenCalled(); + }); + it('should close when clicking on the overlay backdrop', () => { dialog.open(PizzaMsg, { viewContainerRef: testViewContainerRef @@ -150,6 +194,25 @@ describe('MdDialog', () => { expect(overlayContainerElement.querySelector('md-dialog-container')).toBeFalsy(); }); + it('should notify the user that overlay backdrop was clicked', () => { + let dialogRef = dialog.open(PizzaMsg, { + viewContainerRef: testViewContainerRef + }); + + viewContainerFixture.detectChanges(); + + let backdrop = overlayContainerElement.querySelector('.cdk-overlay-backdrop') as HTMLElement; + + let clickedSpy = jasmine.createSpy('clicked callback'); + + dialogRef.backdropClick.subscribe(clickedSpy); + + backdrop.click(); + + expect(clickedSpy).toHaveBeenCalled(); + + }); + it('should notify the observers if a dialog has been opened', () => { let ref: MdDialogRef; dialog.afterOpen.subscribe(r => { @@ -483,6 +546,15 @@ class PizzaMsg { public dialogInjector: Injector) {} } +/** Components for testing dialog using TemplateRef. */ +@Component({selector: 'cat-dialog', template: '

Cat

'}) +class CatDialog {} + +@Component({template: ''}) +class CatDialogContainer { + @ViewChild('catRef') catRef: TemplateRef; +} + @Component({ template: `

This is the title

@@ -510,6 +582,8 @@ class ComponentThatProvidesMdDialog { const TEST_DIRECTIVES = [ ComponentWithChildViewContainer, PizzaMsg, + CatDialog, + CatDialogContainer, DirectiveWithViewContainer, ContentElementDialog ]; @@ -518,6 +592,12 @@ const TEST_DIRECTIVES = [ imports: [MdDialogModule], exports: TEST_DIRECTIVES, declarations: TEST_DIRECTIVES, - entryComponents: [ComponentWithChildViewContainer, PizzaMsg, ContentElementDialog], + entryComponents: [ + ComponentWithChildViewContainer, + PizzaMsg, + CatDialog, + CatDialogContainer, + ContentElementDialog + ], }) class DialogTestModule { } diff --git a/src/lib/dialog/dialog.ts b/src/lib/dialog/dialog.ts index 93e48cd6b2fc..63494599d2c6 100644 --- a/src/lib/dialog/dialog.ts +++ b/src/lib/dialog/dialog.ts @@ -1,8 +1,15 @@ -import {Injector, ComponentRef, Injectable, Optional, SkipSelf} from '@angular/core'; +import {Injector, ComponentRef, Injectable, Optional, SkipSelf, TemplateRef} from '@angular/core'; import {Observable} from 'rxjs/Observable'; import {Subject} from 'rxjs/Subject'; -import {Overlay, OverlayRef, ComponentType, OverlayState, ComponentPortal} from '../core'; +import { + Overlay, + OverlayRef, + ComponentType, + OverlayState, + ComponentPortal, + TemplatePortal +} from '../core'; import {extendObject} from '../core/util/object-extend'; import {DialogInjector} from './dialog-injector'; @@ -10,12 +17,8 @@ import {MdDialogConfig} from './dialog-config'; import {MdDialogRef} from './dialog-ref'; import {MdDialogContainer} from './dialog-container'; - -// TODO(jelbourn): add support for opening with a TemplateRef // TODO(jelbourn): animations - - /** * Service to open Material Design modal dialogs. */ @@ -51,6 +54,34 @@ export class MdDialog { private _injector: Injector, @Optional() @SkipSelf() private _parentDialog: MdDialog) { } + /** + * Opens a modal dialog containing the given TemplateRef. + * @param TemplateRef to load into the dialog + * @param config Extra configuration options. + * @returns Reference to the newly-opened dialog. + */ + openFromTemplate(templateRef: TemplateRef, config?: MdDialogConfig): MdDialogRef { + config = _applyConfigDefaults(config); + + let overlayRef = this._createOverlay(config); + + let dialogContainer = this._attachDialogContainer(overlayRef, config); + let dialogRef = this._attachDialogContent( + dialogContainer, + overlayRef, + null, + templateRef, + config + ); + + this._openDialogs.push(dialogRef); + dialogRef.afterClosed().subscribe(() => this._removeOpenDialog(dialogRef)); + this._afterOpen.next(dialogRef); + + return dialogRef; + + } + /** * Opens a modal dialog containing the given component. * @param component Type of the component to load into the load. @@ -62,7 +93,13 @@ export class MdDialog { let overlayRef = this._createOverlay(config); let dialogContainer = this._attachDialogContainer(overlayRef, config); - let dialogRef = this._attachDialogContent(component, dialogContainer, overlayRef, config); + let dialogRef = this._attachDialogContent( + dialogContainer, + overlayRef, + component, + null, + config + ); this._openDialogs.push(dialogRef); dialogRef.afterClosed().subscribe(() => this._removeOpenDialog(dialogRef)); @@ -121,9 +158,10 @@ export class MdDialog { * @returns A promise resolving to the MdDialogRef that should be returned to the user. */ private _attachDialogContent( - component: ComponentType, dialogContainer: MdDialogContainer, overlayRef: OverlayRef, + component: ComponentType, + templateRef: TemplateRef, config?: MdDialogConfig): MdDialogRef { // Create a reference to the dialog we're creating in order to give the user a handle // to modify and close it. @@ -137,18 +175,42 @@ export class MdDialog { // Set the dialogRef to the container so that it can use the ref to close the dialog. dialogContainer.dialogRef = dialogRef; - // We create an injector specifically for the component we're instantiating so that it can + if (component) { + this._attachComponentToContainer( + component, + dialogContainer, + dialogRef, + config + ); + } + + if (templateRef) { + this._attachTemplateToContainer(templateRef, dialogContainer); + } + + return dialogRef; + } + + private _attachComponentToContainer( + component: ComponentType, + container: MdDialogContainer, + dialogRef: MdDialogRef, + config: MdDialogConfig) { + // We create an injector specifically for the component we're instantiating so that it can // inject the MdDialogRef. This allows a component loaded inside of a dialog to close itself // and, optionally, to return a value. let userInjector = config && config.viewContainerRef && config.viewContainerRef.injector; let dialogInjector = new DialogInjector(dialogRef, userInjector || this._injector); - let contentPortal = new ComponentPortal(component, null, dialogInjector); + const portal = new ComponentPortal(component, null, dialogInjector); - let contentRef = dialogContainer.attachComponentPortal(contentPortal); + const contentRef = container.attachComponentPortal(portal); dialogRef.componentInstance = contentRef.instance; + } - return dialogRef; + private _attachTemplateToContainer(templateRef: TemplateRef, container: MdDialogContainer) { + const portal = new TemplatePortal(templateRef, container.containerRef); + container.attachTemplatePortal(portal); } /**