diff --git a/src/demo-app/demo-app-module.ts b/src/demo-app/demo-app-module.ts
index d0a14dfd475f..4a292c8f301b 100644
--- a/src/demo-app/demo-app-module.ts
+++ b/src/demo-app/demo-app-module.ts
@@ -29,6 +29,7 @@ import {MdCheckboxDemoNestedChecklist, CheckboxDemo} from './checkbox/checkbox-d
import {SelectDemo} from './select/select-demo';
import {SliderDemo} from './slider/slider-demo';
import {SidenavDemo} from './sidenav/sidenav-demo';
+import {SnackBarDemo} from './snack-bar/snack-bar-demo';
import {PortalDemo, ScienceJoke} from './portal/portal-demo';
import {MenuDemo} from './menu/menu-demo';
import {TabsDemo} from './tabs/tab-group-demo';
@@ -61,6 +62,7 @@ import {TabsDemo} from './tabs/tab-group-demo';
LiveAnnouncerDemo,
MdCheckboxDemoNestedChecklist,
MenuDemo,
+ SnackBarDemo,
OverlayDemo,
PortalDemo,
ProgressBarDemo,
diff --git a/src/demo-app/demo-app/demo-app.html b/src/demo-app/demo-app/demo-app.html
index ff618689e032..15084fbcdc9f 100644
--- a/src/demo-app/demo-app/demo-app.html
+++ b/src/demo-app/demo-app/demo-app.html
@@ -23,6 +23,7 @@
Sidenav
Slider
Slide Toggle
+ Snack Bar
Tabs
Toolbar
Tooltip
diff --git a/src/demo-app/demo-app/routes.ts b/src/demo-app/demo-app/routes.ts
index 03dbfd0ec3ac..b7c3fa47f3a7 100644
--- a/src/demo-app/demo-app/routes.ts
+++ b/src/demo-app/demo-app/routes.ts
@@ -26,6 +26,7 @@ import {MenuDemo} from '../menu/menu-demo';
import {RippleDemo} from '../ripple/ripple-demo';
import {DialogDemo} from '../dialog/dialog-demo';
import {TooltipDemo} from '../tooltip/tooltip-demo';
+import {SnackBarDemo} from '../snack-bar/snack-bar-demo';
export const DEMO_APP_ROUTES: Routes = [
@@ -56,4 +57,5 @@ export const DEMO_APP_ROUTES: Routes = [
{path: 'ripple', component: RippleDemo},
{path: 'dialog', component: DialogDemo},
{path: 'tooltip', component: TooltipDemo},
+ {path: 'snack-bar', component: SnackBarDemo},
];
diff --git a/src/demo-app/snack-bar/snack-bar-demo.html b/src/demo-app/snack-bar/snack-bar-demo.html
new file mode 100644
index 000000000000..22bd06ead17f
--- /dev/null
+++ b/src/demo-app/snack-bar/snack-bar-demo.html
@@ -0,0 +1,13 @@
+
SnackBar demo
+
+
Message:
+
+ Show button
+
+
+
+
+
\ No newline at end of file
diff --git a/src/demo-app/snack-bar/snack-bar-demo.scss b/src/demo-app/snack-bar/snack-bar-demo.scss
new file mode 100644
index 000000000000..87b6bb25d3b8
--- /dev/null
+++ b/src/demo-app/snack-bar/snack-bar-demo.scss
@@ -0,0 +1,3 @@
+.button-label-input {
+ display: inline-block;
+}
\ No newline at end of file
diff --git a/src/demo-app/snack-bar/snack-bar-demo.ts b/src/demo-app/snack-bar/snack-bar-demo.ts
new file mode 100644
index 000000000000..790d045ec6c8
--- /dev/null
+++ b/src/demo-app/snack-bar/snack-bar-demo.ts
@@ -0,0 +1,31 @@
+import {Component, ViewContainerRef} from '@angular/core';
+import {MdSnackBar, MdSnackBarConfig} from '@angular2-material/snack-bar';
+
+@Component({
+ moduleId: module.id,
+ selector: 'snack-bar-demo',
+ templateUrl: 'snack-bar-demo.html',
+})
+export class SnackBarDemo {
+ message: string = 'Snack Bar opened.';
+ actionButtonLabel: string = 'Retry';
+ action: boolean = false;
+
+ constructor(
+ public snackBar: MdSnackBar,
+ public viewContainerRef: ViewContainerRef) { }
+
+ open() {
+ let config = new MdSnackBarConfig(this.viewContainerRef);
+ this.snackBar.open(this.message, this.action && this.actionButtonLabel, config);
+ }
+}
+
+
+@Component({
+ moduleId: module.id,
+ selector: 'demo-snack',
+ templateUrl: 'snack-bar-demo.html',
+ styleUrls: ['./snack-bar-demo.css'],
+})
+export class DemoSnack {}
diff --git a/src/demo-app/system-config.ts b/src/demo-app/system-config.ts
index 1ad0109145b7..baa8a7f056cd 100644
--- a/src/demo-app/system-config.ts
+++ b/src/demo-app/system-config.ts
@@ -20,6 +20,7 @@ const components = [
'sidenav',
'slider',
'slide-toggle',
+ 'snack-bar',
'button-toggle',
'tabs',
'toolbar',
diff --git a/src/lib/all/all.ts b/src/lib/all/all.ts
index 784156e2d7c5..f551ab454a6d 100644
--- a/src/lib/all/all.ts
+++ b/src/lib/all/all.ts
@@ -15,6 +15,7 @@ import {MdProgressCircleModule} from '@angular2-material/progress-circle';
import {MdProgressBarModule} from '@angular2-material/progress-bar';
import {MdInputModule} from '@angular2-material/input';
import {MdTabsModule} from '@angular2-material/tabs';
+import {MdSnackBarModule} from '@angular2-material/snack-bar';
import {MdToolbarModule} from '@angular2-material/toolbar';
import {MdTooltipModule} from '@angular2-material/tooltip';
import {
@@ -47,6 +48,7 @@ const MATERIAL_MODULES = [
MdSidenavModule,
MdSliderModule,
MdSlideToggleModule,
+ MdSnackBarModule,
MdTabsModule,
MdToolbarModule,
MdTooltipModule,
@@ -81,6 +83,7 @@ const MATERIAL_MODULES = [
MdRadioModule.forRoot(),
MdSliderModule.forRoot(),
MdSlideToggleModule.forRoot(),
+ MdSnackBarModule.forRoot(),
MdTooltipModule.forRoot(),
OverlayModule.forRoot(),
],
diff --git a/src/lib/snack-bar/base-snack-bar.ts b/src/lib/snack-bar/base-snack-bar.ts
new file mode 100644
index 000000000000..ffbcb65bae7e
--- /dev/null
+++ b/src/lib/snack-bar/base-snack-bar.ts
@@ -0,0 +1,12 @@
+import {MdSnackBarRef} from './snack-bar-ref';
+
+
+export class BaseSnackBarContent {
+ /** The instance of the component making up the content of the snack bar. */
+ snackBarRef: MdSnackBarRef;
+
+ /** Dismisses the snack bar. */
+ dismiss(): void {
+ this.snackBarRef.dismiss();
+ }
+}
diff --git a/src/lib/snack-bar/index.ts b/src/lib/snack-bar/index.ts
new file mode 100644
index 000000000000..259852618083
--- /dev/null
+++ b/src/lib/snack-bar/index.ts
@@ -0,0 +1 @@
+export * from './snack-bar';
diff --git a/src/lib/snack-bar/package.json b/src/lib/snack-bar/package.json
new file mode 100644
index 000000000000..be0d7e0de4fa
--- /dev/null
+++ b/src/lib/snack-bar/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "@angular2-material/snack-bar",
+ "version": "2.0.0-alpha.8-1",
+ "description": "Angular 2 Material snack bar",
+ "main": "./snack-bar.umd.js",
+ "module": "./index.js",
+ "typings": "./index.d.ts",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/angular/material2.git"
+ },
+ "keywords": [
+ "angular",
+ "material",
+ "material design",
+ "components",
+ "snackbar",
+ "toast",
+ "notification"
+ ],
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/angular/material2/issues"
+ },
+ "homepage": "https://github.com/angular/material2#readme",
+ "peerDependencies": {
+ "@angular2-material/core": "2.0.0-alpha.8-1"
+ }
+}
diff --git a/src/lib/snack-bar/simple-snack-bar.html b/src/lib/snack-bar/simple-snack-bar.html
new file mode 100644
index 000000000000..772bde96d7c5
--- /dev/null
+++ b/src/lib/snack-bar/simple-snack-bar.html
@@ -0,0 +1,2 @@
+{{message}}
+
\ No newline at end of file
diff --git a/src/lib/snack-bar/simple-snack-bar.scss b/src/lib/snack-bar/simple-snack-bar.scss
new file mode 100644
index 000000000000..52b99f292b58
--- /dev/null
+++ b/src/lib/snack-bar/simple-snack-bar.scss
@@ -0,0 +1,28 @@
+:host {
+ display: flex;
+ justify-content: space-between;
+
+ span {
+ box-sizing: border-box;
+ border: none;
+ color: white;
+ font-family: Roboto, 'Helvetica Neue', sans-serif;
+ font-size: 14px;
+ line-height: 20px;
+ outline: none;
+ text-decoration: none;
+ word-break: break-all;
+ }
+
+ button {
+ box-sizing: border-box;
+ color: white;
+ float: right;
+ font-weight: 600;
+ line-height: 20px;
+ margin: -5px 0 0 48px;
+ min-width: initial;
+ padding: 5px;
+ text-transform: uppercase;
+ }
+}
\ No newline at end of file
diff --git a/src/lib/snack-bar/simple-snack-bar.ts b/src/lib/snack-bar/simple-snack-bar.ts
new file mode 100644
index 000000000000..542cbe08f586
--- /dev/null
+++ b/src/lib/snack-bar/simple-snack-bar.ts
@@ -0,0 +1,20 @@
+import {Component} from '@angular/core';
+import {BaseSnackBarContent} from './base-snack-bar';
+
+
+@Component({
+ moduleId: module.id,
+ selector: 'simple-snack-bar',
+ templateUrl: 'simple-snack-bar.html',
+ styleUrls: ['simple-snack-bar.css'],
+})
+export class SimpleSnackBar extends BaseSnackBarContent {
+ /** The message to be shown in the snack bar. */
+ message: string;
+
+ /** The label for the button in the snack bar. */
+ action: string;
+
+ /** If the action button should be shown. */
+ get hasAction(): boolean { return !!this.action; }
+}
diff --git a/src/lib/snack-bar/snack-bar-config.ts b/src/lib/snack-bar/snack-bar-config.ts
new file mode 100644
index 000000000000..9225964d33a6
--- /dev/null
+++ b/src/lib/snack-bar/snack-bar-config.ts
@@ -0,0 +1,16 @@
+import {ViewContainerRef} from '@angular/core';
+
+
+export type SnackBarRole = 'alert' | 'polite';
+
+export class MdSnackBarConfig {
+ /** The aria-role of the snack bar. */
+ role: SnackBarRole = 'alert';
+
+ /** The view container to place the overlay for the snack bar into. */
+ viewContainerRef: ViewContainerRef;
+
+ constructor(viewContainerRef: ViewContainerRef) {
+ this.viewContainerRef = viewContainerRef;
+ }
+}
diff --git a/src/lib/snack-bar/snack-bar-container.html b/src/lib/snack-bar/snack-bar-container.html
new file mode 100644
index 000000000000..23e1d44627f8
--- /dev/null
+++ b/src/lib/snack-bar/snack-bar-container.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/lib/snack-bar/snack-bar-container.scss b/src/lib/snack-bar/snack-bar-container.scss
new file mode 100644
index 000000000000..2a47c390795b
--- /dev/null
+++ b/src/lib/snack-bar/snack-bar-container.scss
@@ -0,0 +1,19 @@
+@import '../core/style/elevation';
+
+$md-snack-bar-padding: 14px 24px !default;
+$md-snack-bar-height: 20px !default;
+$md-snack-bar-min-width: 288px !default;
+$md-snack-bar-max-width: 568px !default;
+
+
+:host {
+ background: #323232;
+ border-radius: 2px;
+ display: block;
+ height: $md-snack-bar-height;
+ @include md-elevation(24);
+ max-width: $md-snack-bar-max-width;
+ min-width: $md-snack-bar-min-width;
+ overflow: hidden;
+ padding: $md-snack-bar-padding;
+}
\ No newline at end of file
diff --git a/src/lib/snack-bar/snack-bar-container.ts b/src/lib/snack-bar/snack-bar-container.ts
new file mode 100644
index 000000000000..2de010fc3a5d
--- /dev/null
+++ b/src/lib/snack-bar/snack-bar-container.ts
@@ -0,0 +1,46 @@
+import {
+ Component,
+ ComponentRef,
+ ViewChild
+} from '@angular/core';
+import {
+ BasePortalHost,
+ ComponentPortal,
+ TemplatePortal,
+ PortalHostDirective} from '@angular2-material/core';
+import {MdSnackBarConfig} from './snack-bar-config';
+import {MdSnackBarContentAlreadyAttached} from './snack-bar-errors';
+
+
+/**
+ * Internal component that wraps user-provided snack bar content.
+ */
+@Component({
+ moduleId: module.id,
+ selector: 'snack-bar-content',
+ templateUrl: 'snack-bar-container.html',
+ styleUrls: ['snack-bar-container.css'],
+ host: {
+ '[attr.role]': 'snackBarConfig?.role'
+ }
+})
+export class MdSnackBarContainer extends BasePortalHost {
+ /** The portal host inside of this container into which the snack bar content will be loaded. */
+ @ViewChild(PortalHostDirective) private _portalHost: PortalHostDirective;
+
+ /** The snack bar configuration. */
+ snackBarConfig: MdSnackBarConfig;
+
+ /** Attach a portal as content to this snack bar container. */
+ attachComponentPortal(portal: ComponentPortal): ComponentRef {
+ if (this._portalHost.hasAttached()) {
+ throw new MdSnackBarContentAlreadyAttached();
+ }
+
+ return this._portalHost.attachComponentPortal(portal);
+ }
+
+ attachTemplatePortal(portal: TemplatePortal): Map {
+ throw Error('Not yet implemented');
+ }
+}
diff --git a/src/lib/snack-bar/snack-bar-errors.ts b/src/lib/snack-bar/snack-bar-errors.ts
new file mode 100644
index 000000000000..cb8bbe2b5723
--- /dev/null
+++ b/src/lib/snack-bar/snack-bar-errors.ts
@@ -0,0 +1,8 @@
+import {MdError} from '@angular2-material/core';
+
+
+export class MdSnackBarContentAlreadyAttached extends MdError {
+ constructor() {
+ super('Attempting to attach snack bar content after content is already attached');
+ }
+}
diff --git a/src/lib/snack-bar/snack-bar-ref.ts b/src/lib/snack-bar/snack-bar-ref.ts
new file mode 100644
index 000000000000..4f1581fdd972
--- /dev/null
+++ b/src/lib/snack-bar/snack-bar-ref.ts
@@ -0,0 +1,41 @@
+import {OverlayRef} from '@angular2-material/core';
+import {Observable} from 'rxjs/Observable';
+import {Subject} from 'rxjs/Subject';
+
+// TODO(josephperrott): Implement onAction observable.
+
+
+/**
+ * Reference to a snack bar dispatched from the snack bar service.
+ */
+export class MdSnackBarRef {
+ /** The instance of the component making up the content of the snack bar. */
+ readonly instance: T|any;
+
+ /** Subject for notifying the user that the snack bar has closed. */
+ private _afterClosed: Subject = new Subject();
+
+ /** If the snack bar is active. */
+ private _isActive: boolean = true;
+
+ constructor(instance: T|any, private _overlayRef: OverlayRef) {
+ // Sets the readonly instance of the snack bar content component.
+ this.instance = instance;
+ this.afterDismissed().subscribe(null, null, () => { this._isActive = false; });
+ }
+
+
+ /** Dismisses the snack bar. */
+ dismiss(): void {
+ if (this._isActive) {
+ this._overlayRef.dispose();
+ this._afterClosed.complete();
+ }
+ }
+
+
+ /** Gets an observable that is notified when the snack bar is finished closing. */
+ afterDismissed(): Observable {
+ return this._afterClosed.asObservable();
+ }
+}
diff --git a/src/lib/snack-bar/snack-bar.ts b/src/lib/snack-bar/snack-bar.ts
new file mode 100644
index 000000000000..544c48394e37
--- /dev/null
+++ b/src/lib/snack-bar/snack-bar.ts
@@ -0,0 +1,124 @@
+import {
+ NgModule,
+ ModuleWithProviders,
+ Injectable,
+ ComponentRef,
+} from '@angular/core';
+import {
+ Overlay,
+ OverlayState,
+ OverlayRef,
+ ComponentType,
+ ComponentPortal,
+ OverlayModule,
+ PortalModule,
+ OVERLAY_PROVIDERS,
+} from '@angular2-material/core';
+import {CommonModule} from '@angular/common';
+import {MdSnackBarConfig} from './snack-bar-config';
+import {MdSnackBarRef} from './snack-bar-ref';
+import {MdSnackBarContainer} from './snack-bar-container';
+import {SimpleSnackBar} from './simple-snack-bar';
+
+export {MdSnackBarRef} from './snack-bar-ref';
+export {MdSnackBarConfig} from './snack-bar-config';
+
+// TODO(josephperrott): Animate entrance and exit of snack bars.
+// TODO(josephperrott): Automate dismiss after timeout.
+
+
+/**
+ * Service to dispatch Material Design snack bar messages.
+ */
+@Injectable()
+export class MdSnackBar {
+ /** A reference to the current snack bar in the view. */
+ private _snackBarRef: MdSnackBarRef;
+
+ constructor(private _overlay: Overlay) {}
+
+ /**
+ * Creates and dispatches a snack bar with a custom component for the content, removing any
+ * currently opened snack bars.
+ */
+ openFromComponent(component: ComponentType,
+ config: MdSnackBarConfig): MdSnackBarRef {
+ if (this._snackBarRef) {
+ this._snackBarRef.dismiss();
+ }
+ let overlayRef = this._createOverlay();
+ let snackBarContainer = this._attachSnackBarContainer(overlayRef, config);
+
+ return this._fillSnackBarContainer(component, snackBarContainer, overlayRef);
+ }
+
+
+ /**
+ * Creates and dispatches a snack bar.
+ */
+ open(message: string, buttonLabel: string,
+ config: MdSnackBarConfig): MdSnackBarRef {
+ let simpleSnackBar = this.openFromComponent(SimpleSnackBar, config);
+ simpleSnackBar.instance.message = message;
+ simpleSnackBar.instance.action = buttonLabel;
+ return simpleSnackBar;
+ }
+
+
+ /**
+ * Attaches the snack bar container component to the overlay.
+ */
+ private _attachSnackBarContainer(overlayRef: OverlayRef,
+ config: MdSnackBarConfig): MdSnackBarContainer {
+ let containerPortal = new ComponentPortal(MdSnackBarContainer, config.viewContainerRef);
+ let containerRef: ComponentRef = overlayRef.attach(containerPortal);
+ containerRef.instance.snackBarConfig = config;
+
+ return containerRef.instance;
+ }
+
+
+ /**
+ * Places a new component as the content of the snack bar container.
+ */
+ private _fillSnackBarContainer(component: ComponentType,
+ container: MdSnackBarContainer,
+ overlayRef: OverlayRef): MdSnackBarRef {
+ let portal = new ComponentPortal(component);
+ let contentRef = container.attachComponentPortal(portal);
+ let snackBarRef = > new MdSnackBarRef(contentRef.instance, overlayRef);
+ snackBarRef.instance.snackBarRef = snackBarRef;
+
+ this._snackBarRef = snackBarRef;
+ return snackBarRef;
+ }
+
+
+ /**
+ * Creates a new overlay and places it in the correct location.
+ */
+ private _createOverlay(): OverlayRef {
+ let state = new OverlayState();
+ state.positionStrategy = this._overlay.position().global()
+ .fixed()
+ .centerHorizontally()
+ .bottom('0px');
+ return this._overlay.create(state);
+ }
+}
+
+
+@NgModule({
+ imports: [OverlayModule, PortalModule, CommonModule],
+ exports: [MdSnackBarContainer],
+ declarations: [MdSnackBarContainer, SimpleSnackBar],
+ entryComponents: [MdSnackBarContainer, SimpleSnackBar],
+})
+export class MdSnackBarModule {
+ static forRoot(): ModuleWithProviders {
+ return {
+ ngModule: MdSnackBarModule,
+ providers: [MdSnackBar, OVERLAY_PROVIDERS]
+ };
+ }
+}
diff --git a/src/lib/system-config-spec.ts b/src/lib/system-config-spec.ts
index 0490942f8fd9..42d17ab29500 100644
--- a/src/lib/system-config-spec.ts
+++ b/src/lib/system-config-spec.ts
@@ -20,6 +20,7 @@ const components = [
'sidenav',
'slider',
'slide-toggle',
+ 'snack-bar',
'button-toggle',
'tabs',
'toolbar',