Skip to content

Commit

Permalink
feat(menu): support lazy rendering and passing in context data
Browse files Browse the repository at this point in the history
* Introduces the `matMenuContent` directive that allows for menu content to be rendered lazily.
* Adds the `matMenuTriggerData` input to the `MatMenuTrigger` that allows for contextual data to be passed in to the lazily-rendered menu panel. This allows for the menu instance to be re-used between triggers.

Fixes #9251.
  • Loading branch information
crisbeto committed Jan 8, 2018
1 parent af44b9d commit 5e636f9
Show file tree
Hide file tree
Showing 9 changed files with 286 additions and 28 deletions.
11 changes: 6 additions & 5 deletions src/cdk/portal/dom-portal-outlet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ import {BasePortalOutlet, ComponentPortal, TemplatePortal} from './portal';
*/
export class DomPortalOutlet extends BasePortalOutlet {
constructor(
private _hostDomElement: Element,
/** Element into which the content is projected. */
public hostDomElement: Element,
private _componentFactoryResolver: ComponentFactoryResolver,
private _appRef: ApplicationRef,
private _defaultInjector: Injector) {
Expand Down Expand Up @@ -59,7 +60,7 @@ export class DomPortalOutlet extends BasePortalOutlet {
}
// At this point the component has been instantiated, so we move it to the location in the DOM
// where we want it to be rendered.
this._hostDomElement.appendChild(this._getComponentRootNode(componentRef));
this.hostDomElement.appendChild(this._getComponentRootNode(componentRef));

return componentRef;
}
Expand All @@ -78,7 +79,7 @@ export class DomPortalOutlet extends BasePortalOutlet {
// But for the DomPortalOutlet the view can be added everywhere in the DOM
// (e.g Overlay Container) To move the view to the specified host element. We just
// re-append the existing root nodes.
viewRef.rootNodes.forEach(rootNode => this._hostDomElement.appendChild(rootNode));
viewRef.rootNodes.forEach(rootNode => this.hostDomElement.appendChild(rootNode));

this.setDisposeFn((() => {
let index = viewContainer.indexOf(viewRef);
Expand All @@ -96,8 +97,8 @@ export class DomPortalOutlet extends BasePortalOutlet {
*/
dispose(): void {
super.dispose();
if (this._hostDomElement.parentNode != null) {
this._hostDomElement.parentNode.removeChild(this._hostDomElement);
if (this.hostDomElement.parentNode != null) {
this.hostDomElement.parentNode.removeChild(this.hostDomElement);
}
}

Expand Down
68 changes: 68 additions & 0 deletions src/lib/menu/menu-content.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {
Directive,
TemplateRef,
ComponentFactoryResolver,
ApplicationRef,
Injector,
ViewContainerRef,
Inject,
OnDestroy,
} from '@angular/core';
import {TemplatePortal, DomPortalOutlet} from '@angular/cdk/portal';
import {DOCUMENT} from '@angular/common';

/**
* Menu content that will be rendered lazily once the menu is opened.
*/
@Directive({
selector: 'ng-template[matMenuContent]'
})
export class MatMenuContent implements OnDestroy {
private _portal: TemplatePortal<any>;
private _outlet: DomPortalOutlet;

constructor(
private _template: TemplateRef<any>,
private _componentFactoryResolver: ComponentFactoryResolver,
private _appRef: ApplicationRef,
private _injector: Injector,
private _viewContainerRef: ViewContainerRef,
@Inject(DOCUMENT) private _document: any) {}

/**
* Attaches the content with a particular context.
* @docs-private
*/
attach(context: any = {}) {
if (!this._portal) {
this._portal = new TemplatePortal(this._template, this._viewContainerRef);
} else if (this._portal.isAttached) {
this._portal.detach();
}

if (!this._outlet) {
this._outlet = new DomPortalOutlet(this._document.createElement('div'),
this._componentFactoryResolver, this._appRef, this._injector);
}

const element: HTMLElement = this._template.elementRef.nativeElement;

// Since the menu might be attached to a different DOM node, we have to re-insert it every time.
element.parentNode!.insertBefore(this._outlet.hostDomElement, element);
this._portal.attach(this._outlet, context);
}

ngOnDestroy() {
if (this._outlet) {
this._outlet.dispose();
}
}
}
17 changes: 16 additions & 1 deletion src/lib/menu/menu-directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
AfterContentInit,
ChangeDetectionStrategy,
Component,
ContentChild,
ContentChildren,
ElementRef,
EventEmitter,
Expand All @@ -38,6 +39,7 @@ import {matMenuAnimations} from './menu-animations';
import {throwMatMenuInvalidPositionX, throwMatMenuInvalidPositionY} from './menu-errors';
import {MatMenuItem} from './menu-item';
import {MatMenuPanel} from './menu-panel';
import {MatMenuContent} from './menu-content';
import {MenuPositionX, MenuPositionY} from './menu-positions';
import {coerceBooleanProperty} from '@angular/cdk/coercion';

Expand Down Expand Up @@ -128,6 +130,12 @@ export class MatMenu implements AfterContentInit, MatMenuPanel, OnDestroy {
/** List of the items inside of a menu. */
@ContentChildren(MatMenuItem) items: QueryList<MatMenuItem>;

/**
* Menu content that will be rendered lazily.
* @docs-private
*/
@ContentChild(MatMenuContent) lazyContent: MatMenuContent;

/** Whether the menu should overlap its trigger. */
@Input()
set overlapTrigger(value: boolean) {
Expand Down Expand Up @@ -232,7 +240,14 @@ export class MatMenu implements AfterContentInit, MatMenuPanel, OnDestroy {
* to focus the first item when the menu is opened by the ENTER key.
*/
focusFirstItem() {
this._keyManager.setFirstItemActive();
// When the content is rendered lazily, it takes a bit before the items are inside the DOM.
if (this.lazyContent) {
this._ngZone.onStable.asObservable()
.pipe(take(1))
.subscribe(() => this._keyManager.setFirstItemActive());
} else {
this._keyManager.setFirstItemActive();
}
}

/**
Expand Down
7 changes: 5 additions & 2 deletions src/lib/menu/menu-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,24 @@ import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {MatCommonModule} from '@angular/material/core';
import {OverlayModule} from '@angular/cdk/overlay';
import {PortalModule} from '@angular/cdk/portal';
import {MatMenu, MAT_MENU_DEFAULT_OPTIONS} from './menu-directive';
import {MatMenuItem} from './menu-item';
import {MatMenuTrigger, MAT_MENU_SCROLL_STRATEGY_PROVIDER} from './menu-trigger';
import {MatRippleModule} from '@angular/material/core';
import {MatMenuContent} from './menu-content';


@NgModule({
imports: [
PortalModule,
OverlayModule,
CommonModule,
MatRippleModule,
MatCommonModule,
],
exports: [MatMenu, MatMenuItem, MatMenuTrigger, MatCommonModule],
declarations: [MatMenu, MatMenuItem, MatMenuTrigger],
exports: [MatMenu, MatMenuItem, MatMenuTrigger, MatMenuContent, MatCommonModule],
declarations: [MatMenu, MatMenuItem, MatMenuTrigger, MatMenuContent],
providers: [
MAT_MENU_SCROLL_STRATEGY_PROVIDER,
{
Expand Down
2 changes: 2 additions & 0 deletions src/lib/menu/menu-panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import {EventEmitter, TemplateRef} from '@angular/core';
import {MenuPositionX, MenuPositionY} from './menu-positions';
import {Direction} from '@angular/cdk/bidi';
import {MatMenuContent} from './menu-content';

/**
* Interface for a custom menu panel that can be used with `matMenuTriggerFor`.
Expand All @@ -26,4 +27,5 @@ export interface MatMenuPanel {
resetActiveItem: () => void;
setPositionClasses: (x: MenuPositionX, y: MenuPositionY) => void;
setElevation?(depth: number): void;
lazyContent?: MatMenuContent;
}
24 changes: 17 additions & 7 deletions src/lib/menu/menu-trigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
/** References the menu instance that the trigger is associated with. */
@Input('matMenuTriggerFor') menu: MatMenuPanel;

/** Data to be passed along to any lazily-rendered content. */
@Input('matMenuTriggerData') menuData: any;

/** Event emitted when the associated menu is opened. */
@Output() menuOpened = new EventEmitter<void>();

Expand Down Expand Up @@ -191,14 +194,21 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {

/** Opens the menu. */
openMenu(): void {
if (!this._menuOpen) {
this._createOverlay().attach(this._portal);
this._closeSubscription = this._menuClosingActions().subscribe(() => this.closeMenu());
this._initMenu();
if (this._menuOpen) {
return;
}

if (this.menu instanceof MatMenu) {
this.menu._startAnimation();
}
this._createOverlay().attach(this._portal);

if (this.menu.lazyContent) {
this.menu.lazyContent.attach(this.menuData);
}

this._closeSubscription = this._menuClosingActions().subscribe(() => this.closeMenu());
this._initMenu();

if (this.menu instanceof MatMenu) {
this.menu._startAnimation();
}
}

Expand Down
62 changes: 51 additions & 11 deletions src/lib/menu/menu.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ By itself, the `<mat-menu>` element does not render anything. The menu is attach
via application of the `matMenuTriggerFor` directive:
```html
<mat-menu #appMenu="matMenu">
<button mat-menu-item> Settings </button>
<button mat-menu-item> Help </button>
<button mat-menu-item>Settings</button>
<button mat-menu-item>Help</button>
</mat-menu>

<button mat-icon-button [matMenuTriggerFor]="appMenu">
<mat-icon>more_vert</mat-icon>
<mat-icon>more_vert</mat-icon>
</button>
```

Expand All @@ -36,16 +36,16 @@ Menus support displaying `mat-icon` elements before the menu item text.
```html
<mat-menu #menu="matMenu">
<button mat-menu-item>
<mat-icon> dialpad </mat-icon>
<span> Redial </span>
<mat-icon>dialpad</mat-icon>
<span>Redial</span>
</button>
<button mat-menu-item disabled>
<mat-icon> voicemail </mat-icon>
<span> Check voicemail </span>
<mat-icon>voicemail</mat-icon>
<span>Check voicemail</span>
</button>
<button mat-menu-item>
<mat-icon> notifications_off </mat-icon>
<span> Disable alerts </span>
<mat-icon>notifications_off</mat-icon>
<span>Disable alerts</span>
</button>
</mat-menu>
```
Expand All @@ -59,8 +59,8 @@ The position can be changed using the `xPosition` (`before | after`) and `yPosit

```html
<mat-menu #appMenu="matMenu" yPosition="above">
<button mat-menu-item> Settings </button>
<button mat-menu-item> Help </button>
<button mat-menu-item>Settings</button>
<button mat-menu-item>Help</button>
</mat-menu>

<button mat-icon-button [matMenuTriggerFor]="appMenu">
Expand Down Expand Up @@ -93,6 +93,46 @@ that should trigger the sub-menu:

<!-- example(nested-menu) -->

### Lazy rendering
By default, the menu content will be initialized even when the panel is closed. To defer
initialization until the menu is open, the content can be provided as an `ng-template`
with the `matMenuContent` attribute:

```html
<mat-menu #appMenu="matMenu">
<ng-template matMenuContent>
<button mat-menu-item>Settings</button>
<button mat-menu-item>Help</button>
</ng-template>
</mat-menu>

<button mat-icon-button [matMenuTriggerFor]="appMenu">
<mat-icon>more_vert</mat-icon>
</button>
```

### Passing in data to a menu
When using lazy rendering, additional context data can be passed to the menu panel via
the `matMenuTriggerData` input. This allows for a single menu instance to be rendered
with a different set of data, depending on the trigger that opened it:

```html
<mat-menu #appMenu="matMenu" let-user="user">
<ng-template matMenuContent>
<button mat-menu-item>Settings</button>
<button mat-menu-item>Log off {{name}}</button>
</ng-template>
</mat-menu>

<button mat-icon-button [matMenuTriggerFor]="appMenu" [matMenuTriggerData]="{name: 'Sally'}">
<mat-icon>more_vert</mat-icon>
</button>

<button mat-icon-button [matMenuTriggerFor]="appMenu" [matMenuTriggerData]="{name: 'Bob'}">
<mat-icon>more_vert</mat-icon>
</button>
```

### Keyboard interaction
- <kbd>DOWN_ARROW</kbd>: Focuses the next menu item
- <kbd>UP_ARROW</kbd>: Focuses previous menu item
Expand Down
Loading

0 comments on commit 5e636f9

Please sign in to comment.