feat(material): add prototype dialog component w/ demo.

This commit is contained in:
Jeremy Elbourn 2015-04-28 10:56:33 -07:00
parent 75da6e4c4a
commit f88c4b77ca
10 changed files with 456 additions and 4 deletions

View File

@ -54,7 +54,7 @@ export class EventManagerPlugin {
// We are assuming here that all plugins support bubbled and non-bubbled events.
// That is equivalent to having supporting $event.target
// The bubbling flag (currently ^) is stripped before calling the supports and
// The bubbling flag (currently ^) is stripped before calling the supports and
// addEventListener methods.
supports(eventName: string): boolean {
return false;

View File

@ -39,7 +39,7 @@ export class MdCheckbox {
/** Setter for tabindex */
tabindex: number;
constructor(@Attribute('tabindex') tabindex: string) {
constructor(@Attribute('tabindex') tabindex: String) {
this.role = 'checkbox';
this.checked = false;
this.tabindex = isPresent(tabindex) ? NumberWrapper.parseInt(tabindex, 10) : 0;

View File

@ -0,0 +1,33 @@
<style>
.md-dialog {
position: absolute;
z-index: 80;
/** Center the dialog. */
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 300px;
height: 300px;
background-color: white;
border: 1px solid black;
box-shadow: 0 4px 4px;;
padding: 20px;
}
.md-backdrop {
position: absolute;
top:0 ;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.12);
}
</style>
<md-dialog-content></md-dialog-content>
<div tabindex="0" (focus)="wrapFocus()"></div>

View File

@ -0,0 +1,265 @@
import {DynamicComponentLoader, ElementRef, ComponentRef, onDestroy} from 'angular2/angular2';
import {bind, 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(radokirov): Once the application is transpiled by TS instead of Traceur,
// add those imports back into 'angular2/angular2';
import {Component, Directive} from 'angular2/src/core/annotations_impl/annotations';
import {Parent} from 'angular2/src/core/annotations_impl/visibility';
import {View} from 'angular2/src/core/annotations_impl/view';
// 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.
*/
export class MdDialog {
componentLoader: DynamicComponentLoader;
constructor(loader: DynamicComponentLoader) {
this.componentLoader = loader;
}
/**
* Opens a modal dialog.
* @param type The component to open.
* @param elementRef The logical location into which the component will be opened.
* @returns Promise for a reference to the dialog.
*/
open(
type: Type,
elementRef: ElementRef,
parentInjector: Injector,
options: MdDialogConfig = null): Promise<MdDialogRef> {
var config = isPresent(options) ? options : new MdDialogConfig();
// 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 = DOM.createElement('div');
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);
}
// Create the dialogRef here so that it can be injected into the content component.
var dialogRef = new MdDialogRef();
var dialogRefBinding = bind(MdDialogRef).toValue(dialogRef);
var contentInjector = parentInjector.resolveAndCreateChild([dialogRefBinding]);
var backdropRefPromise = this._openBackdrop(elementRef, contentInjector);
// First, load the MdDialogContainer, into which the given component will be loaded.
return this.componentLoader.loadIntoNewLocation(
MdDialogContainer, elementRef, dialogElement).then(containerRef => {
dialogRef.containerRef = containerRef;
// Now load the given component into the MdDialogContainer.
return this.componentLoader.loadNextToExistingLocation(
type, containerRef.instance.contentRef, contentInjector).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, injector: Injector): Promise<ComponentRef> {
var backdropElement = DOM.createElement('div');
DOM.addClass(backdropElement, 'md-backdrop');
DOM.appendChild(DOM.query('body'), backdropElement);
return this.componentLoader.loadIntoNewLocation(
MdBackdrop, elementRef, backdropElement, injector);
}
alert(message: string, okMessage: string): Promise {
throw "Not implemented";
}
confirm(message: string, okMessage: string, cancelMessage: string): Promise {
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 {
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',
hostListeners: {
'body:^keydown': 'documentKeypress($event)'
}
})
@View({
templateUrl: 'angular2_material/src/components/dialog/dialog.html',
directives: [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();
}
}
}
/** Component for the dialog "backdrop", a transparent overlay over the rest of the page. */
@Component({
selector: 'md-backdrop',
hostListeners: {
'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();
}
}
/**
* 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(@Parent() dialogContainer: MdDialogContainer, elementRef: ElementRef) {
dialogContainer.contentRef = elementRef;
}
}

View File

@ -42,7 +42,7 @@ export class MdProgressLinear {
ariaValuemin: string;
ariaValuemax: string;
constructor(@Attribute('md-mode') mode: string) {
constructor(@Attribute('md-mode') mode: String) {
this.primaryBarTransform = '';
this.secondaryBarTransform = '';

View File

@ -37,7 +37,7 @@ export class MdSwitch {
tabindex: number;
role: string;
constructor(@Attribute('tabindex') tabindex: string) {
constructor(@Attribute('tabindex') tabindex: String) {
this.role = 'checkbox';
this.checked = false;
this.tabindex = isPresent(tabindex) ? NumberWrapper.parseInt(tabindex, 10) : 0;

View File

@ -1,6 +1,7 @@
// TODO: switch to proper enums when we support them.
// Key codes
export const KEY_ESC = 27;
export const KEY_SPACE = 32;
export const KEY_UP = 38;
export const KEY_DOWN = 40;

View File

@ -0,0 +1,32 @@
<div>
<h2>Dialog demo</h2>
<button type="button" (click)="open()" [disabled]="!!dialogRef">
Open a dialog
</button>
<button type="button" (click)="close()" [disabled]="!dialogRef">
Close the dialog
</button>
<p>
Last result: {{lastResult}}
</p>
<hr>
<p> Here are some paragaphs to make the page scrollable</p>
<p> Here are some paragaphs to make the page scrollable</p>
<p> Here are some paragaphs to make the page scrollable</p>
<p> Here are some paragaphs to make the page scrollable</p>
<p> Here are some paragaphs to make the page scrollable</p>
<p> Here are some paragaphs to make the page scrollable</p>
<p> Here are some paragaphs to make the page scrollable</p>
<p> Here are some paragaphs to make the page scrollable</p>
<p> Here are some paragaphs to make the page scrollable</p>
<p> Here are some paragaphs to make the page scrollable</p>
<p> Here are some paragaphs to make the page scrollable</p>
<p> Here are some paragaphs to make the page scrollable</p>
<p> Here are some paragaphs to make the page scrollable</p>
</div>

View File

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title>ng-material dialog demo</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=RobotoDraft:400,500,700,400italic">
<style>
* {
font-family: RobotoDraft, Roboto;
}
</style>
</head>
<body>
$SCRIPTS$
<demo-app>Loading...</demo-app>
</body>
</html>

View File

@ -0,0 +1,103 @@
import {bootstrap, ElementRef, ComponentRef} from 'angular2/angular2';
import {MdDialog, MdDialogRef, MdDialogConfig} from 'angular2_material/src/components/dialog/dialog'
import {UrlResolver} from 'angular2/src/services/url_resolver';
import {commonDemoSetup, DemoUrlResolver} from '../demo_common';
import {bind, Injector} from 'angular2/di';
import {isPresent} from 'angular2/src/facade/lang';
// TODO(radokirov): Once the application is transpiled by TS instead of Traceur,
// add those imports back into 'angular2/angular2';
import {Component, Directive} from 'angular2/src/core/annotations_impl/annotations';
import {View} from 'angular2/src/core/annotations_impl/view';
@Component({
selector: 'demo-app',
injectables: [MdDialog]
})
@View({
templateUrl: './demo_app.html',
directives: []
})
class DemoApp {
dialog: MdDialog;
elementRef: ElementRef;
dialogRef: MdDialogRef;
dialogConfig: MdDialogConfig;
injector: Injector;
lastResult: string;
constructor(mdDialog: MdDialog, elementRef: ElementRef, injector: Injector) {
this.dialog = mdDialog;
this.elementRef = elementRef;
this.dialogConfig = new MdDialogConfig();
this.injector = injector;
this.dialogConfig.width = '60%';
this.dialogConfig.height = '60%';
this.lastResult = '';
}
open() {
if (isPresent(this.dialogRef)) {
return;
}
this.dialog.open(SimpleDialogComponent,
this.elementRef, this.injector, this.dialogConfig).then(ref => {
this.dialogRef = ref;
ref.instance.numCoconuts = 777;
ref.whenClosed.then(result => {
this.dialogRef = null;
this.lastResult = result;
});
});
}
close() {
this.dialogRef.close();
}
}
@Component({
selector: 'simple-dialog',
properties: {'numCoconuts': 'numCoconuts'}
})
@View({
template: `
<h2>This is the dialog content</h2>
<p>There are {{numCoconuts}} coconuts.</p>
<p>Return: <input (input)="updateValue($event)"></p>
<button type="button" (click)="done()">Done</button>
`
})
class SimpleDialogComponent {
numCoconuts: number;
dialogRef: MdDialogRef;
toReturn: string;
constructor(dialogRef: MdDialogRef) {
this.numCoconuts = 0;
this.dialogRef = dialogRef;
this.toReturn = '';
}
updateValue(event) {
this.toReturn = event.target.value;
}
done() {
this.dialogRef.close(this.toReturn);
}
}
export function main() {
commonDemoSetup();
bootstrap(DemoApp, [
bind(UrlResolver).toValue(new DemoUrlResolver())
]);
}