From baba6efed4ffa02390d036ea6fd4de7bb6183a52 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Tue, 11 Jul 2017 18:30:40 +0200 Subject: [PATCH] feat(snack-bar): inject data and MdSnackBarRef into custom snack-bar component (#5383) * feat(snack-bar): inject data and MdSnackBarRef into custom snack-bar component * Injects the `MdSnackBarRef` into custom snack bar instances. * Adds the option to inject arbitrary data into a snack bar. * Turns the `DialogInjector` into a `PortalInjector` to allow it to be used elsewhere (e.g. the snack-bar). * Minor tweaks to the `SimpleSnackBar` to get it to use the new DI tokens instead of assigning data directly to the component instance. Fixes #5371. --- .../portal/portal-injector.ts} | 8 +- src/lib/dialog/dialog.ts | 6 +- src/lib/snack-bar/simple-snack-bar.html | 8 +- src/lib/snack-bar/simple-snack-bar.ts | 19 ++-- src/lib/snack-bar/snack-bar-config.ts | 7 +- src/lib/snack-bar/snack-bar-ref.ts | 13 +-- src/lib/snack-bar/snack-bar.md | 33 ++++++- src/lib/snack-bar/snack-bar.spec.ts | 59 +++++++++--- src/lib/snack-bar/snack-bar.ts | 92 ++++++++++++------- 9 files changed, 168 insertions(+), 77 deletions(-) rename src/lib/{dialog/dialog-injector.ts => core/portal/portal-injector.ts} (76%) diff --git a/src/lib/dialog/dialog-injector.ts b/src/lib/core/portal/portal-injector.ts similarity index 76% rename from src/lib/dialog/dialog-injector.ts rename to src/lib/core/portal/portal-injector.ts index 9dc99537d135..98945fa75299 100644 --- a/src/lib/dialog/dialog-injector.ts +++ b/src/lib/core/portal/portal-injector.ts @@ -8,8 +8,12 @@ import {Injector} from '@angular/core'; -/** Custom injector type specifically for instantiating components with a dialog. */ -export class DialogInjector implements Injector { +/** + * Custom injector to be used when providing custom + * injection tokens to components inside a portal. + * @docs-private + */ +export class PortalInjector implements Injector { constructor( private _parentInjector: Injector, private _customTokens: WeakMap) { } diff --git a/src/lib/dialog/dialog.ts b/src/lib/dialog/dialog.ts index dd93c5b22901..2a5b830c882a 100644 --- a/src/lib/dialog/dialog.ts +++ b/src/lib/dialog/dialog.ts @@ -25,9 +25,9 @@ import { OverlayState, ComponentPortal, } from '../core'; +import {PortalInjector} from '../core/portal/portal-injector'; import {extendObject} from '../core/util/object-extend'; import {ESCAPE} from '../core/keyboard/keycodes'; -import {DialogInjector} from './dialog-injector'; import {MdDialogConfig} from './dialog-config'; import {MdDialogRef} from './dialog-ref'; import {MdDialogContainer} from './dialog-container'; @@ -222,7 +222,7 @@ export class MdDialog { private _createInjector( config: MdDialogConfig, dialogRef: MdDialogRef, - dialogContainer: MdDialogContainer): DialogInjector { + dialogContainer: MdDialogContainer): PortalInjector { let userInjector = config && config.viewContainerRef && config.viewContainerRef.injector; let injectionTokens = new WeakMap(); @@ -231,7 +231,7 @@ export class MdDialog { injectionTokens.set(MdDialogContainer, dialogContainer); injectionTokens.set(MD_DIALOG_DATA, config.data); - return new DialogInjector(userInjector || this._injector, injectionTokens); + return new PortalInjector(userInjector || this._injector, injectionTokens); } /** diff --git a/src/lib/snack-bar/simple-snack-bar.html b/src/lib/snack-bar/simple-snack-bar.html index 52d14fb40b35..cff85a370ce5 100644 --- a/src/lib/snack-bar/simple-snack-bar.html +++ b/src/lib/snack-bar/simple-snack-bar.html @@ -1,2 +1,6 @@ -{{message}} - +{{data.message}} + + diff --git a/src/lib/snack-bar/simple-snack-bar.ts b/src/lib/snack-bar/simple-snack-bar.ts index 2f3b71ad6e24..4b247562611d 100644 --- a/src/lib/snack-bar/simple-snack-bar.ts +++ b/src/lib/snack-bar/simple-snack-bar.ts @@ -6,8 +6,9 @@ * found in the LICENSE file at https://angular.io/license */ -import {Component, ViewEncapsulation, ChangeDetectionStrategy} from '@angular/core'; +import {Component, ViewEncapsulation, Inject, ChangeDetectionStrategy} from '@angular/core'; import {MdSnackBarRef} from './snack-bar-ref'; +import {MD_SNACK_BAR_DATA} from './snack-bar-config'; /** @@ -26,14 +27,14 @@ import {MdSnackBarRef} from './snack-bar-ref'; } }) export class SimpleSnackBar { - /** The message to be shown in the snack bar. */ - message: string; + /** Data that was injected into the snack bar. */ + data: { message: string, action: string }; - /** The label for the button in the snack bar. */ - action: string; - - /** The instance of the component making up the content of the snack bar. */ - snackBarRef: MdSnackBarRef; + constructor( + public snackBarRef: MdSnackBarRef, + @Inject(MD_SNACK_BAR_DATA) data: any) { + this.data = data; + } /** Dismisses the snack bar. */ dismiss(): void { @@ -42,6 +43,6 @@ export class SimpleSnackBar { /** If the action button should be shown. */ get hasAction(): boolean { - return !!this.action; + return !!this.data.action; } } diff --git a/src/lib/snack-bar/snack-bar-config.ts b/src/lib/snack-bar/snack-bar-config.ts index 1979520ed8e7..11c8046918cd 100644 --- a/src/lib/snack-bar/snack-bar-config.ts +++ b/src/lib/snack-bar/snack-bar-config.ts @@ -6,9 +6,11 @@ * found in the LICENSE file at https://angular.io/license */ -import {ViewContainerRef} from '@angular/core'; +import {ViewContainerRef, InjectionToken} from '@angular/core'; import {AriaLivePoliteness, Direction} from '../core'; +export const MD_SNACK_BAR_DATA = new InjectionToken('MdSnackBarData'); + /** * Configuration used when opening a snack-bar. */ @@ -30,4 +32,7 @@ export class MdSnackBarConfig { /** Text layout direction for the snack bar. */ direction?: Direction = 'ltr'; + + /** Data being injected into the child component. */ + data?: any = null; } diff --git a/src/lib/snack-bar/snack-bar-ref.ts b/src/lib/snack-bar/snack-bar-ref.ts index 6028a9c2416b..a56bdc64b381 100644 --- a/src/lib/snack-bar/snack-bar-ref.ts +++ b/src/lib/snack-bar/snack-bar-ref.ts @@ -11,18 +11,12 @@ import {Observable} from 'rxjs/Observable'; import {Subject} from 'rxjs/Subject'; import {MdSnackBarContainer} from './snack-bar-container'; -// TODO(josephperrott): Implement onAction observable. - /** * Reference to a snack bar dispatched from the snack bar service. */ export class MdSnackBarRef { - private _instance: T; - /** The instance of the component making up the content of the snack bar. */ - get instance(): T { - return this._instance; - } + instance: T; /** * The instance of the component making up the content of the snack bar. @@ -45,11 +39,8 @@ export class MdSnackBarRef { */ private _durationTimeoutId: number; - constructor(instance: T, - containerInstance: MdSnackBarContainer, + constructor(containerInstance: MdSnackBarContainer, private _overlayRef: OverlayRef) { - // Sets the readonly instance of the snack bar content component. - this._instance = instance; this.containerInstance = containerInstance; // Dismiss snackbar on action. this.onAction().subscribe(() => this.dismiss()); diff --git a/src/lib/snack-bar/snack-bar.md b/src/lib/snack-bar/snack-bar.md index b42281068f09..9054620e02d9 100644 --- a/src/lib/snack-bar/snack-bar.md +++ b/src/lib/snack-bar/snack-bar.md @@ -15,9 +15,11 @@ let snackBarRef = snackBar.open('Message archived', 'Undo'); let snackBarRef = snackbar.openFromComponent(MessageArchivedComponent); ``` -In either case, an `MdSnackBarRef` is returned. This can be used to dismiss the snack-bar or to -receive notification of when the snack-bar is dismissed. For simple messages with an action, the +In either case, a `MdSnackBarRef` is returned. This can be used to dismiss the snack-bar or to +receive notification of when the snack-bar is dismissed. For simple messages with an action, the `MdSnackBarRef` exposes an observable for when the action is triggered. +If you want to close a custom snack-bar that was opened via `openFromComponent`, from within the +component itself, you can inject the `MdSnackBarRef`. ```ts snackBarRef.afterDismissed().subscribe(() => { @@ -33,7 +35,7 @@ snackBarRef.dismiss(); ``` ### Dismissal -A snack-bar can be dismissed manually by calling the `dismiss` method on the `MdSnackBarRef` +A snack-bar can be dismissed manually by calling the `dismiss` method on the `MdSnackBarRef` returned from the call to `open`. Only one snack-bar can ever be opened at one time. If a new snackbar is opened while a previous @@ -45,3 +47,28 @@ snackbar.open('Message archived', 'Undo', { duration: 3000 }); ``` + +### Sharing data with a custom snack-bar. +You can share data with the custom snack-bar, that you opened via the `openFromComponent` method, +by passing it through the `data` property. + +```ts +snackbar.openFromComponent(MessageArchivedComponent, { + data: 'some data' +}); +``` + +To access the data in your component, you have to use the `MD_SNACK_BAR_DATA` injection token: + +```ts +import {Component, Inject} from '@angular/core'; +import {MD_SNACK_BAR_DATA} from '@angular/material'; + +@Component({ + selector: 'your-snack-bar', + template: 'passed in {{ data }}', +}) +export class MessageArchivedComponent { + constructor(@Inject(MD_SNACK_BAR_DATA) public data: any) { } +} +``` diff --git a/src/lib/snack-bar/snack-bar.spec.ts b/src/lib/snack-bar/snack-bar.spec.ts index ff3232a5ca0e..730e55cf66f4 100644 --- a/src/lib/snack-bar/snack-bar.spec.ts +++ b/src/lib/snack-bar/snack-bar.spec.ts @@ -7,11 +7,18 @@ import { flushMicrotasks, tick } from '@angular/core/testing'; -import {NgModule, Component, Directive, ViewChild, ViewContainerRef} from '@angular/core'; +import {NgModule, Component, Directive, ViewChild, ViewContainerRef, Inject} from '@angular/core'; import {CommonModule} from '@angular/common'; import {NoopAnimationsModule} from '@angular/platform-browser/animations'; -import {MdSnackBarModule, MdSnackBar, MdSnackBarConfig, SimpleSnackBar} from './index'; import {OverlayContainer, LiveAnnouncer} from '../core'; +import { + MdSnackBarModule, + MdSnackBar, + MdSnackBarConfig, + MdSnackBarRef, + SimpleSnackBar, + MD_SNACK_BAR_DATA, +} from './index'; // TODO(josephperrott): Update tests to mock waiting for time to complete for animations. @@ -174,17 +181,6 @@ describe('MdSnackBar', () => { }); })); - it('should open a custom component', () => { - let config = {viewContainerRef: testViewContainerRef}; - let snackBarRef = snackBar.openFromComponent(BurritosNotification, config); - - expect(snackBarRef.instance instanceof BurritosNotification) - .toBe(true, 'Expected the snack bar content component to be BurritosNotification'); - expect(overlayContainerElement.textContent!.trim()) - .toBe('Burritos are on the way.', - `Expected the overlay text content to be 'Burritos are on the way'`); - }); - it('should set the animation state to visible on entry', () => { let config = {viewContainerRef: testViewContainerRef}; let snackBarRef = snackBar.open(simpleMessage, undefined, config); @@ -361,6 +357,37 @@ describe('MdSnackBar', () => { expect(pane.getAttribute('dir')).toBe('rtl', 'Expected the pane to be in RTL mode.'); }); + describe('with custom component', () => { + it('should open a custom component', () => { + const snackBarRef = snackBar.openFromComponent(BurritosNotification); + + expect(snackBarRef.instance instanceof BurritosNotification) + .toBe(true, 'Expected the snack bar 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 snack bar reference into the component', () => { + const snackBarRef = snackBar.openFromComponent(BurritosNotification); + + expect(snackBarRef.instance.snackBarRef) + .toBe(snackBarRef, 'Expected component to have an injected snack bar 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('MdSnackBar with parent MdSnackBar', () => { @@ -453,7 +480,11 @@ class ComponentWithChildViewContainer { /** Simple component for testing ComponentPortal. */ @Component({template: '

Burritos are on the way.

'}) -class BurritosNotification {} +class BurritosNotification { + constructor( + public snackBarRef: MdSnackBarRef, + @Inject(MD_SNACK_BAR_DATA) public data: any) { } +} @Component({ diff --git a/src/lib/snack-bar/snack-bar.ts b/src/lib/snack-bar/snack-bar.ts index 581435bb0a2a..c51a1efcaa65 100644 --- a/src/lib/snack-bar/snack-bar.ts +++ b/src/lib/snack-bar/snack-bar.ts @@ -6,7 +6,13 @@ * found in the LICENSE file at https://angular.io/license */ -import {Injectable, ComponentRef, Optional, SkipSelf} from '@angular/core'; +import { + Injectable, + ComponentRef, + Optional, + SkipSelf, + Injector, +} from '@angular/core'; import { ComponentType, ComponentPortal, @@ -15,11 +21,12 @@ import { OverlayState, LiveAnnouncer, } from '../core'; -import {MdSnackBarConfig} from './snack-bar-config'; +import {PortalInjector} from '../core/portal/portal-injector'; +import {extendObject} from '../core/util/object-extend'; +import {MdSnackBarConfig, MD_SNACK_BAR_DATA} from './snack-bar-config'; import {MdSnackBarRef} from './snack-bar-ref'; import {MdSnackBarContainer} from './snack-bar-container'; import {SimpleSnackBar} from './simple-snack-bar'; -import {extendObject} from '../core/util/object-extend'; /** @@ -36,7 +43,7 @@ export class MdSnackBar { /** Reference to the currently opened snackbar at *any* level. */ get _openedSnackBarRef(): MdSnackBarRef | null { - let parent = this._parentSnackBar; + const parent = this._parentSnackBar; return parent ? parent._openedSnackBarRef : this._snackBarRefAtThisLevel; } @@ -51,6 +58,7 @@ export class MdSnackBar { constructor( private _overlay: Overlay, private _live: LiveAnnouncer, + private _injector: Injector, @Optional() @SkipSelf() private _parentSnackBar: MdSnackBar) {} /** @@ -61,10 +69,8 @@ export class MdSnackBar { * @param config Extra configuration for the snack bar. */ openFromComponent(component: ComponentType, config?: MdSnackBarConfig): MdSnackBarRef { - config = _applyConfigDefaults(config); - let overlayRef = this._createOverlay(config); - let snackBarContainer = this._attachSnackBarContainer(overlayRef, config); - let snackBarRef = this._attachSnackbarContent(component, snackBarContainer, overlayRef); + const _config = _applyConfigDefaults(config); + const snackBarRef = this._attach(component, _config); // When the snackbar is dismissed, clear the reference to it. snackBarRef.afterDismissed().subscribe(() => { @@ -74,27 +80,25 @@ export class MdSnackBar { } }); - // If a snack bar is already in view, dismiss it and enter the new snack bar after exit - // animation is complete. 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(); - // If no snack bar is in view, enter the new snack bar. } 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.duration && _config.duration > 0) { + snackBarRef.afterOpened().subscribe(() => snackBarRef._dismissAfter(_config!.duration!)); } - if (config.announcementMessage) { - this._live.announce(config.announcementMessage, config.politeness); + if (_config.announcementMessage) { + this._live.announce(_config.announcementMessage, _config.politeness); } this._openedSnackBarRef = snackBarRef; @@ -108,14 +112,14 @@ export class MdSnackBar { * @param config Additional configuration options for the snackbar. */ open(message: string, action = '', config?: MdSnackBarConfig): MdSnackBarRef { - let _config = _applyConfigDefaults(config); + const _config = _applyConfigDefaults(config); + + // Since the user doesn't have access to the component, we can + // override the data to pass in our own message and action. + _config.data = {message, action}; _config.announcementMessage = message; - let simpleSnackBarRef = this.openFromComponent(SimpleSnackBar, _config); - simpleSnackBarRef.instance.snackBarRef = simpleSnackBarRef; - simpleSnackBarRef.instance.message = message; - simpleSnackBarRef.instance.action = action; - return simpleSnackBarRef; + return this.openFromComponent(SimpleSnackBar, _config); } /** @@ -132,8 +136,8 @@ export class MdSnackBar { */ private _attachSnackBarContainer(overlayRef: OverlayRef, config: MdSnackBarConfig): MdSnackBarContainer { - let containerPortal = new ComponentPortal(MdSnackBarContainer, config.viewContainerRef); - let containerRef: ComponentRef = overlayRef.attach(containerPortal); + const containerPortal = new ComponentPortal(MdSnackBarContainer, config.viewContainerRef); + const containerRef: ComponentRef = overlayRef.attach(containerPortal); containerRef.instance.snackBarConfig = config; return containerRef.instance; } @@ -141,12 +145,18 @@ export class MdSnackBar { /** * Places a new component as the content of the snack bar container. */ - private _attachSnackbarContent(component: ComponentType, - container: MdSnackBarContainer, - overlayRef: OverlayRef): MdSnackBarRef { - let portal = new ComponentPortal(component); - let contentRef = container.attachComponentPortal(portal); - return new MdSnackBarRef(contentRef.instance, container, overlayRef); + private _attach(component: ComponentType, config: MdSnackBarConfig): MdSnackBarRef { + const overlayRef = this._createOverlay(config); + const container = this._attachSnackBarContainer(overlayRef, config); + const snackBarRef = new MdSnackBarRef(container, overlayRef); + const injector = this._createInjector(config, snackBarRef); + const portal = new ComponentPortal(component, 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; + + return snackBarRef; } /** @@ -154,11 +164,29 @@ export class MdSnackBar { * @param config The user-specified snack bar config. */ private _createOverlay(config: MdSnackBarConfig): OverlayRef { - let state = new OverlayState(); + const state = new OverlayState(); state.direction = config.direction; state.positionStrategy = this._overlay.position().global().centerHorizontally().bottom('0'); return this._overlay.create(state); } + + /** + * Creates an injector to be used inside of a snack bar component. + * @param config Config that was used to create the snack bar. + * @param snackBarRef Reference to the snack bar. + */ + private _createInjector( + config: MdSnackBarConfig, + snackBarRef: MdSnackBarRef): PortalInjector { + + const userInjector = config && config.viewContainerRef && config.viewContainerRef.injector; + const injectionTokens = new WeakMap(); + + injectionTokens.set(MdSnackBarRef, snackBarRef); + injectionTokens.set(MD_SNACK_BAR_DATA, config.data); + + return new PortalInjector(userInjector || this._injector, injectionTokens); + } } /**