a suite of UI Components for development of web apps
Advanced User Interface Controls and Components
Created: 02 September 2015
In this article, we will show you how to create a Menu in AngularJS dynamically, by using different JSON files. Each menu and submenu is created on demand, whenever a menu is clicked or hovered, a corresponding JSON file is loaded and menu is populated.
Similar: Menu Component for Angular 2
At first when page loads, the root menu is created, including only root menu items. For this purpose we are using a JSON data in following format:
[
{ "id": 1, "text": "File", "icon": "", "submenu": "file-menu.json", "hasChildren": true },
{ "id": 2, "text": "Edit", "icon": "", "hasChildren": true },
{ "id": 3, "text": "View", "icon": "", "hasChildren": true },
{ "id": 4, "text": "Help", "icon": "", "hasChildren": true }
]
Each menu item contains an id, text, icon, link to the corresponding JSON file and whether it will have submenus or not. The JSON file contains the data for the submenu. In our case, all root menus have submenus so the hasChildren field is set to true. This allows a dropdown button to appear in menu item space to notify the user that a submenu will pop up whenever root menu is clicked or hovered.
Note Submenus does not exist in this moment. They will be created on demand, when root menu is clicked.
When File menu item is clicked, the content of file-menu.json file is loaded and submenu is created dynamically. This file contains the following data:
[
{ "id": 11, "pid": 1, "text": "New", "icon": "icons-medium new-document", "submenu": "new-menu.json", "hasChildren": true },
{ "id": 12, "pid": 1, "text": "Open" },
{ "id": 13, "pid": 1, "text": "Save As...", "icon": "icons-medium save" },
{ "id": 14, "pid": 1, "text": "Save All" },
{ "id": 15, "pid": 1, "type": "separator" },
{ "id": 16, "pid": 1, "text": "Page Setup" },
{ "id": 17, "pid": 1, "text": "Print", "icon": "icons-medium print" },
{ "id": 18, "pid": 1, "type": "separator" },
{ "id": 19, "pid": 1, "text": "Exit" }
]
Loading process is accompanied with a change of the dropdown icon to a loading icon which notifies the user that some new data is loaded. After data load is complete, the submenu will popup. Behind the scene, this is accomplished by calling the following methods:
angular
.module("appModule", ["integralui"])
.controller("appCtrl", ["$scope", "IntegralUIMenuService", "$timeout", "$http", function($scope, $menuService, $timeout, $http){
// A unique identifier of teh Menu directive
$scope.menuName = "menuSample";
// Use the default menu icon if there is no specified icon
$scope.defaultIcon = 'icons-medium empty';
// An array object that will hold all menu items
$scope.data = [];
// Initially create the root menu without any submenus
var initTimer = $timeout(function(){
var dataSource = $http.get('json/main-menu.txt');
if (dataSource){
dataSource.success(function(data){
// Load Root Menu
$menuService.loadData($scope.menuName, data);
});
dataSource.error(function(data){
alert("AJAX failed to Load Data");
});
}
}, 1);
var loadMenu = function(item){
if (item && item.submenu){
var dataSource = $http.get('json/' + item.submenu);
if (dataSource){
dataSource.success(function(data){
// Change the dropdown icon to a loading icon
$menuService.beginLoad($scope.menuName, item);
var loadTimer = $timeout(function(){
// Load a Menu from JSON
$menuService.loadData($scope.menuName, data, item);
// Open the loaded sub menu
$menuService.openMenu($scope.menuName, item);
// Mark that submenu is loaded successfuly
item.isLoaded = true;
// Restores the dropdown icon to a default one
$menuService.endLoad($scope.menuName);
$timeout.cancel(loadTimer);
}, 1000);
});
dataSource.error(function(data){
alert("AJAX failed to Load Data");
});
}
}
}
// Handle itemClick event to open submenu for the clicked root menu
$scope.onItemClick = function(e){
// Load menu from JSON only if it's not loaded already
if (e.item && e.item.hasChildren && !e.item.isLoaded)
loadMenu(e.item);
}
});
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="css/integralui.css" />
<link rel="stylesheet" href="css/integralui.menu.css" />
<link rel="stylesheet" href="css/themes/theme-flat-blue.css" />
<script type="text/javascript" src="external/angular.min.js"></script>
<script type="text/javascript" src="js/angular.integralui.min.js"></script>
<script type="text/javascript" src="js/angular.integralui.menu.min.js"></script>
</head>
<body>
<div ng-app="appModule" ng-controller="appCtrl">
<iui-menu name="{{menuName}}" items="data" item-click="onItemClick(e)" item-hover="onItemHover(e)"></iui-menu>
</div>
</body>
</html>
.iui-menu-item-root-content
{
width: 50px;
}
.iui-menu-item-root-content, .iui-menu-item-content
{
margin: 0 7px;
}
.iui-menu-item
{
width: 125px;
}
The loadData method can accept data in flat or tree format, and will create a menu in whole or only a small part of it, like a specific submenu.
Note Submenus does not exist in this moment. They will be created on demand, when root menu is clicked.
When a specific submenu is loaded, we also set the isLoaded item field to true, so that next time a menu is opened, it doesn't require a reload. In this case, next time File menu is clicked, it will only open the previously loaded submenu.
In similar way, when other root menus are clicked a specific submenus are loaded, using the specifications of the clicked menu item.
In case when submenu contains its own submenu, instead of clicking the submenu item, we will open its submenu when mouse cursor hovers over menu item longer than 500ms. For this purpose, we need to handle the itemHover event, like this:
var hoverTimer = null;
var prevHoverItem = null;
$scope.onItemHover = function(e){
if (hoverTimer && e.item != prevHoverItem)
$timeout.cancel(hoverTimer);
// For menu items that are not root menus, we can load submenus when mouse cursor hovers over their space
if (e.item && e.item.pid && e.item.hasChildren && !e.item.isLoaded){
// Load a submenu after mouse cursor hovers longer than 500ms
hoverTimer = $timeout(function(){
if (hoverTimer)
loadMenu(e.item);
}, 500);
}
prevHoverItem = e.item;
}
We are using the $timeout AngularJS service to create a time span of 500ms, after which menu is loaded. In our example, the New submenu of the File menu contains a submenu located in separate JSON file:
[
{ "id": 111, "pid": 11, "text": "Project", "icon": "" },
{ "id": 112, "pid": 11, "text": "Window", "icon": "" }
]
In similar way as for root menu, when submenu is loaded, it doesn't require a reload. Next time the menu item is hovered, the already created submenu will appear. Of course, if you require a reload every time a menu is clicked or hovered, this can be easily handled in your application code.
Other menus and submenus are created and opened in similar way, as above demonstration shows. Root menus to open are clicked, and submenus that contain other submenus are opened while mouse cursor hovers over their space.
Of course you don't need to load specific menus on demand, you can load the entire menu with all its submenus at one time. For this purpose, only one JSON file is needed:
[
{ "id": 1, "text": "File", "icon": "" },
{ "id": 11, "pid": 1, "text": "New", "icon": "icons-medium new-document" },
{ "id": 111, "pid": 11, "text": "Project", "icon": "" },
{ "id": 112, "pid": 11, "text": "Window", "icon": "" },
{ "id": 12, "pid": 1, "text": "Open" },
{ "id": 13, "pid": 1, "text": "Save As...", "icon": "icons-medium save" },
{ "id": 14, "pid": 1, "text": "Save All" },
{ "id": 15, "pid": 1, "type": "separator" },
{ "id": 16, "pid": 1, "text": "Page Setup" },
{ "id": 17, "pid": 1, "text": "Print", "icon": "icons-medium print" },
{ "id": 18, "pid": 1, "type": "separator" },
{ "id": 19, "pid": 1, "text": "Exit" },
{ "id": 2, "text": "Edit", "icon": "" },
{ "id": 21, "pid": 2, "text": "Undo" },
{ "id": 22, "pid": 2, "text": "Redo" },
{ "id": 23, "pid": 2, "type": "separator" },
{ "id": 24, "pid": 2, "text": "Cut" },
{ "id": 25, "pid": 2, "text": "Copy", "icon": "icons-medium copy" },
{ "id": 26, "pid": 2, "text": "Paste" },
{ "id": 27, "pid": 2, "text": "Delete", "icon": "icons-medium delete-document" },
{ "id": 3, "text": "View", "icon": "" },
{ "id": 31, "pid": 3, "text": "Print Layout" },
{ "id": 32, "pid": 3, "text": "Zoom", "icon": "icons-medium zoom" },
{ "id": 321, "pid": 32, "text": "Zoom In", "icon": "icons-medium zoom-in" },
{ "id": 322, "pid": 32, "text": "Zoom Out", "icon": "icons-medium zoom-out" },
{ "id": 323, "pid": 32, "type": "separator" },
{ "id": 324, "pid": 32, "text": "Restore" },
{ "id": 33, "pid": 3, "type": "separator" },
{ "id": 34, "pid": 3, "text": "Full Screen" },
{ "id": 4, "text": "Help", "icon": "" },
{ "id": 41, "pid": 4, "text": "Search", "icon": "" },
{ "id": 42, "pid": 4, "text": "Documents", "icon": "" },
{ "id": 43, "pid": 4, "type": "separator", "icon": "" },
{ "id": 44, "pid": 4, "text": "About", "icon": "" }
]
Later if you decide to change the content of specific menu, you can do it dynamically by loading a different JSON file and apply its content only to a specific menu or submenu. This is done by using the loadData method and specify the parent menu item to which data will be applied (as it is mentioned previously in above sections of this article).