In this article, we build an AngularJS collapsible menu with nested sub-menus from a hierarchical JSON model that can contain an undefined tree depth like the one shown below:
JSON Model | Collapsible Menu |
|
|
Our JSON model has a nested structure in which a node can have other nodes associated to the menu property. This model represents navigation groups and sub-groups for an app which can enable us to build a hierarchical menu. A common approach to render this model is to build a nested view that iterates through the arrays to display the information. Let’s look a simple implementation here:
<ul class="nav" style="width:200px" data-ng-controller="app.ctrl.dev as ctrl"> <li data-ng-repeat="item in ctrl.menu"> <a href="{{item.url}}" class="dropdown-toggle" data-toggle="collapse" title="{{item.title}}" data-target="#{{item.id}}"> <i class="fa fa-fw fa-2x {{item.class}}"></i> <span class="nav-header-primary" data-ng-bind="item.title"></span> <span data-ng-if="item.menu" class="fa fa-caret-down fa-2x pull-right"></span> </a> <ul class="nav collapse" data-ng-if="item.menu" id="{{item.id}}"> <li data-ng-repeat="subitem in item.menu" class="collapsed active"> <a href="{{subitem.url}}" class="dropdown-toggle" data-toggle="collapse" data-target="#{{subitem.id}}" title="{{subitem.title}}"> <i class="fa {{subitem.class}} fa-fw fa-2x"></i> <span data-ng-bind="subitem.title"></span> <span data-ng-if="subitem.menu" class="fa fa-caret-down fa-2x pull-right"></span> </a> </li> </ul> </li> </ul>
|
If we look at our implementation, we can see that our view have the nested HTML mark-up rendering the same model properties on the same HTML document structure. This may not be a problem if the levels on the JSON model are just two levels deep, but the moment we need to support more levels, the view becomes unmanageable as we need to duplicate more mark-up as shown on the example below:
<ul class="nav" style="width:200px" data-ng-controller="app.ctrl.dev as ctrl"> <li data-ng-repeat="item in ctrl.menu"> <a href="{{item.url}}" class="dropdown-toggle" data-toggle="collapse" title="{{item.title}}" data-target="#{{item.id}}"> <i class="fa fa-fw fa-2x {{item.class}}"></i> <span class="nav-header-primary" data-ng-bind="item.title"></span> <span data-ng-if="item.menu" class="fa fa-caret-down fa-2x pull-right"></span> </a> <ul class="nav collapse" data-ng-if="item.menu" id="{{item.id}}"> <li data-ng-repeat="subitem in item.menu" class="collapsed active"> <a href="{{subitem.url}}" class="dropdown-toggle" data-toggle="collapse" data-target="#{{subitem.id}}" title="{{subitem.title}}"> <i class="fa {{subitem.class}} fa-fw fa-2x"></i> <span data-ng-bind="subitem.title"></span> <span data-ng-if="subitem.menu" class="fa fa-caret-down fa-2x pull-right"></span> </a> <ul class="nav collapse" data-ng-if="subitem.menu" id="{{subitem.id}}"> <li data-ng-repeat="item3 in subitem.menu"> <a href="{{item3.url}}" title="{{item3.title}}"> <i class="fa {{item3.class}} fa-fw fa-2x"></i> <span data-ng-bind="item3.title"></span> </a> </li> </ul> </li> </ul> </li> </ul>
|
In the example above, we can continue to nest more mark-up, but this eventually becomes unreadably, and it is still limited to the number of segments/levels that we implement on the view. If the model has a deeper level, our view does not account for that. We need to look for a dynamic way to handle these cases.
Recursive Views:
Luckily for us AngularJS let us include content into our app with the use of a directive. The ngInclude directive can be used to recursively include the HTML mark-up as a template as we iterate thru the different levels. Let’s modify our mark-up to demonstrate this:
Entry Point:
<nav class="navbar" style="width:200px"> <ul class="nav"> <li data-ng-repeat="item in ctrl.menu" class="collapse" ng-init="level=1" ng-include="'recursiveView'"> </li> </ul> </nav>
|
In our view, we first create the entry point to include a template into our content. We do this by adding the ng-include directive to load a template while we read the elements on the menu array using ngRepeat which is the step that allows us to create the item scope context that is used on the template.
The Recursive Template:
<script type="text/ng-template" id="recursiveView"> <a href="{{item.url}}" class="dropdown-toggle" data-toggle="collapse" title="{{item.title}}" data-target="#nav{{item.id}}"> <i class="fa fa-fw fa-2x {{item.class}}"></i> <span class="nav-header-primary" ng-bind="item.title"></span> <span ng-if="item.menu" class="fa fa-caret-down fa-2x pull-right"></span> </a> <ul class="nav collapse" style="margin-left:{{level}}0px" data-ng-if="item.menu" id="nav{{item.id}}"> <li ng-repeat="subitem in item.menu" data-ng-init="level=level+1;item=subitem" ng-include="'recursiveView'"> </li> </ul> </script>
|
Within the template itself, the item object properties are used to render the information. We also load the template again using the include directive while reading the item menu property. This enables us to iterate the nested model and inject the template as we navigate through the different levels recursively.
The solution supports n nested levels without duplicating the markup. A few key items to notice using this strategy are the following:
1 – Initialize the level scope variable: This enables to track the levels and add some padding by using an inline style markup. We do not want to set a class name here since we want to support multiple levels dynamically.
2- Item as a scope variable: Notice how at the start we use ngRepeat directive and set the item scope variable. On the nested level, we use the subitem scope variable to iterate the item.menu array. We need to use different scope variable names to prevent resetting the item scope context. However when we include the template again recursively, the subitem scope context is passed. Obviously, the view is expecting an item scope variable name instead of subitem. To overcome this, we use the ngInit directive to set the item scope context to the subitem array. At this point, we no longer need the previous context, and we can continue to render the nested levels.
Once we have accounted for the scope variable problem, our recursive view should work properly.
This can be seen in action on the following demo:
DEMO
As we can see, we now have support for multiple levels without having to duplicate all the HTML mark-up.
Originally published by ozkary.com