In this article, we build an Angular, Bootstrap3 dynamic navigational menu with nested sub-menus from a hierarchical JSON model that can contain an undefined tree depth. To build this solution, we can first take a simple approach and enhance it as we identify certain patterns in the solution.
Menu Component
import { Component, OnInit } from '@angular/core';
import { IMenu } from '../models/imenu.model';
@Component({
selector: 'app-recursive',
templateUrl: './recursive.component.html',
})
export class RecursiveComponent implements OnInit {
constructor() { }
public menus: Array<IMenu>;
ngOnInit() {
this.menus = [{
"id": 1,
"title": "Azure",
"class": "fa-cloud",
"url": "#",
"menu": [{
"id": 121,
"title": "Azure AD",
"class": "fa-cloud",
"url": "#",
"menu": [{
"id": 1210,
"title": "Settings",
"class": "fa-cloud",
"url": "#",
"menu": [{
"id": 1210,
"title": "Apps",
"class": "fa-cloud",
"url": "#"
}, {
"id": 1211,
"title": "Users",
"class": "fa-cloud",
"url": "#"
}]
}]
}]
}, {
"id": 2,
"title": "AWS",
"class": "fa-cloud",
"url": "#",
"menu": [{
"id": 21,
"title": "DMS",
"class": "fa-cloud",
"url": "#",
"menu": [{
"id": 211,
"title": "End Points",
"class": "fa-cloud",
"url": "#"
}, {
"id": 2112,
"title": "Replication",
"class": "fa-cloud",
"url": "#"
}]
}]
}];
}
}
Menu Interface
export interface IMenu {
id : number;
title : string;
class : string;
url : string;
menu? : IMenu[]
}
In our component, we declare our menu property which implements an array of IMenu interface which in turn also implements an optional (?) IMenu array property for the sub-menus. This is the model that enables us to load the strongly typed JSON model for our view. On the OnInit event, we set the menu property with our JSON data. This is the area where we would use a shared service that can load the data for our component.
Component View
<div class="row" >
<div class="col-sm-12">
<nav class="navbar">
<ul class="nav">
<li *ngFor="let menu of menus" class="dropdown-toggle" data-toggle="collapse" >
<a href="{{menu.url}}" title="{{menu.title}}" (click)="menu.selected = !menu.selected"
data-target="#nav{{menu.id}}">
<i class="fa fa-fw fa-2x {{menu.class}}"></i> <span class="nav-header-primary">{{menu.title}}</span>
<span *ngIf="menu.menu" class="fa fa-caret-down fa-2x pull-right"></span>
</a>
<ul class="nav collapse" style="margin-left:20px" *ngIf="menu.menu" id="nav{{menu.id}}" [ngClass]="{'in':menu.selected}">
<li *ngFor="let menu of menu.menu">
<a href="{{menu.url}}" title="{{menu.title}}" (click)="menu.selected = !menu.selected"
data-target="#nav{{menu.id}}">
<i class="fa fa-fw fa-2x {{menu.class}}"></i> <span class="nav-header-primary">{{menu.title}}</span>
<span *ngIf="menu.menu" class="fa fa-caret-down fa-2x pull-right"> </span>
</a>
</li>
</ul>
</li>
</ul>
</nav>
</div>
</div>
On our component view, we use an *ngFor directive to iterate the array of objects to build the menus and the collapsible behavior. For each sub-level, we add another UL tag with a structure similar to the parent menu. To expand the collapsed areas, we add the ngClass directive to set the Bootrap class that we need. We also use the (click) event to set the object property to know the current menu state.
The problem with the above solution is that the nested code can get really confusing and hard to maintain. The code however shows us some patterns that we can leverage. We can see that the area to build the menu structure is really similar, so perhaps we could create a template that can enable us to reuse that HTML section.
The problem with the above solution is that the nested code can get really confusing and hard to maintain. The code however shows us some patterns that we can leverage. We can see that the area to build the menu structure is really similar, so perhaps we could create a template that can enable us to reuse that HTML section.
ng-templates
Templates, as we already know, provides us with the ability to create HTML segments that can be injected into the DOM. This is what we can use to build our menu structure. If we take the common repeatable code and put it in a reusable template, we can make our solution look much simpler. Let’s take a look at that.
<ng-template #recursiveMenu let-menus>
<li *ngFor="let menu of menus" class="dropdown-toggle" data-toggle="collapse" >
<a href="{{menu.url}}" title="{{menu.title}}" (click)="menu.selected = !menu.selected"
data-target="#nav{{menu.id}}">
<i class="fa fa-fw fa-2x {{menu.class}}"></i> <span class="nav-header-primary">{{menu.title}}</span>
<span *ngIf="menu.menu" class="fa fa-caret-down fa-2x pull-right">
</span>
</a>
</li>
</ng-template>
Now that we have the template, our next question should be how do we use it? For that, we can use template with containers.
ng-container
A container allows us to load HTML templates with an implicit data context. Basically, we can load the named template, and as a parameter we can also pass a reference to the object that we want to use. As you can see on the code below, the container is used on the area where we want to inject the HTML on the DOM. As parameters, we give it the id of the template and the object reference.
<div class="row" >
<div class="col-sm-12">
<nav class="navbar">
<ul class="nav">
<ng-container *ngTemplateOutlet="recursiveMenu; context:{ $implicit: menus }">
</ng-container>
</ng-container>
</ul>
</nav>
</div>
</div>
How is this recursive?
By just calling our template one time, we can only show one level of the menus. How do we load the sub-menus with this template? This is where the power of recursive templates comes into play. We are already able to load our template. How about if we change our template, so that it can call itself for the other menu levels?
Template with container
Our template can have any HTML mark-up, so why not just add another container and pass the sub-menu references. This can recursively keep adding new levels.
<ng-template #recursiveMenu let-menus>
<li *ngFor="let menu of menus" class="dropdown-toggle" data-toggle="collapse" >
<a href="{{menu.url}}" title="{{menu.title}}" (click)="menu.selected = !menu.selected"
data-target="#nav{{menu.id}}">
<i class="fa fa-fw fa-2x {{menu.class}}"></i> <span class="nav-header-primary">{{menu.title}}</span>
<span *ngIf="menu.menu" class="fa fa-caret-down fa-2x pull-right">
</span>
</a>
<ul class="nav collapse" style="margin-left:20px" *ngIf="menu.menu" id="nav{{menu.id}}" [ngClass]="{'in':menu.selected}">
<ng-container *ngTemplateOutlet="recursiveMenu; context:{ $implicit: menu.menu }"></ng-container>
</ul>
</li>
</ng-template>
The nested UL (sub-menus) have the ng-container call and passes the sub-menu object reference. This is the area where the template is injected again on the DOM, and it recursively continues until there are no more valid references which is evaluate by the *ngIf directive.
We have now shown how with typescript we can model a hierarchical JSON structures. We also saw some areas of reusability on our components views and change them to re-usable templates that can be recursively called with containers.
0 comments :
Post a Comment
What do you think?