mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
feat(snack-bar): add the ability to open from a TemplateRef (#10268)
Allows consumers to open a snack bar through a custom `TemplateRef`, in addition to a component type. Fixes #6136.
This commit is contained in:
committed by
Jeremy Elbourn
parent
749f2a0c82
commit
c44e253261
@@ -50,4 +50,12 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button mat-raised-button (click)="open()">OPEN</button>
|
||||
<p>
|
||||
<button mat-raised-button (click)="open()">OPEN</button>
|
||||
</p>
|
||||
|
||||
<button mat-raised-button (click)="openTemplate()">OPEN TEMPLATE</button>
|
||||
|
||||
<ng-template #template>
|
||||
Template snack bar: {{message}}
|
||||
</ng-template>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import {Dir} from '@angular/cdk/bidi';
|
||||
import {Component, ViewEncapsulation} from '@angular/core';
|
||||
import {Component, ViewEncapsulation, ViewChild, TemplateRef} from '@angular/core';
|
||||
import {
|
||||
MatSnackBar,
|
||||
MatSnackBarConfig,
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
preserveWhitespaces: false,
|
||||
})
|
||||
export class SnackBarDemo {
|
||||
@ViewChild('template') template: TemplateRef<any>;
|
||||
message: string = 'Snack Bar opened.';
|
||||
actionButtonLabel: string = 'Retry';
|
||||
action: boolean = false;
|
||||
@@ -38,12 +39,22 @@ export class SnackBarDemo {
|
||||
}
|
||||
|
||||
open() {
|
||||
let config = new MatSnackBarConfig();
|
||||
const config = this._createConfig();
|
||||
this.snackBar.open(this.message, this.action ? this.actionButtonLabel : undefined, config);
|
||||
}
|
||||
|
||||
openTemplate() {
|
||||
const config = this._createConfig();
|
||||
this.snackBar.openFromTemplate(this.template, config);
|
||||
}
|
||||
|
||||
private _createConfig() {
|
||||
const config = new MatSnackBarConfig();
|
||||
config.verticalPosition = this.verticalPosition;
|
||||
config.horizontalPosition = this.horizontalPosition;
|
||||
config.duration = this.setAutoHide ? this.autoHide : 0;
|
||||
config.panelClass = this.addExtraClass ? ['party'] : undefined;
|
||||
config.direction = this.dir.value;
|
||||
this.snackBar.open(this.message, this.action ? this.actionButtonLabel : undefined, config);
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
BasePortalOutlet,
|
||||
ComponentPortal,
|
||||
CdkPortalOutlet,
|
||||
TemplatePortal,
|
||||
} from '@angular/cdk/portal';
|
||||
import {take} from 'rxjs/operators/take';
|
||||
import {Observable} from 'rxjs/Observable';
|
||||
@@ -78,31 +79,16 @@ export class MatSnackBarContainer extends BasePortalOutlet implements OnDestroy
|
||||
|
||||
/** Attach a component portal as content to this snack bar container. */
|
||||
attachComponentPortal<T>(portal: ComponentPortal<T>): ComponentRef<T> {
|
||||
if (this._portalOutlet.hasAttached()) {
|
||||
throw Error('Attempting to attach snack bar content after content is already attached');
|
||||
}
|
||||
|
||||
const element: HTMLElement = this._elementRef.nativeElement;
|
||||
|
||||
if (this.snackBarConfig.panelClass || this.snackBarConfig.extraClasses) {
|
||||
this._setCssClasses(this.snackBarConfig.panelClass);
|
||||
this._setCssClasses(this.snackBarConfig.extraClasses);
|
||||
}
|
||||
|
||||
if (this.snackBarConfig.horizontalPosition === 'center') {
|
||||
element.classList.add('mat-snack-bar-center');
|
||||
}
|
||||
|
||||
if (this.snackBarConfig.verticalPosition === 'top') {
|
||||
element.classList.add('mat-snack-bar-top');
|
||||
}
|
||||
|
||||
this._assertNotAttached();
|
||||
this._applySnackBarClasses();
|
||||
return this._portalOutlet.attachComponentPortal(portal);
|
||||
}
|
||||
|
||||
/** Attach a template portal as content to this snack bar container. */
|
||||
attachTemplatePortal(): EmbeddedViewRef<any> {
|
||||
throw Error('Not yet implemented');
|
||||
attachTemplatePortal<C>(portal: TemplatePortal<C>): EmbeddedViewRef<C> {
|
||||
this._assertNotAttached();
|
||||
this._applySnackBarClasses();
|
||||
return this._portalOutlet.attachTemplatePortal(portal);
|
||||
}
|
||||
|
||||
/** Handle end of animations, updating the state of the snackbar. */
|
||||
@@ -171,4 +157,29 @@ export class MatSnackBarContainer extends BasePortalOutlet implements OnDestroy
|
||||
element.classList.add(classList);
|
||||
}
|
||||
}
|
||||
|
||||
/** Applies the various positioning and user-configured CSS classes to the snack bar. */
|
||||
private _applySnackBarClasses() {
|
||||
const element: HTMLElement = this._elementRef.nativeElement;
|
||||
|
||||
if (this.snackBarConfig.panelClass || this.snackBarConfig.extraClasses) {
|
||||
this._setCssClasses(this.snackBarConfig.panelClass);
|
||||
this._setCssClasses(this.snackBarConfig.extraClasses);
|
||||
}
|
||||
|
||||
if (this.snackBarConfig.horizontalPosition === 'center') {
|
||||
element.classList.add('mat-snack-bar-center');
|
||||
}
|
||||
|
||||
if (this.snackBarConfig.verticalPosition === 'top') {
|
||||
element.classList.add('mat-snack-bar-top');
|
||||
}
|
||||
}
|
||||
|
||||
/** Asserts that no content is already attached to the container. */
|
||||
private _assertNotAttached() {
|
||||
if (this._portalOutlet.hasAttached()) {
|
||||
throw Error('Attempting to attach snack bar content after content is already attached');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,15 @@ import {
|
||||
tick,
|
||||
flush,
|
||||
} from '@angular/core/testing';
|
||||
import {NgModule, Component, Directive, ViewChild, ViewContainerRef, Inject} from '@angular/core';
|
||||
import {
|
||||
NgModule,
|
||||
Component,
|
||||
Directive,
|
||||
ViewChild,
|
||||
ViewContainerRef,
|
||||
Inject,
|
||||
TemplateRef,
|
||||
} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
|
||||
import {OverlayContainer} from '@angular/cdk/overlay';
|
||||
@@ -464,6 +472,42 @@ describe('MatSnackBar', () => {
|
||||
|
||||
});
|
||||
|
||||
describe('with TemplateRef', () => {
|
||||
let templateFixture: ComponentFixture<ComponentWithTemplateRef>;
|
||||
|
||||
beforeEach(() => {
|
||||
templateFixture = TestBed.createComponent(ComponentWithTemplateRef);
|
||||
templateFixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should be able to open a snack bar using a TemplateRef', () => {
|
||||
templateFixture.componentInstance.localValue = 'Pizza';
|
||||
snackBar.openFromTemplate(templateFixture.componentInstance.templateRef);
|
||||
templateFixture.detectChanges();
|
||||
|
||||
const containerElement = overlayContainerElement.querySelector('snack-bar-container')!;
|
||||
|
||||
expect(containerElement.textContent).toContain('Fries');
|
||||
expect(containerElement.textContent).toContain('Pizza');
|
||||
|
||||
templateFixture.componentInstance.localValue = 'Pasta';
|
||||
templateFixture.detectChanges();
|
||||
|
||||
expect(containerElement.textContent).toContain('Pasta');
|
||||
});
|
||||
|
||||
it('should be able to pass in contextual data when opening with a TemplateRef', () => {
|
||||
snackBar.openFromTemplate(templateFixture.componentInstance.templateRef, {
|
||||
data: {value: 'Oranges'}
|
||||
});
|
||||
|
||||
const containerElement = overlayContainerElement.querySelector('snack-bar-container')!;
|
||||
|
||||
expect(containerElement.textContent).toContain('Oranges');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('MatSnackBar with parent MatSnackBar', () => {
|
||||
@@ -810,6 +854,20 @@ class ComponentWithChildViewContainer {
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'arbitrary-component-with-template-ref',
|
||||
template: `
|
||||
<ng-template let-data>
|
||||
Fries {{localValue}} {{data?.value}}
|
||||
</ng-template>
|
||||
`,
|
||||
})
|
||||
class ComponentWithTemplateRef {
|
||||
@ViewChild(TemplateRef) templateRef: TemplateRef<any>;
|
||||
localValue: string;
|
||||
}
|
||||
|
||||
|
||||
/** Simple component for testing ComponentPortal. */
|
||||
@Component({template: '<p>Burritos are on the way.</p>'})
|
||||
class BurritosNotification {
|
||||
@@ -835,7 +893,8 @@ class ComponentThatProvidesMatSnackBar {
|
||||
*/
|
||||
const TEST_DIRECTIVES = [ComponentWithChildViewContainer,
|
||||
BurritosNotification,
|
||||
DirectiveWithViewContainer];
|
||||
DirectiveWithViewContainer,
|
||||
ComponentWithTemplateRef];
|
||||
@NgModule({
|
||||
imports: [CommonModule, MatSnackBarModule],
|
||||
exports: TEST_DIRECTIVES,
|
||||
|
||||
@@ -9,15 +9,17 @@
|
||||
import {LiveAnnouncer} from '@angular/cdk/a11y';
|
||||
import {BreakpointObserver, Breakpoints} from '@angular/cdk/layout';
|
||||
import {Overlay, OverlayConfig, OverlayRef} from '@angular/cdk/overlay';
|
||||
import {ComponentPortal, ComponentType, PortalInjector} from '@angular/cdk/portal';
|
||||
import {ComponentPortal, TemplatePortal, ComponentType, PortalInjector} from '@angular/cdk/portal';
|
||||
import {
|
||||
ComponentRef,
|
||||
EmbeddedViewRef,
|
||||
Inject,
|
||||
Injectable,
|
||||
Injector,
|
||||
InjectionToken,
|
||||
Optional,
|
||||
SkipSelf,
|
||||
TemplateRef,
|
||||
} from '@angular/core';
|
||||
import {take} from 'rxjs/operators/take';
|
||||
import {takeUntil} from 'rxjs/operators/takeUntil';
|
||||
@@ -72,41 +74,21 @@ export class MatSnackBar {
|
||||
* @param component Component to be instantiated.
|
||||
* @param config Extra configuration for the snack bar.
|
||||
*/
|
||||
openFromComponent<T>(component: ComponentType<T>, config?: MatSnackBarConfig): MatSnackBarRef<T> {
|
||||
const _config = {...this._defaultConfig, ...config};
|
||||
const snackBarRef = this._attach(component, _config);
|
||||
openFromComponent<T>(component: ComponentType<T>, config?: MatSnackBarConfig):
|
||||
MatSnackBarRef<T> {
|
||||
return this._attach(component, config) as MatSnackBarRef<T>;
|
||||
}
|
||||
|
||||
// When the snackbar is dismissed, clear the reference to it.
|
||||
snackBarRef.afterDismissed().subscribe(() => {
|
||||
// Clear the snackbar ref if it hasn't already been replaced by a newer snackbar.
|
||||
if (this._openedSnackBarRef == snackBarRef) {
|
||||
this._openedSnackBarRef = null;
|
||||
}
|
||||
});
|
||||
|
||||
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();
|
||||
} 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.announcementMessage) {
|
||||
this._live.announce(_config.announcementMessage, _config.politeness);
|
||||
}
|
||||
|
||||
this._openedSnackBarRef = snackBarRef;
|
||||
return this._openedSnackBarRef;
|
||||
/**
|
||||
* Creates and dispatches a snack bar with a custom template for the content, removing any
|
||||
* currently opened snack bars.
|
||||
*
|
||||
* @param template Template to be instantiated.
|
||||
* @param config Extra configuration for the snack bar.
|
||||
*/
|
||||
openFromTemplate(template: TemplateRef<any>, config?: MatSnackBarConfig):
|
||||
MatSnackBarRef<EmbeddedViewRef<any>> {
|
||||
return this._attach(template, config);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -148,18 +130,31 @@ export class MatSnackBar {
|
||||
}
|
||||
|
||||
/**
|
||||
* Places a new component as the content of the snack bar container.
|
||||
* Places a new component or a template as the content of the snack bar container.
|
||||
*/
|
||||
private _attach<T>(component: ComponentType<T>, config: MatSnackBarConfig): MatSnackBarRef<T> {
|
||||
private _attach<T>(content: ComponentType<T> | TemplateRef<T>, userConfig?: MatSnackBarConfig):
|
||||
MatSnackBarRef<T | EmbeddedViewRef<any>> {
|
||||
|
||||
const config = {...this._defaultConfig, ...userConfig};
|
||||
const overlayRef = this._createOverlay(config);
|
||||
const container = this._attachSnackBarContainer(overlayRef, config);
|
||||
const snackBarRef = new MatSnackBarRef<T>(container, overlayRef);
|
||||
const injector = this._createInjector(config, snackBarRef);
|
||||
const portal = new ComponentPortal(component, undefined, injector);
|
||||
const contentRef = container.attachComponentPortal(portal);
|
||||
const snackBarRef = new MatSnackBarRef<T | EmbeddedViewRef<any>>(container, overlayRef);
|
||||
|
||||
// We can't pass this via the injector, because the injector is created earlier.
|
||||
snackBarRef.instance = contentRef.instance;
|
||||
if (content instanceof TemplateRef) {
|
||||
const portal = new TemplatePortal(content, null!, {
|
||||
$implicit: config.data,
|
||||
snackBarRef
|
||||
} as any);
|
||||
|
||||
snackBarRef.instance = container.attachTemplatePortal(portal);
|
||||
} else {
|
||||
const injector = this._createInjector(config, snackBarRef);
|
||||
const portal = new ComponentPortal(content, undefined, injector);
|
||||
const contentRef = container.attachComponentPortal<T>(portal);
|
||||
|
||||
// We can't pass this via the injector, because the injector is created earlier.
|
||||
snackBarRef.instance = contentRef.instance;
|
||||
}
|
||||
|
||||
// Subscribe to the breakpoint observer and attach the mat-snack-bar-handset class as
|
||||
// appropriate. This class is applied to the overlay element because the overlay must expand to
|
||||
@@ -174,7 +169,41 @@ export class MatSnackBar {
|
||||
}
|
||||
});
|
||||
|
||||
return snackBarRef;
|
||||
this._animateSnackBar(snackBarRef, config);
|
||||
this._openedSnackBarRef = snackBarRef;
|
||||
return this._openedSnackBarRef;
|
||||
}
|
||||
|
||||
/** Animates the old snack bar out and the new one in. */
|
||||
private _animateSnackBar(snackBarRef: MatSnackBarRef<any>, config: MatSnackBarConfig) {
|
||||
// When the snackbar is dismissed, clear the reference to it.
|
||||
snackBarRef.afterDismissed().subscribe(() => {
|
||||
// Clear the snackbar ref if it hasn't already been replaced by a newer snackbar.
|
||||
if (this._openedSnackBarRef == snackBarRef) {
|
||||
this._openedSnackBarRef = null;
|
||||
}
|
||||
});
|
||||
|
||||
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();
|
||||
} 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.announcementMessage) {
|
||||
this._live.announce(config.announcementMessage, config.politeness);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user