-
Notifications
You must be signed in to change notification settings - Fork 6.8k
feat(menu): support lazy rendering and passing in context data #9271
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
/** | ||
* @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; | ||
|
||
// Because we support opening the same menu from different triggers (which in turn have their | ||
// own `OverlayRef` panel), we have to re-insert the host element every time, otherwise we | ||
// risk it staying attached to a pane that's no longer in the DOM. | ||
element.parentNode!.insertBefore(this._outlet.outletElement, element); | ||
this._portal.attach(this._outlet, context); | ||
} | ||
|
||
ngOnDestroy() { | ||
if (this._outlet) { | ||
this._outlet.dispose(); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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> | ||
``` | ||
|
||
|
@@ -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> | ||
``` | ||
|
@@ -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"> | ||
|
@@ -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"> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What do you think of avoiding the extra <ng-template matMenu>
...
</ng-template> The selector for the menu would change to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's what I was going for initially, but Angular throws an error when you have a component on an |
||
<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 | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would an end user need to ever interact with this class (i.e. should it be docs-private)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did it, because we support providing your own menu instance that matches the
MatMenuPanel
interface.