I have the following data structure for items in my sidemenu, in an Angular app based on a paid-for web site theme. The data structure is my own, and the menu is derived fro
If your intention is to draw an menu with indefinite level of sub items, probably a good implementation is to make a directive.
With a directive you will be able to assume more control over your menu.
I created a basic exepmle with full recursion, on with you can see an easy implementation of more than one menu on the same page and more than 3 levels on one of the menus, see this plunker.
Code:
.directive('myMenu', ['$parse', function($parse) {
return {
restrict: 'A',
scope: true,
template:
'<li ng-repeat="item in List" ' +
'ng-class="{\'start\': item.isStart, \'nav-item\': item.isNavItem}">' +
'<a href="{{item.href}}" ng-class="{\'nav-link nav-toggle\': item.subItems && item.subItems.length > 0}">'+
'<span class="title"> {{item.text}}</span> ' +
'</a>'+
'<ul my-menu="item.subItems" class="sub-menu"> </ul>' +
'</li>',
link: function(scope,element,attrs){
//this List will be scope invariant so if you do multiple directive
//menus all of them wil now what list to use
scope.List = $parse(attrs.myMenu)(scope);
}
}
}])
Markup:
<ul class="page-sidebar-menu"
data-keep-expanded="false"
data-auto-scroll="true"
data-slide-speed="200"
ng-class="{'page-sidebar-menu-closed': settings.layout.pageSidebarClosed}"
my-menu="menuItems">
</ul>
Edit
Some notes
When it comes to make the decision about ng-include
(that i think is a fair enough solution) or .directive
, you have to ask yourself at least two questions first. Does my code fragment going to need some logic? If not you could as well just go for the ng-include.
But if you are going to put more logic in the fragment making it customizable, make changes to the element or attrs (DOM) manipulation, you should go for directive.
As well one point that make me fell more confortable with the directive is reusability of the code you write, since in my example you can give controll more and make a more generic one i assume you should go for that if your project is big and needs to grow. So the second question does my code going to be reusable?
A reminder for a clean directive is that instead of template
you could use the templateUrl
, and you can give a file to feed the html code that is currently in the template
.
If you are using 1.5+ you can now choose to use .component
. Component is a wrapper arroud .direcitve
that has a lot less boilerplate code. Here you can see the diference.
Directive Component
bindings No Yes (binds to controller)
bindToController Yes (default: false) No (use bindings instead)
compile function Yes No
controller Yes Yes (default function() {})
controllerAs Yes (default: false) Yes (default: $ctrl)
link functions Yes No
multiElement Yes No
priority Yes No
require Yes Yes
restrict Yes No (restricted to elements only)
scope Yes (default: false) No (scope is always isolate)
template Yes Yes, injectable
templateNamespace Yes No
templateUrl Yes Yes, injectable
terminal Yes No
transclude Yes (default: false) Yes (default: false)
Source angular guide for component
Edit
As sugested by Mathew Berg if you want not to include the ul element if the list of subItems is empty you can change the ul to this something like this <ul ng-if="item.subItems.length>0" my-menu="item.subItems" class="sub-menu"> </ul>
After reviewing these options I found this article very clean/helpful for an ng-include approach that handles model changes well: http://benfoster.io/blog/angularjs-recursive-templates
In summary:
<script type="text/ng-template" id="categoryTree">
{{ category.title }}
<ul ng-if="category.categories">
<li ng-repeat="category in category.categories" ng-include="'categoryTree'">
</li>
</ul>
</script>
then
<ul>
<li ng-repeat="category in categories" ng-include="'categoryTree'"></li>
</ul>
I am sure this exactly you are looking for -
You can achieve unlimited recursion by ng-repeat
<script type="text/ng-template" id="tree_item_renderer.html">
{{data.name}}
<button ng-click="add(data)">Add node</button>
<button ng-click="delete(data)" ng-show="data.nodes.length > 0">Delete nodes</button>
<ul>
<li ng-repeat="data in data.nodes" ng-include="'tree_item_renderer.html'"></li>
</ul>
angular.module("myApp", []).
controller("TreeController", ['$scope', function($scope) {
$scope.delete = function(data) {
data.nodes = [];
};
$scope.add = function(data) {
var post = data.nodes.length + 1;
var newName = data.name + '-' + post;
data.nodes.push({name: newName,nodes: []});
};
$scope.tree = [{name: "Node", nodes: []}];
}]);
Here is jsfiddle
Recursion can be very tricky. As things will get out of hand depending on how deep your tree is. Here is my suggestion:
.directive('menuItem', function($compile){
return {
restrict: 'A',
scope: {
menuItem: '=menuItem'
},
templateUrl: 'menuItem.html',
replace: true,
link: function(scope, element){
var watcher = scope.$watch('menuItem.subItems', function(){
if(scope.menuItem.subItems && scope.menuItem.subItems.length){
var subMenuItems = angular.element('<ul><li ng-repeat="subItem in menuItem.subItems" menu-item="subItem"></li></ul>')
$compile(subMenuItems)(scope);
element.append(subMenuItems);
watcher();
}
});
}
}
})
HTML:
<li>
<a ng-href="{{ menuItem.href }}">{{ menuItem.text }}</a>
</li>
This will make sure it doesn't create sub items repeatedly. You can see it working in a jsFiddle here: http://jsfiddle.net/gnk8vcrv/
If you find it's crashing your app because you have a massive amount of lists (I'd be interested to see) you can hide the parts of the if statement besides the watcher behind a $timeout.
Rahul Arora's answer is good, see this blog post for a similar example. The one change I would make is to use a component instead of ng-include. For an example see this Plunker:
app
.component('recursiveItem', {
bindings: {
item: '<'
},
controllerAs: 'vm',
templateUrl: 'newpartial.html'
});
In order to make recursion in Angular, I would love to use the basic feature of angularJS and i.e directive.
index.html
<rec-menu menu-items="menuItems"></rec-menu>
recMenu.html
<ul>
<li ng-repeat="item in $ctrl.menuItems">
<a ng-href="{{item.href}}">
<span ng-bind="item.text"></span>
</a>
<div ng-if="item.menuItems && item.menuItems.length">
<rec-menu menu-items="item.menuItems"></rec-menu>
</div>
</li>
</ul>
recMenu.html
angular.module('myApp').component('recMenu', {
templateUrl: 'recMenu.html',
bindings: {
menuItems: '<'
}
});
Here is working Plunker