vsavkin 6f4a39c337 refactor(di): removed @Parent
BREAKING CHANGE
    The @Parent annotation has been removed. Use @Ancestor instead.
    @Parent was used to enforce a particular DOM structure (e.g., a pane component is a direct child of the tabs component).
    DI is not the right mechanism to do it. We should enforce it using schema instead.
2015-07-21 14:39:28 -07:00

270 lines
8.8 KiB
TypeScript

import {
Component,
Directive,
View,
Ancestor,
ElementRef,
DynamicComponentLoader,
ComponentRef,
DomRenderer
} from 'angular2/angular2';
import {bind, Injectable, forwardRef, ResolvedBinding, Injector} from 'angular2/di';
import {ObservableWrapper, Promise, PromiseWrapper} from 'angular2/src/facade/async';
import {isPresent, Type} from 'angular2/src/facade/lang';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {MouseEvent, KeyboardEvent} from 'angular2/src/facade/browser';
import {KEY_ESC} from 'angular2_material/src/core/constants';
// TODO(jelbourn): Opener of dialog can control where it is rendered.
// TODO(jelbourn): body scrolling is disabled while dialog is open.
// TODO(jelbourn): Don't manually construct and configure a DOM element. See #1402
// TODO(jelbourn): Wrap focus from end of dialog back to the start. Blocked on #1251
// TODO(jelbourn): Focus the dialog element when it is opened.
// TODO(jelbourn): Real dialog styles.
// TODO(jelbourn): Pre-built `alert` and `confirm` dialogs.
// TODO(jelbourn): Animate dialog out of / into opening element.
/**
* Service for opening modal dialogs.
*/
@Injectable()
export class MdDialog {
componentLoader: DynamicComponentLoader;
domRenderer: DomRenderer;
constructor(loader: DynamicComponentLoader, domRenderer: DomRenderer) {
this.componentLoader = loader;
this.domRenderer = domRenderer;
}
/**
* Opens a modal dialog.
* @param type The component to open.
* @param elementRef The logical location into which the component will be opened.
* @param parentInjector
* @param options
* @returns Promise for a reference to the dialog.
*/
open(type: Type, elementRef: ElementRef, options: MdDialogConfig = null): Promise<MdDialogRef> {
var config = isPresent(options) ? options : new MdDialogConfig();
// Create the dialogRef here so that it can be injected into the content component.
var dialogRef = new MdDialogRef();
var bindings = Injector.resolve([bind(MdDialogRef).toValue(dialogRef)]);
var backdropRefPromise = this._openBackdrop(elementRef, bindings);
// First, load the MdDialogContainer, into which the given component will be loaded.
return this.componentLoader.loadNextToLocation(MdDialogContainer, elementRef)
.then(containerRef => {
// TODO(tbosch): clean this up when we have custom renderers
// (https://github.com/angular/angular/issues/1807)
// TODO(jelbourn): Don't use direct DOM access. Need abstraction to create an element
// directly on the document body (also needed for web workers stuff).
// Create a DOM node to serve as a physical host element for the dialog.
var dialogElement = containerRef.location.nativeElement;
DOM.appendChild(DOM.query('body'), dialogElement);
// TODO(jelbourn): Use hostProperties binding to set these once #1539 is fixed.
// Configure properties on the host element.
DOM.addClass(dialogElement, 'md-dialog');
DOM.setAttribute(dialogElement, 'tabindex', '0');
// TODO(jelbourn): Do this with hostProperties (or another rendering abstraction) once
// ready.
if (isPresent(config.width)) {
DOM.setStyle(dialogElement, 'width', config.width);
}
if (isPresent(config.height)) {
DOM.setStyle(dialogElement, 'height', config.height);
}
dialogRef.containerRef = containerRef;
// Now load the given component into the MdDialogContainer.
return this.componentLoader.loadNextToLocation(type, containerRef.instance.contentRef,
bindings)
.then(contentRef => {
// Wrap both component refs for the container and the content so that we can return
// the `instance` of the content but the dispose method of the container back to the
// opener.
dialogRef.contentRef = contentRef;
containerRef.instance.dialogRef = dialogRef;
backdropRefPromise.then(backdropRef => {
dialogRef.whenClosed.then((_) => { backdropRef.dispose(); });
});
return dialogRef;
});
});
}
/** Loads the dialog backdrop (transparent overlay over the rest of the page). */
_openBackdrop(elementRef: ElementRef, bindings: ResolvedBinding[]): Promise<ComponentRef> {
return this.componentLoader.loadNextToLocation(MdBackdrop, elementRef, bindings)
.then((componentRef) => {
// TODO(tbosch): clean this up when we have custom renderers
// (https://github.com/angular/angular/issues/1807)
var backdropElement = componentRef.location.nativeElement;
DOM.addClass(backdropElement, 'md-backdrop');
DOM.appendChild(DOM.query('body'), backdropElement);
return componentRef;
});
}
alert(message: string, okMessage: string): Promise<any> {
throw "Not implemented";
}
confirm(message: string, okMessage: string, cancelMessage: string): Promise<any> {
throw "Not implemented";
}
}
/**
* Reference to an opened dialog.
*/
export class MdDialogRef {
// Reference to the MdDialogContainer component.
containerRef: ComponentRef;
// Reference to the Component loaded as the dialog content.
_contentRef: ComponentRef;
// Whether the dialog is closed.
isClosed: boolean;
// Deferred resolved when the dialog is closed. The promise for this deferred is publicly exposed.
whenClosedDeferred: any;
// Deferred resolved when the content ComponentRef is set. Only used internally.
contentRefDeferred: any;
constructor() {
this._contentRef = null;
this.containerRef = null;
this.isClosed = false;
this.contentRefDeferred = PromiseWrapper.completer();
this.whenClosedDeferred = PromiseWrapper.completer();
}
set contentRef(value: ComponentRef) {
this._contentRef = value;
this.contentRefDeferred.resolve(value);
}
/** Gets the component instance for the content of the dialog. */
get instance() {
if (isPresent(this._contentRef)) {
return this._contentRef.instance;
}
// The only time one could attempt to access this property before the value is set is if an
// access occurs during
// the constructor of the very instance they are trying to get (which is much more easily
// accessed as `this`).
throw "Cannot access dialog component instance *from* that component's constructor.";
}
/** Gets a promise that is resolved when the dialog is closed. */
get whenClosed(): Promise<any> {
return this.whenClosedDeferred.promise;
}
/** Closes the dialog. This operation is asynchronous. */
close(result: any = null) {
this.contentRefDeferred.promise.then((_) => {
if (!this.isClosed) {
this.isClosed = true;
this.containerRef.dispose();
this.whenClosedDeferred.resolve(result);
}
});
}
}
/** Confiuration for a dialog to be opened. */
export class MdDialogConfig {
width: string;
height: string;
constructor() {
// Default configuration.
this.width = null;
this.height = null;
}
}
/**
* Container for user-provided dialog content.
*/
@Component({
selector: 'md-dialog-container',
host: {'(body:^keydown)': 'documentKeypress($event)'},
})
@View({
templateUrl: 'angular2_material/src/components/dialog/dialog.html',
directives: [forwardRef(() => MdDialogContent)]
})
class MdDialogContainer {
// Ref to the dialog content. Used by the DynamicComponentLoader to load the dialog content.
contentRef: ElementRef;
// Ref to the open dialog. Used to close the dialog based on certain events.
dialogRef: MdDialogRef;
constructor() {
this.contentRef = null;
this.dialogRef = null;
}
wrapFocus() {
// Return the focus to the host element. Blocked on #1251.
}
documentKeypress(event: KeyboardEvent) {
if (event.keyCode == KEY_ESC) {
this.dialogRef.close();
}
}
}
/**
* Simple decorator used only to communicate an ElementRef to the parent MdDialogContainer as the
* location
* for where the dialog content will be loaded.
*/
@Directive({selector: 'md-dialog-content'})
class MdDialogContent {
constructor(@Ancestor() dialogContainer: MdDialogContainer, elementRef: ElementRef) {
dialogContainer.contentRef = elementRef;
}
}
/** Component for the dialog "backdrop", a transparent overlay over the rest of the page. */
@Component({
selector: 'md-backdrop',
host: {'(click)': 'onClick()'},
})
@View({template: ''})
class MdBackdrop {
dialogRef: MdDialogRef;
constructor(dialogRef: MdDialogRef) {
this.dialogRef = dialogRef;
}
onClick() {
// TODO(jelbourn): Use MdDialogConfig to capture option for whether dialog should close on
// clicking outside.
this.dialogRef.close();
}
}