src/app/components/menu-tree/menu-tree.component.ts
Displays a menu overlay on smaller screens
selector | menu-tree |
styleUrls | ./menu-tree.component.scss |
templateUrl | ./menu-tree.component.html |
Properties |
Methods |
Inputs |
Accessors |
constructor(router: Router, scroller: ViewportScroller, overlay: Overlay)
|
||||||||||||
Creates instance of Router, ViewportScroller and Overlay
Parameters :
|
icon | |
Type : string
|
|
Default value : ''
|
|
Icon name for the menu button |
overlayClass | |
Type : string
|
|
Default value : ''
|
|
Custom class name for the overlay |
positions | |
Type : ConnectedPosition[]
|
|
Default value : []
|
|
Position details of the overlay |
treeClass | |
Type : string
|
|
Default value : ''
|
|
Default class name for the menu |
treeItems | |
Type : NavItems[]
|
|
Sets the menu items to the datasource |
externalWindow | ||||||
externalWindow(url: string)
|
||||||
Opens URL in an external window
Parameters :
Returns :
void
|
scrollAfterDetach |
scrollAfterDetach()
|
Scrolls to the id of the selected element
Returns :
void
|
scrollTo | ||||||
scrollTo(id: string)
|
||||||
Sets the scrollToId with the id of selected menu item
Parameters :
Returns :
void
|
dataSource |
Default value : new MatTreeNestedDataSource<NavItems>()
|
Data source for the menu tree |
hasChild |
Default value : () => {...}
|
Checks if current node has children |
isOpen |
Default value : false
|
Flag to check if menu is open |
scrollStrategy |
Default value : this.overlay.scrollStrategies.block()
|
Scroll strategy for the overlay |
Optional scrollToId |
Type : string
|
Id of the element on page to be scrolled to |
treeControl |
Default value : new NestedTreeControl<NavItems>((node) => node.children)
|
Tree Controller |
treeItems | ||||||
settreeItems(items: NavItems[])
|
||||||
Sets the menu items to the datasource
Parameters :
Returns :
void
|
import { ConnectedPosition, Overlay } from '@angular/cdk/overlay';
import { NestedTreeControl } from '@angular/cdk/tree';
import { ViewportScroller } from '@angular/common';
import { Component, Input } from '@angular/core';
import { MatTreeNestedDataSource } from '@angular/material/tree';
import { Router } from '@angular/router';
import { NavItems } from '../toolbar/nav-items';
/** Displays a menu overlay on smaller screens */
@Component({
selector: 'menu-tree',
templateUrl: './menu-tree.component.html',
styleUrls: ['./menu-tree.component.scss'],
})
export class MenuTreeComponent {
/** Sets the menu items to the datasource */
@Input() set treeItems(items: NavItems[]) {
this.dataSource.data = items;
}
/** Icon name for the menu button */
@Input() icon = '';
/** Custom class name for the overlay */
@Input() overlayClass = '';
/** Default class name for the menu */
@Input() treeClass = '';
/** Position details of the overlay */
@Input() positions: ConnectedPosition[] = [];
/** Flag to check if menu is open */
isOpen = false;
/** Id of the element on page to be scrolled to */
scrollToId?: string;
/** Tree Controller */
treeControl = new NestedTreeControl<NavItems>((node) => node.children);
/** Data source for the menu tree */
dataSource = new MatTreeNestedDataSource<NavItems>();
/** Scroll strategy for the overlay */
scrollStrategy = this.overlay.scrollStrategies.block();
/** Creates instance of Router, ViewportScroller and Overlay */
constructor(
private readonly router: Router,
private readonly scroller: ViewportScroller,
private readonly overlay: Overlay,
) {}
/** Checks if current node has children */
hasChild = (_: number, node: NavItems) => !!node.children && node.children.length > 0;
/** Opens URL in an external window */
externalWindow(url: string): void {
window.open(url, '_blank');
}
/** Sets the scrollToId with the id of selected menu item */
scrollTo(id: string): void {
this.router.navigate([], { fragment: id });
this.scrollToId = id;
}
/** Scrolls to the id of the selected element */
scrollAfterDetach(): void {
if (this.scrollToId) {
this.scroller.scrollToAnchor(this.scrollToId);
this.scrollToId = undefined;
}
}
}
<button
mat-icon-button
(click)="this.isOpen = !this.isOpen"
cdkOverlayOrigin
#trigger="cdkOverlayOrigin"
class="menu-tree-button"
[disableRipple]="true"
>
<mat-icon *ngIf="icon; else default" class="menu-icon">{{ icon }}</mat-icon>
<ng-template #default>
<mat-icon>{{ isOpen ? 'menu' : 'menu' }}</mat-icon>
</ng-template>
</button>
<ng-template
cdkConnectedOverlay
[cdkConnectedOverlayOrigin]="trigger"
[cdkConnectedOverlayOpen]="isOpen"
[cdkConnectedOverlayHasBackdrop]="true"
[cdkConnectedOverlayBackdropClass]="$any(['cdk-overlay-dark-backdrop', 'menu-tree-backdrop'])"
(detach)="isOpen = false; scrollAfterDetach()"
[cdkConnectedOverlayPanelClass]="['menu-tree-panel', overlayClass]"
[cdkConnectedOverlayPositions]="positions"
[cdkConnectedOverlayScrollStrategy]="scrollStrategy"
(overlayOutsideClick)="!trigger.elementRef.nativeElement.contains($event.target) && (isOpen = false)"
>
<mat-tree cdkScrollable [dataSource]="dataSource" [treeControl]="treeControl" [class]="treeClass">
<mat-tree-node *matTreeNodeDef="let node" matTreeNodeToggle (click)="this.isOpen = !this.isOpen">
<div *ngIf="node.route" [routerLink]="[node.route]" class="menu-name">
{{ node.menuName }}
</div>
<mat-divider *ngIf="node.divider" class="menu-tree-divider"></mat-divider>
<div (click)="externalWindow(node.url!)" *ngIf="node.url" class="menu-name">
{{ node.menuName }}
</div>
<div *ngIf="node.id" (click)="scrollTo(node.id)" class="table-of-contents">
{{ node.menuName }}
</div>
<mat-divider *ngIf="node.id"></mat-divider>
</mat-tree-node>
<mat-nested-tree-node *matTreeNodeDef="let node; when: hasChild">
<div class="mat-parent-node" matTreeNodeToggle [attr.aria-label]="'Toggle ' + node.menuName">
{{ node.menuName }}
<button mat-icon-button>
<mat-icon [class.inverse]="treeControl.isExpanded(node)">arrow_drop_down</mat-icon>
</button>
</div>
<div [class.example-tree-invisible]="!treeControl.isExpanded(node)" role="group">
<ng-container matTreeNodeOutlet></ng-container>
</div>
</mat-nested-tree-node>
</mat-tree>
</ng-template>
./menu-tree.component.scss
:host {
--mat-icon-color: white;
.menu-tree-button {
background-color: #444c65;
z-index: 2;
}
}
::ng-deep {
.example-tree-invisible {
display: none;
}
.mat-tree {
width: 26rem;
border: 1px solid #e0e0e0;
border-radius: 0px 0px 8px 8px;
height: auto;
overflow-y: auto;
}
.inverse {
transform: rotate(180deg);
display: inline-block;
}
.menu-tree-backdrop {
margin-top: 4rem;
}
}
.mat-parent-node {
display: flex;
align-items: center;
}
.mat-tree-node,
.mat-parent-node {
font-size: 1rem;
color: #444c65;
font-weight: 300;
line-height: 1.5rem;
}
.mat-tree-node {
flex-direction: column;
align-items: flex-start;
justify-content: center;
}
.mat-nested-tree-node:not([aria-expanded='true']):hover,
.mat-tree > .mat-tree-node:hover {
background: rgba(0, 0, 0, 0.04);
}
.mat-tree-node {
z-index: 1;
}
.mat-tree-node[aria-level='2']:hover::after {
position: absolute;
content: '';
inset: 0 -2rem;
z-index: -1;
background: rgba(0, 0, 0, 0.04);
}