Skip to content

Commit

Permalink
feat(snack-bar): inject data and MdSnackBarRef into custom snack-bar …
Browse files Browse the repository at this point in the history
…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.
  • Loading branch information
crisbeto authored and jelbourn committed Jul 11, 2017
1 parent 8c13325 commit baba6ef
Show file tree
Hide file tree
Showing 9 changed files with 168 additions and 77 deletions.
Expand Up @@ -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>) { }
Expand Down
6 changes: 3 additions & 3 deletions src/lib/dialog/dialog.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -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();
Expand All @@ -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);
}

/**
Expand Down
8 changes: 6 additions & 2 deletions src/lib/snack-bar/simple-snack-bar.html
@@ -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>
19 changes: 10 additions & 9 deletions src/lib/snack-bar/simple-snack-bar.ts
Expand Up @@ -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';


/**
Expand All @@ -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 {
Expand All @@ -42,6 +43,6 @@ export class SimpleSnackBar {

/** If the action button should be shown. */
get hasAction(): boolean {
return !!this.action;
return !!this.data.action;
}
}
7 changes: 6 additions & 1 deletion src/lib/snack-bar/snack-bar-config.ts
Expand Up @@ -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.
*/
Expand All @@ -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;
}
13 changes: 2 additions & 11 deletions src/lib/snack-bar/snack-bar-ref.ts
Expand Up @@ -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.
Expand All @@ -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());
Expand Down
33 changes: 30 additions & 3 deletions src/lib/snack-bar/snack-bar.md
Expand Up @@ -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(() => {
Expand All @@ -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
Expand All @@ -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) { }
}
```
59 changes: 45 additions & 14 deletions src/lib/snack-bar/snack-bar.spec.ts
Expand Up @@ -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.
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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({
Expand Down

0 comments on commit baba6ef

Please sign in to comment.