diff --git a/src/lib/menu/menu-trigger.ts b/src/lib/menu/menu-trigger.ts
index 784903c3bd4c..5c7ff283dcd7 100644
--- a/src/lib/menu/menu-trigger.ts
+++ b/src/lib/menu/menu-trigger.ts
@@ -84,6 +84,7 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
private _menuOpen: boolean = false;
private _closeSubscription = Subscription.EMPTY;
private _hoverSubscription = Subscription.EMPTY;
+ private _menuCloseSubscription = Subscription.EMPTY;
private _scrollStrategy: () => ScrollStrategy;
// Tracking input type is necessary so it's possible to only auto-focus
@@ -95,16 +96,34 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
* @breaking-change 8.0.0
*/
@Input('mat-menu-trigger-for')
- get _deprecatedMatMenuTriggerFor(): MatMenuPanel {
- return this.menu;
- }
-
+ get _deprecatedMatMenuTriggerFor(): MatMenuPanel { return this.menu; }
set _deprecatedMatMenuTriggerFor(v: MatMenuPanel) {
this.menu = v;
}
/** References the menu instance that the trigger is associated with. */
- @Input('matMenuTriggerFor') menu: MatMenuPanel;
+ @Input('matMenuTriggerFor')
+ get menu() { return this._menu; }
+ set menu(menu: MatMenuPanel) {
+ if (menu === this._menu) {
+ return;
+ }
+
+ this._menu = menu;
+ this._menuCloseSubscription.unsubscribe();
+
+ if (menu) {
+ this._menuCloseSubscription = menu.close.asObservable().subscribe(reason => {
+ this._destroyMenu();
+
+ // If a click closed the menu, we should close the entire chain of nested menus.
+ if ((reason === 'click' || reason === 'tab') && this._parentMenu) {
+ this._parentMenu.closed.emit(reason);
+ }
+ });
+ }
+ }
+ private _menu: MatMenuPanel;
/** Data to be passed along to any lazily-rendered content. */
@Input('matMenuTriggerData') menuData: any;
@@ -151,16 +170,6 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
ngAfterContentInit() {
this._checkMenu();
-
- this.menu.close.asObservable().subscribe(reason => {
- this._destroyMenu();
-
- // If a click closed the menu, we should close the entire chain of nested menus.
- if ((reason === 'click' || reason === 'tab') && this._parentMenu) {
- this._parentMenu.closed.emit(reason);
- }
- });
-
this._handleHover();
}
@@ -203,7 +212,7 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
const overlayRef = this._createOverlay();
this._setPosition(overlayRef.getConfig().positionStrategy as FlexibleConnectedPositionStrategy);
- overlayRef.attach(this._portal);
+ overlayRef.attach(this._getPortal());
if (this.menu.lazyContent) {
this.menu.lazyContent.attach(this.menuData);
@@ -347,7 +356,6 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
*/
private _createOverlay(): OverlayRef {
if (!this._overlayRef) {
- this._portal = new TemplatePortal(this.menu.templateRef, this._viewContainerRef);
const config = this._getOverlayConfig();
this._subscribeToPositions(config.positionStrategy as FlexibleConnectedPositionStrategy);
this._overlayRef = this._overlay.create(config);
@@ -531,4 +539,16 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
});
}
+ /** Gets the portal that should be attached to the overlay. */
+ private _getPortal(): TemplatePortal {
+ // Note that we can avoid this check by keeping the portal on the menu panel.
+ // While it would be cleaner, we'd have to introduce another required method on
+ // `MatMenuPanel`, making it harder to consume.
+ if (!this._portal || this._portal.templateRef !== this.menu.templateRef) {
+ this._portal = new TemplatePortal(this.menu.templateRef, this._viewContainerRef);
+ }
+
+ return this._portal;
+ }
+
}
diff --git a/src/lib/menu/menu.spec.ts b/src/lib/menu/menu.spec.ts
index 9bb994c25b5b..87c25446cbee 100644
--- a/src/lib/menu/menu.spec.ts
+++ b/src/lib/menu/menu.spec.ts
@@ -517,6 +517,39 @@ describe('MatMenu', () => {
}).toThrowError(/must pass in an mat-menu instance/);
});
+ it('should be able to swap out a menu after the first time it is opened', fakeAsync(() => {
+ const fixture = createComponent(DynamicPanelMenu);
+ fixture.detectChanges();
+ expect(overlayContainerElement.textContent).toBe('');
+
+ fixture.componentInstance.trigger.openMenu();
+ fixture.detectChanges();
+
+ expect(overlayContainerElement.textContent).toContain('One');
+ expect(overlayContainerElement.textContent).not.toContain('Two');
+
+ fixture.componentInstance.trigger.closeMenu();
+ fixture.detectChanges();
+ tick(500);
+ fixture.detectChanges();
+
+ expect(overlayContainerElement.textContent).toBe('');
+
+ fixture.componentInstance.trigger.menu = fixture.componentInstance.secondMenu;
+ fixture.componentInstance.trigger.openMenu();
+ fixture.detectChanges();
+
+ expect(overlayContainerElement.textContent).not.toContain('One');
+ expect(overlayContainerElement.textContent).toContain('Two');
+
+ fixture.componentInstance.trigger.closeMenu();
+ fixture.detectChanges();
+ tick(500);
+ fixture.detectChanges();
+
+ expect(overlayContainerElement.textContent).toBe('');
+ }));
+
describe('lazy rendering', () => {
it('should be able to render the menu content lazily', fakeAsync(() => {
const fixture = createComponent(SimpleLazyMenu);
@@ -2048,3 +2081,22 @@ class LazyMenuWithContext {
@ViewChild('triggerTwo') triggerTwo: MatMenuTrigger;
}
+
+
+@Component({
+ template: `
+
+
+
+
+
+
+
+
+ `
+})
+class DynamicPanelMenu {
+ @ViewChild(MatMenuTrigger) trigger: MatMenuTrigger;
+ @ViewChild('one') firstMenu: MatMenu;
+ @ViewChild('two') secondMenu: MatMenu;
+}