First I would like to talk about using MVC views (CSHTML files) for Angular views. In my option, I like powerful features that MVC views bring to the table. I can for example, use my models to generate input controls and validation messages on the server automatically. More on that subject in subsequent posts. I can easily enforce user rights. I can mush easier manage resources, such as labels, from both multi-lingual support and user customization perspective. Hence, I am using MVC Views instead of plain HTML views for this demo. Ok, done with aside comment.
Generating dynamic menus is a common tasks in business apps. I would like to illustrate how to do this in an Angular application. There are a few steps I am going to illustrate with code in this post.
- Menu items list will be generated via Web Api controller on the server.
- Menu will be presented as Bootstrap nav bar control
- Angular service will call Web Api to get the data.
- Data will be put into a controller and ng-repeat directive will build menu items inside nav bar control
- The same service will be used to generate Angular routes.
Let’s get started. Web Api controller is the easiest part, but first I need to define menu item class.
public class MenuItem
{
public string Path { get; set; }
public string Controller { get; set; }
public string TemplateUrl { get; set; }
public string Title { get; set; }
}
Path will hold Angular route path. Controller will hold Angular controller responsible for display that menu item’s view and binding to it. Template URL will hold partial URL to the MVC controller method that will return a partial view. Finally, Title will hold the actual menu text. When all said and done, menu will look as following.
For now, I am going to just hard-code the menu, but in your case you can just as easily get it from the database. My controller as a result is very simple.
using System.Web.Http;
using AngularAspNetMvc.Models.Common;
using AngularAspNetMvc.WebApi.Core;
using System.Collections.Generic;
namespace AngularAspNetMvc.WebApi.Common
{
public class HomeController : RelayController
{
[HttpGet]
public List<MenuItem> Menu()
{
return new List<MenuItem>
{
new MenuItem
{
Path = "/home",
Controller = "homeController",
TemplateUrl = "home/home",
Title = "Home"
},
new MenuItem
{
Path = "/contacttypes",
Controller = "app.contacts.contactsTypesController",
TemplateUrl = "contacttypes/index",
Title = "Contact Types"
},
new MenuItem
{
Path = "/contacts",
Controller = "app.contacts.contactsController",
TemplateUrl = "contacts/index",
Title = "Contacts"
}
};
}
}
}
Now I need to create the Angular service. I am using TypeScript for this demo, but you can use JavaScript just as well. Simply remove the typings code. Service lives in its own Angular module and its own TypeScript module. The service just relies on http service in Angular and a global module with URL to web api. I blogged about injecting global into Angular app here. So, service code is very simple, it just returns a promise with http call.
/// <reference path="../interfaces.ts" />
/// <reference path="../../../Scripts/typings/angularjs/angular.d.ts" />
module app.home.services {
export interface IMenuServiceProvider extends ng.IServiceProvider {
$get: () => IMenuService;
}
export interface IMenuService {
getMenu: () => ng.IHttpPromise<Array<interfaces.IMenuItem>>;
}
class MenuService implements IMenuService {
getMenu: () => ng.IHttpPromise<Array<interfaces.IMenuItem>>;
constructor(private $http: ng.IHttpService, private globals: interfaces.IGLobals) {
this.getMenu = function () {
return $http.get(globals.webApiBaseUrl + '/home/menu');
};
}
}
angular.module('app.home.services', [])
.factory('app.home.menuService', function () {
var injector = angular.injector(['ng', 'app.globalsModule']);
var $http: ng.IHttpService = injector.get('$http');
var globals: interfaces.IGLobals = injector.get('globalsService');
return new MenuService($http, globals);
});
}
Few interfaces on top are needed to call the service from config function of a module. The reason for this approach is that you cannot inject services into config function call. Hence, I have to manually get an instance of my service from my main app module’s confg call as follows.
/// <reference path="home/services/menuService.ts" />
/// <reference path="home/interfaces.ts" />
/// <reference path="../Scripts/typings/angularjs/angular.d.ts" />
module app {
class appUtils {
static createViewUrl(fragmnt: string, globals: interfaces.IGLobals) {
return globals.baseUrl + fragmnt + '?v=' + globals.version;
}
}
angular.module('app',
[
'app.globalsModule',
'app.home.controllers',
'app.contacts.contactsController',
'app.contacts.contactsTypesController',
'app.directives.Common',
'app.home.services'
])
.config(['$routeProvider', '$windowProvider', 'globalsServiceProvider', 'app.home.menuServiceProvider',
function ($routeProvider: ng.IRouteProvider, $window: ng.IWindowService, globalsServiceProvider: interfaces.IGlobalsProvider, menuServiceProvider: app.home.services.IMenuServiceProvider) {
var globals: interfaces.IGLobals = globalsServiceProvider.$get();
var menuService: app.home.services.IMenuService = menuServiceProvider.$get();
menuService.getMenu()
.success(function (data) {
angular.forEach(data, function (menu: interfaces.IMenuItem) {
$routeProvider.when(menu.Path, {
controller: menu.Controller,
templateUrl: appUtils.createViewUrl(menu.TemplateUrl, globals)
});
});
$routeProvider.otherwise({
redirectTo: '/home'
});
})
.error(function (error) {
$window.alert('Could not get the menu');
});
}]);
}
If you look at what I injecting is app.home.menuServiceProvider. I am getting this name by adding word Provider to my factory (service) name in menu service definition in code snipped before this one. This word is added by Angular when handling services (factories). Then, by calling $get function of this provider I actually get an instance of my service. Pretty convoluted, but I have to get around injection limitation. I am using the same technique to get an instance of global variables service. Error handling is just mocked, showing an alert dialog. I am subscribing to promise successful resolution function (.success) next. I am looping through menus returned by Web Api controller, adding routes to route provider. Now, my routes are all setup. Next step is UI.
There I am going to use an object (TypeScript class) to expose menus to the UI as well as handle click events. Here is what my home controller (Angular controller to be specific) looks like.
/// <reference path="../services/menuService.ts" />
/// <reference path="../../../Scripts/typings/angularjs/angular.d.ts" />
/// <reference path="../../core/controllers/appController.ts" />
module app.home.controllers {
interface homeScope extends ng.IScope {
navigate: (path: string) => void;
menus: Array<interfaces.IMenuItem>;
}
class homeController extends app.core.controllers.CoreController {
constructor(private $location: ng.ILocationService, private $scope: homeScope, private menuService: app.home.services.IMenuService) {
super();
$scope.navigate = function (path: string) {
$location.path(path);
};
$scope.menus = new Array<interfaces.IMenuItem>();
menuService.getMenu()
.success(function (data) {
angular.forEach(data, function (menu: interfaces.IMenuItem) {
$scope.menus.push(menu);
});
$scope.$apply();
})
.error(function () {
alert('error');
});
}
}
angular.module('app.home.controllers', ['app.home.services'])
.controller('homeController', ['$location', '$scope', 'app.home.menuService',
function ($location: ng.ILocationService, $scope: homeScope, menuService: app.home.services.IMenuService) {
return new homeController($location, $scope, menuService);
}]);
}
I am using injection in this controller to give it access to the same menu service and location service. I am also defining custom scope interface. TypeScript allows me to do this. In turn, it helps me keep my code clean and avoids typing errors. At the end, I am calling the same service, but here I am adding menu items to the collection of items, exposed to UI through scope via menus property. Also, I am defining a function that will handle click events on my menu items. It is called navigate. Now, what is the parameter to this function? Of course it will be Path property of my menu item. Let’s keep going. UI is next.
<body data-ng-controller="homeController">
<nav class="navbar navbar-default navbar-fixed-top" role="navigation">
<!-- Brand and toggle get grouped for better mobile display -->
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#menuBarDiv">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<div class="navbar-brand">Contacts</div>
</div>
<!-- Collect the nav links, forms, and other content for toggling -->
<div id="menuBarDiv" class="collapse navbar-collapse">
<ul class="nav navbar-nav" data-ng-cloak="">
<li data-ng-repeat="oneMenu in menus"><a data-no-click="" data-ng-click="navigate(oneMenu.Path)" data-ng-href="#">{{oneMenu.Title}}</a></li>
</ul>
</div><!-- /.navbar-collapse -->
</nav>
Let’s take a closer look at this HTML fragment, contains inside my layout CHTML view (_Laout.cshtml file). You see, I am using nav bar from Twitter Bootstrap. I am using ng-repeat directive to run through my menu items, creating an <li> element with an embedded anchor tag. I am attaching click (ng-click directive) and I am calling navigate function on my home controller. How do I know it is home controller? You can see its name in the <body> tag. Angular will use my controller and will bind HTML elements to its scope. This statement oversimplifies what is really going on, but at the high level it is quite correct. And of course inner text of my the anchor tag is the Title of corresponding menu item. I am using a custom directive noClick to keep anchor tag’s clicks from propagating beyond my ng-click handler. It is a common trick in Angular when using anchor tags. The code for this directive is very simple. You can find it in the downloaded project inside commonDirectives.ts. I am also using ng-cloak directive. This is very useful when using ng-repeat. This directive will keep raw <li> tag from flashing on the screen until the ng-repeat directive is resolved.
That is all there is to it. You can download entire project here.
[Update]: check out two more posts on the subject: http://dotnetspeak.com/2013/12/angular-dynamic-menu-and-initial-url and http://dotnetspeak.com/2013/12/angular-js-and-dynamic-menu-part-2
Generating dynamic menus is a common tasks in business apps. I would like to illustrate how to do this in an Angular application. There are a few steps I am going to illustrate with code in this post.
- Menu items list will be generated via Web Api controller on the server.
- Menu will be presented as Bootstrap nav bar control
- Angular service will call Web Api to get the data.
- Data will be put into a controller and ng-repeat directive will build menu items inside nav bar control
- The same service will be used to generate Angular routes.
Let’s get started. Web Api controller is the easiest part, but first I need to define menu item class.
public class MenuItem { public string Path { get; set; } public string Controller { get; set; } public string TemplateUrl { get; set; } public string Title { get; set; } }
Path will hold Angular route path. Controller will hold Angular controller responsible for display that menu item’s view and binding to it. Template URL will hold partial URL to the MVC controller method that will return a partial view. Finally, Title will hold the actual menu text. When all said and done, menu will look as following.
For now, I am going to just hard-code the menu, but in your case you can just as easily get it from the database. My controller as a result is very simple.
using System.Web.Http; using AngularAspNetMvc.Models.Common; using AngularAspNetMvc.WebApi.Core; using System.Collections.Generic; namespace AngularAspNetMvc.WebApi.Common { public class HomeController : RelayController { [HttpGet] public List<MenuItem> Menu() { return new List<MenuItem> { new MenuItem { Path = "/home", Controller = "homeController", TemplateUrl = "home/home", Title = "Home" }, new MenuItem { Path = "/contacttypes", Controller = "app.contacts.contactsTypesController", TemplateUrl = "contacttypes/index", Title = "Contact Types" }, new MenuItem { Path = "/contacts", Controller = "app.contacts.contactsController", TemplateUrl = "contacts/index", Title = "Contacts" } }; } } }
Now I need to create the Angular service. I am using TypeScript for this demo, but you can use JavaScript just as well. Simply remove the typings code. Service lives in its own Angular module and its own TypeScript module. The service just relies on http service in Angular and a global module with URL to web api. I blogged about injecting global into Angular app here. So, service code is very simple, it just returns a promise with http call.
/// <reference path="../interfaces.ts" /> /// <reference path="../../../Scripts/typings/angularjs/angular.d.ts" /> module app.home.services { export interface IMenuServiceProvider extends ng.IServiceProvider { $get: () => IMenuService; } export interface IMenuService { getMenu: () => ng.IHttpPromise<Array<interfaces.IMenuItem>>; } class MenuService implements IMenuService { getMenu: () => ng.IHttpPromise<Array<interfaces.IMenuItem>>; constructor(private $http: ng.IHttpService, private globals: interfaces.IGLobals) { this.getMenu = function () { return $http.get(globals.webApiBaseUrl + '/home/menu'); }; } } angular.module('app.home.services', []) .factory('app.home.menuService', function () { var injector = angular.injector(['ng', 'app.globalsModule']); var $http: ng.IHttpService = injector.get('$http'); var globals: interfaces.IGLobals = injector.get('globalsService'); return new MenuService($http, globals); }); }
Few interfaces on top are needed to call the service from config function of a module. The reason for this approach is that you cannot inject services into config function call. Hence, I have to manually get an instance of my service from my main app module’s confg call as follows.
/// <reference path="home/services/menuService.ts" /> /// <reference path="home/interfaces.ts" /> /// <reference path="../Scripts/typings/angularjs/angular.d.ts" /> module app { class appUtils { static createViewUrl(fragmnt: string, globals: interfaces.IGLobals) { return globals.baseUrl + fragmnt + '?v=' + globals.version; } } angular.module('app', [ 'app.globalsModule', 'app.home.controllers', 'app.contacts.contactsController', 'app.contacts.contactsTypesController', 'app.directives.Common', 'app.home.services' ]) .config(['$routeProvider', '$windowProvider', 'globalsServiceProvider', 'app.home.menuServiceProvider', function ($routeProvider: ng.IRouteProvider, $window: ng.IWindowService, globalsServiceProvider: interfaces.IGlobalsProvider, menuServiceProvider: app.home.services.IMenuServiceProvider) { var globals: interfaces.IGLobals = globalsServiceProvider.$get(); var menuService: app.home.services.IMenuService = menuServiceProvider.$get(); menuService.getMenu() .success(function (data) { angular.forEach(data, function (menu: interfaces.IMenuItem) { $routeProvider.when(menu.Path, { controller: menu.Controller, templateUrl: appUtils.createViewUrl(menu.TemplateUrl, globals) }); }); $routeProvider.otherwise({ redirectTo: '/home' }); }) .error(function (error) { $window.alert('Could not get the menu'); }); }]); }
If you look at what I injecting is app.home.menuServiceProvider. I am getting this name by adding word Provider to my factory (service) name in menu service definition in code snipped before this one. This word is added by Angular when handling services (factories). Then, by calling $get function of this provider I actually get an instance of my service. Pretty convoluted, but I have to get around injection limitation. I am using the same technique to get an instance of global variables service. Error handling is just mocked, showing an alert dialog. I am subscribing to promise successful resolution function (.success) next. I am looping through menus returned by Web Api controller, adding routes to route provider. Now, my routes are all setup. Next step is UI.
There I am going to use an object (TypeScript class) to expose menus to the UI as well as handle click events. Here is what my home controller (Angular controller to be specific) looks like.
/// <reference path="../services/menuService.ts" /> /// <reference path="../../../Scripts/typings/angularjs/angular.d.ts" /> /// <reference path="../../core/controllers/appController.ts" /> module app.home.controllers { interface homeScope extends ng.IScope { navigate: (path: string) => void; menus: Array<interfaces.IMenuItem>; } class homeController extends app.core.controllers.CoreController { constructor(private $location: ng.ILocationService, private $scope: homeScope, private menuService: app.home.services.IMenuService) { super(); $scope.navigate = function (path: string) { $location.path(path); }; $scope.menus = new Array<interfaces.IMenuItem>(); menuService.getMenu() .success(function (data) { angular.forEach(data, function (menu: interfaces.IMenuItem) { $scope.menus.push(menu); }); $scope.$apply(); }) .error(function () { alert('error'); }); } } angular.module('app.home.controllers', ['app.home.services']) .controller('homeController', ['$location', '$scope', 'app.home.menuService', function ($location: ng.ILocationService, $scope: homeScope, menuService: app.home.services.IMenuService) { return new homeController($location, $scope, menuService); }]); }
I am using injection in this controller to give it access to the same menu service and location service. I am also defining custom scope interface. TypeScript allows me to do this. In turn, it helps me keep my code clean and avoids typing errors. At the end, I am calling the same service, but here I am adding menu items to the collection of items, exposed to UI through scope via menus property. Also, I am defining a function that will handle click events on my menu items. It is called navigate. Now, what is the parameter to this function? Of course it will be Path property of my menu item. Let’s keep going. UI is next.
<body data-ng-controller="homeController"> <nav class="navbar navbar-default navbar-fixed-top" role="navigation"> <!-- Brand and toggle get grouped for better mobile display --> <div class="navbar-header"> <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#menuBarDiv"> <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> <div class="navbar-brand">Contacts</div> </div> <!-- Collect the nav links, forms, and other content for toggling --> <div id="menuBarDiv" class="collapse navbar-collapse"> <ul class="nav navbar-nav" data-ng-cloak=""> <li data-ng-repeat="oneMenu in menus"><a data-no-click="" data-ng-click="navigate(oneMenu.Path)" data-ng-href="#">{{oneMenu.Title}}</a></li> </ul> </div><!-- /.navbar-collapse --> </nav>
Let’s take a closer look at this HTML fragment, contains inside my layout CHTML view (_Laout.cshtml file). You see, I am using nav bar from Twitter Bootstrap. I am using ng-repeat directive to run through my menu items, creating an <li> element with an embedded anchor tag. I am attaching click (ng-click directive) and I am calling navigate function on my home controller. How do I know it is home controller? You can see its name in the <body> tag. Angular will use my controller and will bind HTML elements to its scope. This statement oversimplifies what is really going on, but at the high level it is quite correct. And of course inner text of my the anchor tag is the Title of corresponding menu item. I am using a custom directive noClick to keep anchor tag’s clicks from propagating beyond my ng-click handler. It is a common trick in Angular when using anchor tags. The code for this directive is very simple. You can find it in the downloaded project inside commonDirectives.ts. I am also using ng-cloak directive. This is very useful when using ng-repeat. This directive will keep raw <li> tag from flashing on the screen until the ng-repeat directive is resolved.
That is all there is to it. You can download entire project here.
[Update]: check out two more posts on the subject: http://dotnetspeak.com/2013/12/angular-dynamic-menu-and-initial-url and http://dotnetspeak.com/2013/12/angular-js-and-dynamic-menu-part-2
No comments:
Post a Comment