Igor Simic
5 years ago

Greedy navigation using Angular 7 and Material Design


Greedy navigation or Priority navigation or prio navigation is a handy way to display responsive navigation to your user. Bit how to build it using Angular Material Design? Well in this example you build your own greedy nav component. Let's take a look.

So the basic idea of prio nav or greedy nav is to calculate how much space complete navigation elements is taking, and then calculate how much space we currently have. And if the total needed space for all navigations elements exceeds available space then move elements from navigation to drop-down navigation until we have enough space again. 

Completed working example of greedy navigation / priority navigation by using Material Design with Angular 7 - you can find on stackblitz: https://stackblitz.com/edit/angular-udcj4c

So first thing first, import Angular Material and flex layout to your app.module.ts file:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import {FlexLayoutModule} from '@angular/flex-layout';
import { AppComponent } from './app.component';
import { MainNavComponent } from './Components/main-nav/main-nav.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

import {
 MatToolbarModule,
 MatSidenavModule,
 MatCardModule,
 MatMenuModule,
 MatButtonModule
} from '@angular/material';


@NgModule({
  imports: [ 
 BrowserModule, 
 FormsModule,
 MatButtonModule,
 MatToolbarModule,
 MatSidenavModule,
 MatCardModule,
 MatMenuModule,
 MatButtonModule,
 BrowserAnimationsModule ],
    declarations: [ AppComponent,MainNavComponent ],
    providers: [MainNavComponent],
    bootstrap: [AppComponent]
})
export class AppModule { }
Ok now we will import main navigation component into app.component.html:
<div fxFlex fxLayout="column" fxFlex="100">
 
 <app-main-nav fxFlex fxLayout="column" fxFlex="100" class="mat-elevation-z6"></app-main-nav>
 
</div>
Since main navigation component is not created let's make it. First let's set up HTML part. Inside of app folder create folder Components and inside of components folder create main-nav folder. In this folder create these 3 files:
main-nav.component.html
main-nav.component.scss
main-nav.component.ts


Lets put greedy navigation html structure. Put this HTML code in main-nav.component.htm
<div fxFlex fxLayout="column" fxFlex="100">

 <mat-sidenav-container class="sidenav-container" style="height:100%;">
 <mat-toolbar class="toolbar-top" color="primary" id="header-toolbar">

 <div id="toolbar-left-section" style="display: flex;flex: 0 0;"> 
      ☀
 </div>
 <span fxFlex style="width:50px;"></span>


 <div id="prio-nav" style="display: flex;flex: 1;justify-content: center;">


 <button *ngFor="let menu of mainMenu; let i = index"
 type="button"
 aria-label="Toggle sidenav" mat-button
 >
          {{menu.title}}
 </button>

 </div>

 <div id="drop-down-items" *ngIf="showDropDownPrio">

 <button mat-button [matMenuTriggerFor]="menu" [matMenuTriggerData]="hiddenPrioMenu">
          ▾
 </button> 

 <mat-menu #menu="matMenu" [innerHtml]="hiddenMenuList" id="hidden-prio-nav">
 <ng-template matMenuContent>
 <button *ngFor="let menu of hiddenPrioMenu; let i = index"
 type="button"
 aria-label="Toggle sidenav" mat-button
 >
              {{menu.title}}
 </button>
 </ng-template>

 </mat-menu>
 </div>

 <span fxFlex style="width:50px;"></span>
 <div id="toolbar-right-section" style="display: flex; flex: 0 0; justify-content: flex-end;">
      ☀
 </div>

 </mat-toolbar>

 </mat-sidenav-container>
</div>
As you can see our parent navigation container, with id "header-toolbar", contains 3 elements: toolbar-left-section, prio-nav, toolbar-right-section. We will calculate needed space for all of these 3 elements, then calculate how much space is needed for header-toolbar and if prio-nav  + side elements are exceeding header toolbar width then move some elements from prio nav to new drop down list and display that drop down list. 

To achieve this goal, we will take widths of all elements, create array of widths breaking points based on prio-nav each elements width. Then on resize, or on page load, we will make needed calculations.

So in our main-nav.component.ts we will first add object which will hold our navigation items and set global vars:
mainMenu = [
 {title:'Home'},
 {title:'Sport'},
 {title:'News'},
 {title:'Politics'},
 {title:'Mixed Martial Arts'}
 ];

hiddenPrioMenu = [];
showDropDownPrio = false;
breakWidths = [];
next, we will use Angular method ngAfterViewInit to get the needed witdhs of prio-nav elements:
ngAfterViewInit(){

 // get prio nav elemenz
 var prioNav = document.getElementById('prio-nav');

 var totalSpace = 0;
 var breakWidths = [];
 var numOfItems = 0;

 // loop over all elements and set sum of widths for each prio nav element
 for (var i = 0; i < prioNav.children.length; i++) {

      totalSpace += prioNav.children[i].clientWidth;
 this.breakWidths.push(totalSpace);

      numOfItems += 1;

 }

 // call calculation method
 this.checkCalculcation();
 }
And also on resize call checkCalcucation method:
onResize(event) {

 // call calculation method
 this.checkCalculcation();

 }
NOTE: to call this onReize method we need to include window resize event to our component:
@Component({
  selector: 'app-main-nav',
  templateUrl: './main-nav.component.html',
  styleUrls: ['./main-nav.component.scss'],

  // call onResize() method when browser window is resized
  host: {
 '(window:resize)': 'onResize($event)'
 }
})
And finally let's create our main calcualtion method:
checkCalculcation(){

 // get current space of parent element of prio-nav
 var availableSpace = document.getElementById('header-toolbar').offsetWidth;

 // get needed space for all elements in prio-nav parent element + last width of visible elements from breakwidth array
 var totalNeededSpace =
 (document.getElementById('toolbar-right-section').offsetWidth)+
 (document.getElementById('toolbar-left-section').offsetWidth) +
 this.breakWidths[this.mainMenu.length - 1]+
 200;


 // if we need more space than we have - hide last element
 if(totalNeededSpace > availableSpace){

 // push last elemen from mainMenu to hiddenPrioMenu
 this.hiddenPrioMenu.push(this.mainMenu[this.mainMenu.length-1]);

 // remove that last element from mainMenu
 this.mainMenu = this.mainMenu.filter(item => item !== this.mainMenu[this.mainMenu.length-1]);

 // show hidden prio menu
 this.showDropDownPrio = true;

 // apply changes
 this.cdr.detectChanges();

 // call this method to recalculate for rest of nav elements
 this.checkCalculcation();

 }else{

 // if we have elements in hidden prio menu

 if(this.hiddenPrioMenu.length>0){

 // push last element from hidden menu to main menu
 this.mainMenu.push(this.hiddenPrioMenu[this.hiddenPrioMenu.length-1]);

 // remove element from hidden menu
 this.hiddenPrioMenu = this.hiddenPrioMenu.filter(item => item !== this.hiddenPrioMenu[this.hiddenPrioMenu.length-1]);

 // if we have or not elements in hiddenPrioMenu hide it or show it
 if(this.hiddenPrioMenu.length>0){

 this.showDropDownPrio = true;

 }else{

 this.showDropDownPrio = false;

 }

 }

 // apply changes
 this.cdr.detectChanges();

 }

 }
Full working example you can find on https://stackblitz.com/edit/angular-udcj4c