mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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:
committed by
Jeremy Elbourn
parent
8c13325ea6
commit
baba6efed4
@@ -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>) { }
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,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;
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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) { }
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user