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.
This commit is contained in:
Kristiyan Kostadinov
2017-07-11 18:30:40 +02:00
committed by Jeremy Elbourn
parent 8c13325ea6
commit baba6efed4
9 changed files with 168 additions and 77 deletions
@@ -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<any, any>) { }
+3 -3
View File
@@ -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<T>(
config: MdDialogConfig,
dialogRef: MdDialogRef<T>,
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);
}
/**
+6 -2
View File
@@ -1,2 +1,6 @@
{{message}}
<button class="mat-simple-snackbar-action" *ngIf="hasAction" (click)="dismiss()">{{action}}</button>
{{data.message}}
<button
class="mat-simple-snackbar-action"
*ngIf="hasAction"
(click)="dismiss()">{{data.action}}</button>
+10 -9
View File
@@ -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<SimpleSnackBar>;
constructor(
public snackBarRef: MdSnackBarRef<SimpleSnackBar>,
@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;
}
}
+6 -1
View File
@@ -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<any>('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;
}
+2 -11
View File
@@ -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<T> {
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<T> {
*/
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());
+30 -3
View File
@@ -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) { }
}
```
+45 -14
View File
@@ -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: '<p>Burritos are on the way.</p>'})
class BurritosNotification {}
class BurritosNotification {
constructor(
public snackBarRef: MdSnackBarRef<BurritosNotification>,
@Inject(MD_SNACK_BAR_DATA) public data: any) { }
}
@Component({
+60 -32
View File
@@ -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<any> | 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<T>(component: ComponentType<T>, config?: MdSnackBarConfig): MdSnackBarRef<T> {
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<SimpleSnackBar> {
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<MdSnackBarContainer> = overlayRef.attach(containerPortal);
const containerPortal = new ComponentPortal(MdSnackBarContainer, config.viewContainerRef);
const containerRef: ComponentRef<MdSnackBarContainer> = 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<T>(component: ComponentType<T>,
container: MdSnackBarContainer,
overlayRef: OverlayRef): MdSnackBarRef<T> {
let portal = new ComponentPortal(component);
let contentRef = container.attachComponentPortal(portal);
return new MdSnackBarRef(contentRef.instance, container, overlayRef);
private _attach<T>(component: ComponentType<T>, config: MdSnackBarConfig): MdSnackBarRef<T> {
const overlayRef = this._createOverlay(config);
const container = this._attachSnackBarContainer(overlayRef, config);
const snackBarRef = new MdSnackBarRef<T>(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<T>(
config: MdSnackBarConfig,
snackBarRef: MdSnackBarRef<T>): 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);
}
}
/**