From 1823ef3fbdf89edd71398af8128915168a35621e Mon Sep 17 00:00:00 2001 From: Dominic Carretto Date: Wed, 19 Feb 2020 19:38:28 -0500 Subject: [PATCH] feat(snackbar): Support custom components --- demos/src/app/app-routing.module.ts | 2 +- .../components/snackbar-demo/examples.html | 80 ---------- .../snackbar-demo/routing.module.ts | 29 ---- .../snackbar-demo/snackbar.module.ts | 13 -- .../{snackbar-demo => snackbar}/api.html | 25 ++- .../src/app/components/snackbar/examples.html | 89 +++++++++++ demos/src/app/components/snackbar/module.ts | 20 +++ .../app/components/snackbar/routing.module.ts | 29 ++++ .../{snackbar-demo => snackbar}/sass.html | 0 .../snackbar-demo.ts => snackbar/snackbar.ts} | 9 +- packages/snackbar/module.ts | 7 +- packages/snackbar/public-api.ts | 1 + packages/snackbar/snackbar-base.ts | 143 ++++++++++++++++++ packages/snackbar/snackbar-container.ts | 23 ++- packages/snackbar/snackbar.component.ts | 123 ++------------- packages/snackbar/snackbar.ts | 47 ++++-- test/snackbar/snackbar.test.ts | 100 +++++++++--- 17 files changed, 455 insertions(+), 285 deletions(-) delete mode 100644 demos/src/app/components/snackbar-demo/examples.html delete mode 100644 demos/src/app/components/snackbar-demo/routing.module.ts delete mode 100644 demos/src/app/components/snackbar-demo/snackbar.module.ts rename demos/src/app/components/{snackbar-demo => snackbar}/api.html (83%) create mode 100644 demos/src/app/components/snackbar/examples.html create mode 100644 demos/src/app/components/snackbar/module.ts create mode 100644 demos/src/app/components/snackbar/routing.module.ts rename demos/src/app/components/{snackbar-demo => snackbar}/sass.html (100%) rename demos/src/app/components/{snackbar-demo/snackbar-demo.ts => snackbar/snackbar.ts} (97%) create mode 100644 packages/snackbar/snackbar-base.ts diff --git a/demos/src/app/app-routing.module.ts b/demos/src/app/app-routing.module.ts index 0565d0993..bf29781d4 100644 --- a/demos/src/app/app-routing.module.ts +++ b/demos/src/app/app-routing.module.ts @@ -67,7 +67,7 @@ const routes: Routes = [ {path: 'slider-demo', loadChildren: () => import('./components/slider/module').then(m => m.SliderModule)}, { path: 'snackbar-demo', loadChildren: () => - import('./components/snackbar-demo/snackbar.module').then(m => m.SnackbarModule) + import('./components/snackbar/module').then(m => m.SnackbarModule) }, {path: 'switch-demo', loadChildren: () => import('./components/switch-demo/switch.module').then(m => m.SwitchModule)}, {path: 'tabs-demo', loadChildren: () => import('./components/tabs-demo/tabs.module').then(m => m.TabsModule)}, diff --git a/demos/src/app/components/snackbar-demo/examples.html b/demos/src/app/components/snackbar-demo/examples.html deleted file mode 100644 index 37868e734..000000000 --- a/demos/src/app/components/snackbar-demo/examples.html +++ /dev/null @@ -1,80 +0,0 @@ -
-
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
- -
-
-
- -
-
- -
-
- -
-
- -
- -
-

Custom

-
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
- -
-

Theme

-
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
\ No newline at end of file diff --git a/demos/src/app/components/snackbar-demo/routing.module.ts b/demos/src/app/components/snackbar-demo/routing.module.ts deleted file mode 100644 index 0ec69fbc3..000000000 --- a/demos/src/app/components/snackbar-demo/routing.module.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { NgModule } from '@angular/core'; -import { Routes, RouterModule } from '@angular/router'; - -import { Api, Examples, Sass, SnackbarDemo } from './snackbar-demo'; - -export const ROUTE_DECLARATIONS = [ - Api, - Examples, - Sass, - SnackbarDemo -]; - -const ROUTES: Routes = [ - { - path: '', component: SnackbarDemo, - children: [ - { path: '', redirectTo: 'api' }, - { path: 'api', component: Api }, - { path: 'sass', component: Sass }, - { path: 'examples', component: Examples } - ] - } -]; - -@NgModule({ - imports: [RouterModule.forChild(ROUTES)], - exports: [RouterModule] -}) -export class RoutingModule { } diff --git a/demos/src/app/components/snackbar-demo/snackbar.module.ts b/demos/src/app/components/snackbar-demo/snackbar.module.ts deleted file mode 100644 index be84b88c8..000000000 --- a/demos/src/app/components/snackbar-demo/snackbar.module.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { NgModule } from '@angular/core'; - -import { SharedModule } from '../../shared.module'; -import { RoutingModule, ROUTE_DECLARATIONS } from './routing.module'; - -@NgModule({ - imports: [ - SharedModule, - RoutingModule - ], - declarations: [ROUTE_DECLARATIONS] -}) -export class SnackbarModule { } diff --git a/demos/src/app/components/snackbar-demo/api.html b/demos/src/app/components/snackbar/api.html similarity index 83% rename from demos/src/app/components/snackbar-demo/api.html rename to demos/src/app/components/snackbar/api.html index 60910e348..a95107c73 100644 --- a/demos/src/app/components/snackbar-demo/api.html +++ b/demos/src/app/components/snackbar/api.html @@ -16,7 +16,8 @@

Properties

afterDismiss: Observable
MdcSnackbarDismissReason
- Gets an observable that is notified when the snackbar is finished closing. + Gets an observable that is notified when the snackbar is finished + closing.

+  
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + + +
+
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+

Custom

+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+

Theme

+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+

Open from Component

+
+
+ +
+
+
\ No newline at end of file diff --git a/demos/src/app/components/snackbar/module.ts b/demos/src/app/components/snackbar/module.ts new file mode 100644 index 000000000..12d060416 --- /dev/null +++ b/demos/src/app/components/snackbar/module.ts @@ -0,0 +1,20 @@ +import {NgModule} from '@angular/core'; + +import {SharedModule} from '../../shared.module'; +import {RoutingModule, ROUTE_DECLARATIONS} from './routing.module'; + +import { + BurritosNotification +} from './snackbar'; + +@NgModule({ + imports: [ + SharedModule, + RoutingModule + ], + declarations: [ + ROUTE_DECLARATIONS, + BurritosNotification, + ] +}) +export class SnackbarModule {} diff --git a/demos/src/app/components/snackbar/routing.module.ts b/demos/src/app/components/snackbar/routing.module.ts new file mode 100644 index 000000000..e563f0e12 --- /dev/null +++ b/demos/src/app/components/snackbar/routing.module.ts @@ -0,0 +1,29 @@ +import {NgModule} from '@angular/core'; +import {Routes, RouterModule} from '@angular/router'; + +import {Api, Examples, Sass, Snackbar} from './snackbar'; + +export const ROUTE_DECLARATIONS = [ + Api, + Examples, + Sass, + Snackbar +]; + +const ROUTES: Routes = [ + { + path: '', component: Snackbar, + children: [ + {path: '', redirectTo: 'api'}, + {path: 'api', component: Api}, + {path: 'sass', component: Sass}, + {path: 'examples', component: Examples} + ] + } +]; + +@NgModule({ + imports: [RouterModule.forChild(ROUTES)], + exports: [RouterModule] +}) +export class RoutingModule {} diff --git a/demos/src/app/components/snackbar-demo/sass.html b/demos/src/app/components/snackbar/sass.html similarity index 100% rename from demos/src/app/components/snackbar-demo/sass.html rename to demos/src/app/components/snackbar/sass.html diff --git a/demos/src/app/components/snackbar-demo/snackbar-demo.ts b/demos/src/app/components/snackbar/snackbar.ts similarity index 97% rename from demos/src/app/components/snackbar-demo/snackbar-demo.ts rename to demos/src/app/components/snackbar/snackbar.ts index 4757597ff..dd597e20c 100644 --- a/demos/src/app/components/snackbar-demo/snackbar-demo.ts +++ b/demos/src/app/components/snackbar/snackbar.ts @@ -11,7 +11,7 @@ interface CustomClasses { } @Component({template: ''}) -export class SnackbarDemo implements OnInit { +export class Snackbar implements OnInit { @ViewChild(ComponentViewer, {static: true}) _componentViewer: ComponentViewer; ngOnInit(): void { @@ -38,6 +38,9 @@ export class Api {} @Component({templateUrl: './sass.html'}) export class Sass {} +@Component({template: '

Burritos are on the way.

'}) +export class BurritosNotification {} + @Component({templateUrl: './examples.html'}) export class Examples { constructor(private snackbar: MdcSnackbar) {} @@ -116,6 +119,10 @@ export class Examples { }); } + openFromComponent() { + this.snackbar.openFromComponent(BurritosNotification); + } + // // Examples // diff --git a/packages/snackbar/module.ts b/packages/snackbar/module.ts index 9c81494df..cac25f4d9 100644 --- a/packages/snackbar/module.ts +++ b/packages/snackbar/module.ts @@ -4,8 +4,9 @@ import {OverlayModule} from '@angular/cdk/overlay'; import {PortalModule} from '@angular/cdk/portal'; import {MdcButtonModule} from '@angular-mdc/web/button'; -import {MdcSnackbarComponent} from './snackbar.component'; +import {MdcSnackbarBase} from './snackbar-base'; import {MdcSnackbarContainer} from './snackbar-container'; +import {MdcSnackbarComponent} from './snackbar.component'; @NgModule({ imports: [ @@ -15,7 +16,7 @@ import {MdcSnackbarContainer} from './snackbar-container'; MdcButtonModule ], exports: [MdcSnackbarContainer], - declarations: [MdcSnackbarContainer, MdcSnackbarComponent], - entryComponents: [MdcSnackbarContainer, MdcSnackbarComponent] + declarations: [MdcSnackbarContainer, MdcSnackbarBase, MdcSnackbarComponent], + entryComponents: [MdcSnackbarContainer, MdcSnackbarComponent, MdcSnackbarBase] }) export class MdcSnackbarModule { } diff --git a/packages/snackbar/public-api.ts b/packages/snackbar/public-api.ts index 69c91a42d..aff52c8d4 100644 --- a/packages/snackbar/public-api.ts +++ b/packages/snackbar/public-api.ts @@ -2,5 +2,6 @@ export * from './module'; export * from './snackbar-container'; export * from './snackbar-config'; export * from './snackbar-ref'; +export * from './snackbar-base'; export * from './snackbar.component'; export * from './snackbar'; diff --git a/packages/snackbar/snackbar-base.ts b/packages/snackbar/snackbar-base.ts new file mode 100644 index 000000000..0a3a8f202 --- /dev/null +++ b/packages/snackbar/snackbar-base.ts @@ -0,0 +1,143 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + Inject, + OnDestroy, + OnInit, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import {LiveAnnouncer} from '@angular/cdk/a11y'; + +import {MDCComponent} from '@angular-mdc/web/base'; +import {MdcSnackbarRef, MdcSnackbarDismissReason} from './snackbar-ref'; +import {MDC_SNACKBAR_DATA, MdcSnackbarConfig} from './snackbar-config'; + +import {MDCSnackbarFoundation, MDCSnackbarAdapter} from '@material/snackbar'; + +@Component({ + moduleId: module.id, + selector: 'mdc-snackbar', + host: { + 'class': 'mdc-snackbar', + '[dir]': 'this.config.direction', + '[class.mdc-snackbar--stacked]': 'config.stacked', + '[class.mdc-snackbar--leading]': 'config.leading', + '[class.ngx-mdc-snackbar--trailing]': 'config.trailing', + '(keydown)': '_onKeydown($event)' + }, + template: ``, + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, + providers: [LiveAnnouncer] +}) +export class MdcSnackbarBase extends MDCComponent implements OnInit, OnDestroy { + @ViewChild('label', {static: true}) label!: ElementRef; + @ViewChild('action', {static: false}) action?: ElementRef; + @ViewChild('dismiss', {static: false}) dismiss?: ElementRef; + + get config(): MdcSnackbarConfig { + return this.snackbarRef.componentInstance.snackbarConfig; + } + + getDefaultFoundation() { + const adapter: MDCSnackbarAdapter = { + addClass: (className: string) => this._getHostElement().classList.add(className), + removeClass: (className: string) => this._getHostElement().classList.remove(className), + announce: () => this.label.nativeElement ? this.liveAnnouncer.announce(this.config.data.message, + this.config.politeness, this.config.timeoutMs || MDCSnackbarFoundation.numbers.ARIA_LIVE_DELAY_MS) : {}, + notifyClosing: () => {}, + notifyOpened: () => {}, + notifyOpening: () => {}, + notifyClosed: (reason: MdcSnackbarDismissReason | string) => this.snackbarRef.dismiss(reason) + }; + return new MDCSnackbarFoundation(adapter); + } + + constructor( + public changeDetectorRef: ChangeDetectorRef, + public liveAnnouncer: LiveAnnouncer, + public elementRef: ElementRef, + public snackbarRef: MdcSnackbarRef, + @Inject(MDC_SNACKBAR_DATA) public data: any) { + super(elementRef); + } + + ngOnInit(): void { + this.changeDetectorRef.detectChanges(); + + this._applyClasses(); + this._applyConfig(); + } + + ngOnDestroy(): void { + this._foundation?.destroy(); + } + + _onKeydown(evt: KeyboardEvent): void { + this._foundation.handleKeyDown(evt); + } + + _onActionClick(evt: MouseEvent): void { + this._foundation.handleActionButtonClick(evt); + } + + _onActionIconClick(evt: MouseEvent): void { + this._foundation.handleActionIconClick(evt); + } + + open(): void { + this._foundation.open(); + } + + close(reason?: MdcSnackbarDismissReason): void { + this._foundation.close(reason !== undefined ? reason.action ? 'action' : reason.dismiss ? 'dismiss' : '' : ''); + } + + private _applyClasses(): void { + const classes = this.config.classes; + if (classes) { + if (classes instanceof Array) { + this._getHostElement().classList.add(...this.config.classes as string[]); + } else { + this._getHostElement().classList.toggle(classes); + } + } + + const actionClasses = this.config.actionClasses; + if (actionClasses && this.action) { + if (actionClasses instanceof Array) { + this.action.nativeElement.classList.add(...this.config.actionClasses as string[]); + } else { + this.action.nativeElement.classList.toggle(actionClasses); + } + } + + if (this.dismiss) { + const dismissClasses = this.config.dismissClasses; + if (dismissClasses) { + if (dismissClasses instanceof Array) { + this.dismiss.nativeElement.classList.add(...this.config.dismissClasses as string[]); + } else { + this.dismiss.nativeElement.classList.toggle(dismissClasses); + } + } + } + } + + private _applyConfig(): void { + if (this.config.timeoutMs) { + this._foundation.setTimeoutMs(this.config.timeoutMs); + } + if (this.config.dismiss) { + this._foundation.setCloseOnEscape(this.config.closeOnEscape ? true : false); + } + } + + /** Retrieves the DOM element of the component host. */ + private _getHostElement(): HTMLElement { + return this.elementRef.nativeElement; + } +} diff --git a/packages/snackbar/snackbar-container.ts b/packages/snackbar/snackbar-container.ts index 24b897bc5..39c1dbda0 100644 --- a/packages/snackbar/snackbar-container.ts +++ b/packages/snackbar/snackbar-container.ts @@ -1,9 +1,3 @@ -import { - BasePortalOutlet, - CdkPortalOutlet, - ComponentPortal, - TemplatePortal, -} from '@angular/cdk/portal'; import { ChangeDetectionStrategy, Component, @@ -14,10 +8,17 @@ import { ViewChild, ViewEncapsulation, } from '@angular/core'; +import { + BasePortalOutlet, + CdkPortalOutlet, + ComponentPortal, + TemplatePortal, +} from '@angular/cdk/portal'; import {Subject} from 'rxjs'; import {take} from 'rxjs/operators'; import {MdcSnackbarConfig} from './snackbar-config'; + @Component({ moduleId: module.id, selector: 'mdc-snackbar-container', @@ -35,17 +36,18 @@ export class MdcSnackbarContainer extends BasePortalOutlet implements OnDestroy constructor( private _ngZone: NgZone, public snackbarConfig: MdcSnackbarConfig) { - super(); } /** Attach a component portal as content to this snackbar container. */ attachComponentPortal(portal: ComponentPortal): ComponentRef { + this._assertNotAttached(); return this._portalOutlet.attachComponentPortal(portal); } /** Attach a template portal as content to this snackbar container. */ attachTemplatePortal(portal: TemplatePortal): EmbeddedViewRef { + this._assertNotAttached(); return this._portalOutlet.attachTemplatePortal(portal); } @@ -63,4 +65,11 @@ export class MdcSnackbarContainer extends BasePortalOutlet implements OnDestroy this._onExit.complete(); }); } + + /** Asserts that no content is already attached to the container. */ + private _assertNotAttached() { + if (this._portalOutlet.hasAttached()) { + throw Error('Attempting to attach snackbar content after content is already attached'); + } + } } diff --git a/packages/snackbar/snackbar.component.ts b/packages/snackbar/snackbar.component.ts index ea02754b6..9923849d9 100644 --- a/packages/snackbar/snackbar.component.ts +++ b/packages/snackbar/snackbar.component.ts @@ -4,18 +4,13 @@ import { Component, ElementRef, Inject, - OnDestroy, - OnInit, - ViewChild, ViewEncapsulation } from '@angular/core'; import {LiveAnnouncer} from '@angular/cdk/a11y'; -import {MDCComponent} from '@angular-mdc/web/base'; -import {MdcSnackbarRef, MdcSnackbarDismissReason} from './snackbar-ref'; -import {MDC_SNACKBAR_DATA, MdcSnackbarConfig} from './snackbar-config'; - -import {MDCSnackbarFoundation, MDCSnackbarAdapter} from '@material/snackbar'; +import {MdcSnackbarRef} from './snackbar-ref'; +import {MDC_SNACKBAR_DATA} from './snackbar-config'; +import {MdcSnackbarBase} from './snackbar-base'; @Component({ moduleId: module.id, @@ -45,113 +40,13 @@ import {MDCSnackbarFoundation, MDCSnackbarAdapter} from '@material/snackbar'; encapsulation: ViewEncapsulation.None, providers: [LiveAnnouncer] }) -export class MdcSnackbarComponent extends MDCComponent implements OnInit, OnDestroy { - @ViewChild('label', {static: true}) label!: ElementRef; - @ViewChild('action', {static: false}) action?: ElementRef; - @ViewChild('dismiss', {static: false}) dismiss?: ElementRef; - - get config(): MdcSnackbarConfig { - return this.snackbarRef.componentInstance.snackbarConfig; - } - - getDefaultFoundation() { - const adapter: MDCSnackbarAdapter = { - addClass: (className: string) => this._getHostElement().classList.add(className), - removeClass: (className: string) => this._getHostElement().classList.remove(className), - announce: () => this.label.nativeElement ? this._liveAnnouncer.announce(this.config.data.message, - this.config.politeness, this.config.timeoutMs || MDCSnackbarFoundation.numbers.ARIA_LIVE_DELAY_MS) : {}, - notifyClosing: () => {}, - notifyOpened: () => {}, - notifyOpening: () => {}, - notifyClosed: (reason: MdcSnackbarDismissReason | string) => this.snackbarRef.dismiss(reason) - }; - return new MDCSnackbarFoundation(adapter); - } - +export class MdcSnackbarComponent extends MdcSnackbarBase { constructor( - private _changeDetectorRef: ChangeDetectorRef, - private _liveAnnouncer: LiveAnnouncer, - public elementRef: ElementRef, - public snackbarRef: MdcSnackbarRef, + changeDetectorRef: ChangeDetectorRef, + liveAnnouncer: LiveAnnouncer, + elementRef: ElementRef, + snackbarRef: MdcSnackbarRef, @Inject(MDC_SNACKBAR_DATA) public data: any) { - super(elementRef); - } - - ngOnInit(): void { - this._changeDetectorRef.detectChanges(); - - this._applyClasses(); - this._applyConfig(); - } - - ngOnDestroy(): void { - if (this._foundation) { - this._foundation.destroy(); - } - } - - _onKeydown(evt: KeyboardEvent): void { - this._foundation.handleKeyDown(evt); - } - - _onActionClick(evt: MouseEvent): void { - this._foundation.handleActionButtonClick(evt); - } - - _onActionIconClick(evt: MouseEvent): void { - this._foundation.handleActionIconClick(evt); - } - - open(): void { - this._foundation.open(); - } - - close(reason?: MdcSnackbarDismissReason): void { - this._foundation.close(reason !== undefined ? reason.action ? 'action' : reason.dismiss ? 'dismiss' : '' : ''); - } - - private _applyClasses(): void { - const classes = this.config.classes; - if (classes) { - if (classes instanceof Array) { - this._getHostElement().classList.add(...this.config.classes as string[]); - } else { - this._getHostElement().classList.toggle(classes); - } - } - - const actionClasses = this.config.actionClasses; - if (actionClasses && this.action) { - if (actionClasses instanceof Array) { - this.action.nativeElement.classList.add(...this.config.actionClasses as string[]); - } else { - this.action.nativeElement.classList.toggle(actionClasses); - } - } - - if (this.dismiss) { - const dismissClasses = this.config.dismissClasses; - if (dismissClasses) { - if (dismissClasses instanceof Array) { - this.dismiss.nativeElement.classList.add(...this.config.dismissClasses as string[]); - } else { - this.dismiss.nativeElement.classList.toggle(dismissClasses); - } - } - } - } - - private _applyConfig(): void { - if (this.config.timeoutMs) { - this._foundation.setTimeoutMs(this.config.timeoutMs); - } - if (this.config.dismiss) { - this._foundation.setCloseOnEscape(this.config.closeOnEscape ? true : false); - } - } - - /** Retrieves the DOM element of the component host. */ - private _getHostElement(): HTMLElement { - return this.elementRef.nativeElement; + super(changeDetectorRef, liveAnnouncer, elementRef, snackbarRef, data); } } diff --git a/packages/snackbar/snackbar.ts b/packages/snackbar/snackbar.ts index e9acd35e6..809be27b3 100644 --- a/packages/snackbar/snackbar.ts +++ b/packages/snackbar/snackbar.ts @@ -7,17 +7,19 @@ import { Injector, OnDestroy, Optional, - SkipSelf + SkipSelf, + TemplateRef } from '@angular/core'; import {Overlay, OverlayRef} from '@angular/cdk/overlay'; -import {ComponentPortal, ComponentType, PortalInjector} from '@angular/cdk/portal'; +import {ComponentPortal, ComponentType, PortalInjector, TemplatePortal} from '@angular/cdk/portal'; import {MdcSnackbarModule} from './module'; import {MdcSnackbarRef} from './snackbar-ref'; -import {MdcSnackbarComponent} from './snackbar.component'; +import {MdcSnackbarBase} from './snackbar-base'; import {MDC_SNACKBAR_DATA, MdcSnackbarConfig} from './snackbar-config'; import {MdcSnackbarContainer} from './snackbar-container'; +import {MdcSnackbarComponent} from './snackbar.component'; /** Injection token that can be used to specify default snackbar. */ export const MDC_SNACKBAR_DEFAULT_OPTIONS = @@ -36,7 +38,7 @@ export class MdcSnackbar implements OnDestroy { /** * Reference to the current snackbar in the view *at this level* (in the Angular injector tree). * If there is a parent snack-bar service, all operations should delegate to that parent - * via `_openedSnackBarRef`. + * via `_openedSnackbarRef`. */ private _snackBarRefAtThisLevel: MdcSnackbarRef | null = null; @@ -72,6 +74,18 @@ export class MdcSnackbar implements OnDestroy { return this._attach(component, config) as MdcSnackbarRef; } + /** + * Creates and dispatches a snackbar with a custom template for the content, removing any + * currently opened snackbars. + * + * @param template Template to be instantiated. + * @param config Extra configuration for the snackbar. + */ + openFromTemplate(template: TemplateRef, config?: MdcSnackbarConfig): + MdcSnackbarRef> { + return this._attach(template, config); + } + /** * Opens a snackbar with a message and an optional action. * @param message Message text. @@ -129,7 +143,7 @@ export class MdcSnackbar implements OnDestroy { /** * Places a new component or a template as the content of the snackbar container. */ - private _attach(content: ComponentType, userConfig?: MdcSnackbarConfig): + private _attach(content: ComponentType | TemplateRef, userConfig?: MdcSnackbarConfig): MdcSnackbarRef> { const config = {...new MdcSnackbarConfig(), ...this._defaultConfig, ...userConfig}; @@ -137,12 +151,21 @@ export class MdcSnackbar implements OnDestroy { const container = this._attachSnackbarContainer(overlayRef, config); const snackbarRef = new MdcSnackbarRef>(container, overlayRef); - const injector = this._createInjector(config, snackbarRef); - const portal = new ComponentPortal(content, undefined, injector); - const contentRef = container.attachComponentPortal(portal); + 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; + // We can't pass this via the injector, because the injector is created earlier. + snackbarRef.instance = contentRef.instance; + } this._loadListeners(snackbarRef); this._openedSnackbarRef = snackbarRef; @@ -162,9 +185,7 @@ export class MdcSnackbar implements OnDestroy { } }); - if (this._openedSnackbarRef) { - this._openedSnackbarRef.dismiss(); - } + this._openedSnackbarRef?.dismiss(); } /** diff --git a/test/snackbar/snackbar.test.ts b/test/snackbar/snackbar.test.ts index 96216b0a0..a82317957 100644 --- a/test/snackbar/snackbar.test.ts +++ b/test/snackbar/snackbar.test.ts @@ -68,10 +68,10 @@ describe('MdcSnackbar', () => { viewContainerFixture.detectChanges(); expect(snackBarRef.instance instanceof MdcSnackbarComponent) - .toBe(true, 'Expected the snack bar content component to be MdcSnackbarComponent'); + .toBe(true, 'Expected the snackbar content component to be MdcSnackbarComponent'); expect(snackBarRef.instance.snackbarRef) .toBe(snackBarRef, - 'Expected the snack bar reference to be placed in the component instance'); + 'Expected the snackbar reference to be placed in the component instance'); }); it('should open a snackbar with non-array CSS classes to apply', () => { @@ -85,10 +85,10 @@ describe('MdcSnackbar', () => { viewContainerFixture.detectChanges(); expect(snackBarRef.instance instanceof MdcSnackbarComponent) - .toBe(true, 'Expected the snack bar content component to be MdcSnackbarComponent'); + .toBe(true, 'Expected the snackbar content component to be MdcSnackbarComponent'); expect(snackBarRef.instance.snackbarRef) .toBe(snackBarRef, - 'Expected the snack bar reference to be placed in the component instance'); + 'Expected the snackbar reference to be placed in the component instance'); }); it('should open a snackbar with an array of CSS classes to apply', () => { @@ -102,10 +102,10 @@ describe('MdcSnackbar', () => { viewContainerFixture.detectChanges(); expect(snackBarRef.instance instanceof MdcSnackbarComponent) - .toBe(true, 'Expected the snack bar content component to be MdcSnackbarComponent'); + .toBe(true, 'Expected the snackbar content component to be MdcSnackbarComponent'); expect(snackBarRef.instance.snackbarRef) .toBe(snackBarRef, - 'Expected the snack bar reference to be placed in the component instance'); + 'Expected the snackbar reference to be placed in the component instance'); }); it('should open a snackbar with 10000 timeoutMs', () => { @@ -136,12 +136,12 @@ describe('MdcSnackbar', () => { viewContainerFixture.detectChanges(); expect(snackBarRef.instance instanceof MdcSnackbarComponent) - .toBe(true, 'Expected the snack bar content component to be MdcSnackbarComponent'); + .toBe(true, 'Expected the snackbar content component to be MdcSnackbarComponent'); expect(snackBarRef.instance.snackbarRef) - .toBe(snackBarRef, 'Expected the snack bar reference to be placed in the component instance'); + .toBe(snackBarRef, 'Expected the snackbar reference to be placed in the component instance'); }); - it('should dismiss the snack bar and remove itself from the view', fakeAsync(() => { + it('should dismiss the snackbar and remove itself from the view', fakeAsync(() => { const config: MdcSnackbarConfig = {viewContainerRef: testViewContainerRef}; const dismissCompleteSpy = jasmine.createSpy('dismiss complete spy'); @@ -246,7 +246,7 @@ describe('MdcSnackbar', () => { expect(viewContainerFixture.isStable()).toBe(true); })); - it('should dismiss the open snack bar on destroy', fakeAsync(() => { + it('should dismiss the open snackbar on destroy', fakeAsync(() => { snackBar.open(simpleMessage); viewContainerFixture.detectChanges(); expect(overlayContainerElement.childElementCount).toBeGreaterThan(0); @@ -257,6 +257,72 @@ describe('MdcSnackbar', () => { expect(overlayContainerElement.childElementCount).toBe(0); })); + + describe('with custom component', () => { + it('should open a custom component', () => { + const snackBarRef = snackBar.openFromComponent(BurritosNotification); + + expect(snackBarRef.instance instanceof BurritosNotification) + .toBe(true, 'Expected the snackbar content component to be BurritosNotification'); + expect(overlayContainerElement.textContent!.trim()) + .toBe('Burritos are on the way.', 'Expected component to have the proper text.'); + }); + + it('should inject the snackbar reference into the component', () => { + const snackBarRef = snackBar.openFromComponent(BurritosNotification); + + expect(snackBarRef.instance.snackBarRef) + .toBe(snackBarRef, 'Expected component to have an injected snackbar reference.'); + }); + + it('should be able to inject arbitrary user data', () => { + const snackBarRef = snackBar.openFromComponent(BurritosNotification, { + data: { + burritoType: 'Chimichanga' + } + }); + + expect(snackBarRef.instance.data).toBeTruthy('Expected component to have a data object.'); + expect(snackBarRef.instance.data.burritoType) + .toBe('Chimichanga', 'Expected the injected data object to be the one the user provided.'); + }); + }); + + describe('with TemplateRef', () => { + let templateFixture: ComponentFixture; + + beforeEach(() => { + templateFixture = TestBed.createComponent(ComponentWithTemplateRef); + templateFixture.detectChanges(); + }); + + it('should be able to open a snackbar using a TemplateRef', () => { + templateFixture.componentInstance.localValue = 'Pizza'; + snackBar.openFromTemplate(templateFixture.componentInstance.templateRef); + templateFixture.detectChanges(); + + const containerElement = overlayContainerElement.querySelector('mdc-snackbar-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'} + }); + templateFixture.detectChanges(); + + const containerElement = overlayContainerElement.querySelector('mdc-snackbar-container')!; + + expect(containerElement.textContent).toContain('Oranges'); + }); + }); }); describe('MdcSnackbar with parent MdcSnackbar', () => { @@ -288,12 +354,12 @@ describe('MdcSnackbar with parent MdcSnackbar', () => { overlayContainer.ngOnDestroy(); }); - it('should close snackBars opened by parent when opening from child', fakeAsync(() => { + it('should close snackbar opened by parent when opening from child', fakeAsync(() => { parentSnackBar.open('Pizza'); fixture.detectChanges(); tick(110); expect(overlayContainerElement.textContent) - .toContain('Pizza', 'Expected a snackBar to be opened'); + .toContain('Pizza', 'Expected a snackbar to be opened'); childSnackBar.open('Taco'); fixture.detectChanges(); @@ -304,12 +370,12 @@ describe('MdcSnackbar with parent MdcSnackbar', () => { flush(); })); - it('should close snackBars opened by child when opening from parent', fakeAsync(() => { + it('should close snackbar opened by child when opening from parent', fakeAsync(() => { childSnackBar.open('Pizza'); fixture.detectChanges(); tick(110); expect(overlayContainerElement.textContent) - .toContain('Pizza', 'Expected a snackBar to be opened'); + .toContain('Pizza', 'Expected a snackbar to be opened'); parentSnackBar.open('Taco'); fixture.detectChanges(); @@ -320,7 +386,7 @@ describe('MdcSnackbar with parent MdcSnackbar', () => { flush(); })); - it('should not dismiss parent snack bar if child is destroyed', fakeAsync(() => { + it('should not dismiss parent snackbar if child is destroyed', fakeAsync(() => { parentSnackBar.open('Pizza'); fixture.detectChanges(); tick(110); @@ -362,7 +428,7 @@ class ComponentWithChildViewContainer { `, }) class ComponentWithTemplateRef { - @ViewChild(TemplateRef, {static: false}) templateRef: TemplateRef; + @ViewChild(TemplateRef) templateRef: TemplateRef; localValue: string; } @@ -383,7 +449,7 @@ class ComponentThatProvidesMdcSnackBar { } /** - * Simple component to open snack bars from. + * Simple component to open snackbars from. * Create a real (non-test) NgModule as a workaround forRoot * https://github.com/angular/angular/issues/10760 */