+
+
+
+
+ Template snack bar: {{message}}
+
diff --git a/src/demo-app/snack-bar/snack-bar-demo.ts b/src/demo-app/snack-bar/snack-bar-demo.ts
index 920bc57c00a..ea95babde2d 100644
--- a/src/demo-app/snack-bar/snack-bar-demo.ts
+++ b/src/demo-app/snack-bar/snack-bar-demo.ts
@@ -7,7 +7,7 @@
*/
import {Dir} from '@angular/cdk/bidi';
-import {Component, ViewEncapsulation} from '@angular/core';
+import {Component, ViewEncapsulation, ViewChild, TemplateRef} from '@angular/core';
import {
MatSnackBar,
MatSnackBarConfig,
@@ -25,6 +25,7 @@ import {
preserveWhitespaces: false,
})
export class SnackBarDemo {
+ @ViewChild('template') template: TemplateRef;
message: string = 'Snack Bar opened.';
actionButtonLabel: string = 'Retry';
action: boolean = false;
@@ -38,12 +39,22 @@ export class SnackBarDemo {
}
open() {
- let config = new MatSnackBarConfig();
+ const config = this._createConfig();
+ this.snackBar.open(this.message, this.action ? this.actionButtonLabel : undefined, config);
+ }
+
+ openTemplate() {
+ const config = this._createConfig();
+ this.snackBar.openFromTemplate(this.template, config);
+ }
+
+ private _createConfig() {
+ const config = new MatSnackBarConfig();
config.verticalPosition = this.verticalPosition;
config.horizontalPosition = this.horizontalPosition;
config.duration = this.setAutoHide ? this.autoHide : 0;
config.panelClass = this.addExtraClass ? ['party'] : undefined;
config.direction = this.dir.value;
- this.snackBar.open(this.message, this.action ? this.actionButtonLabel : undefined, config);
+ return config;
}
}
diff --git a/src/lib/snack-bar/snack-bar-container.ts b/src/lib/snack-bar/snack-bar-container.ts
index 85bf299689c..34dd3668994 100644
--- a/src/lib/snack-bar/snack-bar-container.ts
+++ b/src/lib/snack-bar/snack-bar-container.ts
@@ -23,6 +23,7 @@ import {
BasePortalOutlet,
ComponentPortal,
CdkPortalOutlet,
+ TemplatePortal,
} from '@angular/cdk/portal';
import {take} from 'rxjs/operators/take';
import {Observable} from 'rxjs/Observable';
@@ -78,31 +79,16 @@ export class MatSnackBarContainer extends BasePortalOutlet implements OnDestroy
/** Attach a component portal as content to this snack bar container. */
attachComponentPortal(portal: ComponentPortal): ComponentRef {
- if (this._portalOutlet.hasAttached()) {
- throw Error('Attempting to attach snack bar content after content is already attached');
- }
-
- const element: HTMLElement = this._elementRef.nativeElement;
-
- if (this.snackBarConfig.panelClass || this.snackBarConfig.extraClasses) {
- this._setCssClasses(this.snackBarConfig.panelClass);
- this._setCssClasses(this.snackBarConfig.extraClasses);
- }
-
- if (this.snackBarConfig.horizontalPosition === 'center') {
- element.classList.add('mat-snack-bar-center');
- }
-
- if (this.snackBarConfig.verticalPosition === 'top') {
- element.classList.add('mat-snack-bar-top');
- }
-
+ this._assertNotAttached();
+ this._applySnackBarClasses();
return this._portalOutlet.attachComponentPortal(portal);
}
/** Attach a template portal as content to this snack bar container. */
- attachTemplatePortal(): EmbeddedViewRef {
- throw Error('Not yet implemented');
+ attachTemplatePortal(portal: TemplatePortal): EmbeddedViewRef {
+ this._assertNotAttached();
+ this._applySnackBarClasses();
+ return this._portalOutlet.attachTemplatePortal(portal);
}
/** Handle end of animations, updating the state of the snackbar. */
@@ -171,4 +157,29 @@ export class MatSnackBarContainer extends BasePortalOutlet implements OnDestroy
element.classList.add(classList);
}
}
+
+ /** Applies the various positioning and user-configured CSS classes to the snack bar. */
+ private _applySnackBarClasses() {
+ const element: HTMLElement = this._elementRef.nativeElement;
+
+ if (this.snackBarConfig.panelClass || this.snackBarConfig.extraClasses) {
+ this._setCssClasses(this.snackBarConfig.panelClass);
+ this._setCssClasses(this.snackBarConfig.extraClasses);
+ }
+
+ if (this.snackBarConfig.horizontalPosition === 'center') {
+ element.classList.add('mat-snack-bar-center');
+ }
+
+ if (this.snackBarConfig.verticalPosition === 'top') {
+ element.classList.add('mat-snack-bar-top');
+ }
+ }
+
+ /** Asserts that no content is already attached to the container. */
+ private _assertNotAttached() {
+ if (this._portalOutlet.hasAttached()) {
+ throw Error('Attempting to attach snack bar content after content is already attached');
+ }
+ }
}
diff --git a/src/lib/snack-bar/snack-bar.spec.ts b/src/lib/snack-bar/snack-bar.spec.ts
index 9bd53fff8d1..0680013e74d 100644
--- a/src/lib/snack-bar/snack-bar.spec.ts
+++ b/src/lib/snack-bar/snack-bar.spec.ts
@@ -6,7 +6,15 @@ import {
tick,
flush,
} from '@angular/core/testing';
-import {NgModule, Component, Directive, ViewChild, ViewContainerRef, Inject} from '@angular/core';
+import {
+ NgModule,
+ Component,
+ Directive,
+ ViewChild,
+ ViewContainerRef,
+ Inject,
+ TemplateRef,
+} from '@angular/core';
import {CommonModule} from '@angular/common';
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
import {OverlayContainer} from '@angular/cdk/overlay';
@@ -464,6 +472,42 @@ describe('MatSnackBar', () => {
});
+ describe('with TemplateRef', () => {
+ let templateFixture: ComponentFixture;
+
+ beforeEach(() => {
+ templateFixture = TestBed.createComponent(ComponentWithTemplateRef);
+ templateFixture.detectChanges();
+ });
+
+ it('should be able to open a snack bar using a TemplateRef', () => {
+ templateFixture.componentInstance.localValue = 'Pizza';
+ snackBar.openFromTemplate(templateFixture.componentInstance.templateRef);
+ templateFixture.detectChanges();
+
+ const containerElement = overlayContainerElement.querySelector('snack-bar-container')!;
+
+ expect(containerElement.textContent).toContain('Fries');
+ expect(containerElement.textContent).toContain('Pizza');
+
+ templateFixture.componentInstance.localValue = 'Pasta';
+ templateFixture.detectChanges();
+
+ expect(containerElement.textContent).toContain('Pasta');
+ });
+
+ it('should be able to pass in contextual data when opening with a TemplateRef', () => {
+ snackBar.openFromTemplate(templateFixture.componentInstance.templateRef, {
+ data: {value: 'Oranges'}
+ });
+
+ const containerElement = overlayContainerElement.querySelector('snack-bar-container')!;
+
+ expect(containerElement.textContent).toContain('Oranges');
+ });
+
+ });
+
});
describe('MatSnackBar with parent MatSnackBar', () => {
@@ -810,6 +854,20 @@ class ComponentWithChildViewContainer {
}
}
+@Component({
+ selector: 'arbitrary-component-with-template-ref',
+ template: `
+
+ Fries {{localValue}} {{data?.value}}
+
+ `,
+})
+class ComponentWithTemplateRef {
+ @ViewChild(TemplateRef) templateRef: TemplateRef;
+ localValue: string;
+}
+
+
/** Simple component for testing ComponentPortal. */
@Component({template: '
Burritos are on the way.
'})
class BurritosNotification {
@@ -835,7 +893,8 @@ class ComponentThatProvidesMatSnackBar {
*/
const TEST_DIRECTIVES = [ComponentWithChildViewContainer,
BurritosNotification,
- DirectiveWithViewContainer];
+ DirectiveWithViewContainer,
+ ComponentWithTemplateRef];
@NgModule({
imports: [CommonModule, MatSnackBarModule],
exports: TEST_DIRECTIVES,
diff --git a/src/lib/snack-bar/snack-bar.ts b/src/lib/snack-bar/snack-bar.ts
index c5c2d38ce9a..5cba3f0f74b 100644
--- a/src/lib/snack-bar/snack-bar.ts
+++ b/src/lib/snack-bar/snack-bar.ts
@@ -9,15 +9,17 @@
import {LiveAnnouncer} from '@angular/cdk/a11y';
import {BreakpointObserver, Breakpoints} from '@angular/cdk/layout';
import {Overlay, OverlayConfig, OverlayRef} from '@angular/cdk/overlay';
-import {ComponentPortal, ComponentType, PortalInjector} from '@angular/cdk/portal';
+import {ComponentPortal, TemplatePortal, ComponentType, PortalInjector} from '@angular/cdk/portal';
import {
ComponentRef,
+ EmbeddedViewRef,
Inject,
Injectable,
Injector,
InjectionToken,
Optional,
SkipSelf,
+ TemplateRef,
} from '@angular/core';
import {take} from 'rxjs/operators/take';
import {takeUntil} from 'rxjs/operators/takeUntil';
@@ -72,41 +74,21 @@ export class MatSnackBar {
* @param component Component to be instantiated.
* @param config Extra configuration for the snack bar.
*/
- openFromComponent(component: ComponentType, config?: MatSnackBarConfig): MatSnackBarRef {
- const _config = {...this._defaultConfig, ...config};
- const snackBarRef = this._attach(component, _config);
+ openFromComponent(component: ComponentType, config?: MatSnackBarConfig):
+ MatSnackBarRef {
+ return this._attach(component, config) as MatSnackBarRef;
+ }
- // When the snackbar is dismissed, clear the reference to it.
- snackBarRef.afterDismissed().subscribe(() => {
- // Clear the snackbar ref if it hasn't already been replaced by a newer snackbar.
- if (this._openedSnackBarRef == snackBarRef) {
- this._openedSnackBarRef = null;
- }
- });
-
- if (this._openedSnackBarRef) {
- // If a snack bar is already in view, dismiss it and enter the
- // new snack bar after exit animation is complete.
- this._openedSnackBarRef.afterDismissed().subscribe(() => {
- snackBarRef.containerInstance.enter();
- });
- this._openedSnackBarRef.dismiss();
- } else {
- // If no snack bar is in view, enter the new snack bar.
- snackBarRef.containerInstance.enter();
- }
-
- // If a dismiss timeout is provided, set up dismiss based on after the snackbar is opened.
- if (_config.duration && _config.duration > 0) {
- snackBarRef.afterOpened().subscribe(() => snackBarRef._dismissAfter(_config!.duration!));
- }
-
- if (_config.announcementMessage) {
- this._live.announce(_config.announcementMessage, _config.politeness);
- }
-
- this._openedSnackBarRef = snackBarRef;
- return this._openedSnackBarRef;
+ /**
+ * Creates and dispatches a snack bar with a custom template for the content, removing any
+ * currently opened snack bars.
+ *
+ * @param template Template to be instantiated.
+ * @param config Extra configuration for the snack bar.
+ */
+ openFromTemplate(template: TemplateRef, config?: MatSnackBarConfig):
+ MatSnackBarRef> {
+ return this._attach(template, config);
}
/**
@@ -148,18 +130,31 @@ export class MatSnackBar {
}
/**
- * Places a new component as the content of the snack bar container.
+ * Places a new component or a template as the content of the snack bar container.
*/
- private _attach(component: ComponentType, config: MatSnackBarConfig): MatSnackBarRef {
+ private _attach(content: ComponentType | TemplateRef, userConfig?: MatSnackBarConfig):
+ MatSnackBarRef> {
+
+ const config = {...this._defaultConfig, ...userConfig};
const overlayRef = this._createOverlay(config);
const container = this._attachSnackBarContainer(overlayRef, config);
- const snackBarRef = new MatSnackBarRef(container, overlayRef);
- const injector = this._createInjector(config, snackBarRef);
- const portal = new ComponentPortal(component, undefined, injector);
- const contentRef = container.attachComponentPortal(portal);
+ const snackBarRef = new MatSnackBarRef>(container, overlayRef);
- // We can't pass this via the injector, because the injector is created earlier.
- snackBarRef.instance = contentRef.instance;
+ if (content instanceof TemplateRef) {
+ const portal = new TemplatePortal(content, null!, {
+ $implicit: config.data,
+ snackBarRef
+ } as any);
+
+ snackBarRef.instance = container.attachTemplatePortal(portal);
+ } else {
+ const injector = this._createInjector(config, snackBarRef);
+ const portal = new ComponentPortal(content, undefined, injector);
+ const contentRef = container.attachComponentPortal(portal);
+
+ // We can't pass this via the injector, because the injector is created earlier.
+ snackBarRef.instance = contentRef.instance;
+ }
// Subscribe to the breakpoint observer and attach the mat-snack-bar-handset class as
// appropriate. This class is applied to the overlay element because the overlay must expand to
@@ -174,7 +169,41 @@ export class MatSnackBar {
}
});
- return snackBarRef;
+ this._animateSnackBar(snackBarRef, config);
+ this._openedSnackBarRef = snackBarRef;
+ return this._openedSnackBarRef;
+ }
+
+ /** Animates the old snack bar out and the new one in. */
+ private _animateSnackBar(snackBarRef: MatSnackBarRef, config: MatSnackBarConfig) {
+ // When the snackbar is dismissed, clear the reference to it.
+ snackBarRef.afterDismissed().subscribe(() => {
+ // Clear the snackbar ref if it hasn't already been replaced by a newer snackbar.
+ if (this._openedSnackBarRef == snackBarRef) {
+ this._openedSnackBarRef = null;
+ }
+ });
+
+ if (this._openedSnackBarRef) {
+ // If a snack bar is already in view, dismiss it and enter the
+ // new snack bar after exit animation is complete.
+ this._openedSnackBarRef.afterDismissed().subscribe(() => {
+ snackBarRef.containerInstance.enter();
+ });
+ this._openedSnackBarRef.dismiss();
+ } else {
+ // If no snack bar is in view, enter the new snack bar.
+ snackBarRef.containerInstance.enter();
+ }
+
+ // If a dismiss timeout is provided, set up dismiss based on after the snackbar is opened.
+ if (config.duration && config.duration > 0) {
+ snackBarRef.afterOpened().subscribe(() => snackBarRef._dismissAfter(config.duration!));
+ }
+
+ if (config.announcementMessage) {
+ this._live.announce(config.announcementMessage, config.politeness);
+ }
}
/**