Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(snack-bar): inject data and MdSnackBarRef into custom snack-bar component #5383

Merged
merged 2 commits into from
Jul 11, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add readonly

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't, because the instance gets assigned by the service right after the component is instantiated. This is why I had to remove the getter in the first place. The issue is that we need the snack bar ref to create the injector and we need the inejctor to create the component. Alternatively, I can make the injection tokens on the custom injector public, but that seems even hackier.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, got it


/**
* 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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Loading