After my brief primer (Part 1) of the things I’m looking to accomplish with Angular in what I consider a large-scale application, I’ve had a week or so to toss around ideas and get a solid foundation.
To recap, I want this application to avoid becoming an unruly, behemoth that is unmanageable. I want core, reusable components to be separated from core functionality. That is to say, I want loose coupling. The application should have hooks, navigation, and what not that is self-aware while allowing multiple developers to create their own discrete set of functional areas. In that vein, or to that end, I want “areas” to be independent for the most part and, as such, developers should be able to work on the individual functional areas without stepping all over each other’s code, or worrying much about breaking other parts of the application.
Again, I think Visual Studio’s MVC5 “areas” feature is an excellent way to create independent SPA’s within an over-arching, large-scale application.
So, what I have at this point is (4) discrete areas. The main application is intended to control payroll, commissions, and other related functions. The organization of those areas, and by design, are independent though. Each area will have its own C# code, route registration, scripts, models, and controllers.
In Solution explorer, it looks quite simply laid out:
Drilling into one of the areas, I basically follow the same patterns that I would follow for any stand-alone SPA:
As you can see, this Area represents an application, for the most part, in and of itself. Also, if I want to create a new applicaiton/area/SPA, it becomes a file copy and name refactor activity.
Where things get interesting is sharing resources across applications. This is where Angular’s module injection and naming becomes very useful. Creating a root “holder” for common components because that every area’s “app.js” can simply inject the modules associated with the root, shared components. My root is called “commissions” and my area application is called “commsPayment.” It looks like this, for example:
(function () { angular.module('commsPayment.services', ['ngResource', 'ngAnimate', 'ngCookies']); angular.module('commsPayment.controllers', []); angular.module("commsPayment.directives", []); angular.module("commsPayment.infrastructure", []); angular.module("commsPayment.models", []); angular.module("commsPayment.values", []); angular.module("commsPayment.constants", []); angular.module("commissions.constants", []); angular.module("commissions.controllers", []); angular.module("commissions.directives", []); angular.module("commissions.infrastructure", []); angular.module("commissions.values", []); angular.module("commissions.services", ['ngResource', 'ngAnimate', 'ngCookies']); var commissions = angular.module('commissions', [ 'commissions.controllers', 'commissions.directives', 'commissions.constants', 'commissions.infrastructure', 'commissions.services', 'commissions.values' ]); var commsPayment = angular.module('commsPayment', [ 'commsPayment.services', 'commsPayment.controllers', 'commsPayment.directives', 'commsPayment.infrastructure', 'commsPayment.models', 'commsPayment.values', 'commsPayment.constants', 'commissions', 'ui.bootstrap', 'ui.router', 'ui.sortable', 'ui', 'ngAnimate' ]);
The only other piece that’s required to make this work is the proper script references. MVC bundles make this a cinch to set-up. I create a single “shared” bundle which all of the area applications can reference in their default/Index.cshtml. The path to ~/Scripts, would be pointing to our root-level scripts folder:
var sharedApp = new ScriptBundle("~/bundles/scripts/sharedApp") .IncludeDirectory("~/Scripts/app/directives", "*.js", true) .Include( "~/Scripts/app/interceptors/requestInterceptor.js", "~/Scripts/app/interceptors/unauthorizedInterceptor.js", "~/Scripts/app/services/dialogService.js", "~/Scripts/app/services/notificationService.js", "~/Scripts/app/services/idleService.js", "~/Scripts/app/services/watchCountService.js" );
For our Area application-level bundles, we just spin off bundles for each of the areas:
var paymentApp = new ScriptBundle("~/bundles/scripts/paymentApp") .IncludeDirectory("~/areas/payment/Scripts", "*.js", true);
Finally, in our base Index page for the area SPA, we only need to reference the correct bundles for the area and shared resources:
@section scripts { @Scripts.Render("~/bundles/scripts/lib") @Scripts.Render("~/bundles/scripts/paymentApp") @Scripts.Render("~/bundles/scripts/sharedApp") <script type="text/javascript" src="@Url.Action("angularValues.js", MVC.Home.Name, new { area = "" }, null)"></script> <script type="text/javascript" src="@Url.Action("angularValues.js", MVC.Payment.Home.Name, new { area = MVC.Payment.Name }, null)"></script> }
Note that I use a little helper I call “angularValues” that basically returns a server-generated Angular module for the purposes of pushing information down to client-side code.
Beyond this shared bits of code, we also need, as I alluded to before, a mechanism to tie all of the applications together. In a previously blog post entitled “Advanced Angular Navigation,” I described methods by which we can intercept $location events to determine when a user has navigated away from the current state. With the multiple area SPA’s, our navigiation becomes somewhat different. Our links between the areas become hard, page-refreshing links. So, we have to implement navigation mechanisms that understand this. I still use a base “navigationService,” to deal with this common functionality, but it becomes more of a matter of intercepting click events ourselves and determine whether the current state will allow the user to navigate away. Conditions that could potentially prevent navigating, for example, would be when the user has unsaved changes.
For this particular application, all of our navigation between areas will be handled by a slide-out left navigation bar. Any area can define its own “leftNavCtrl” and define the click event handlers for when a navigation element is clicked.
Our navigation service provides the ability for any “current state” controller that it has been injected to let the navService know when it’s “dirty” and how to handle when the user is requesting a redirect. The “leftNavCtrl” can then have the navService injected into it and call these methods to determine if navigation is allowed:
(function () { var leftNavCtrl = function ($rootScope, $state, $scope, $q, $window, dialogService, navigationService) { var vm = this, navigateCallback = function () { if (navigationService.isDirty()) { return dialogService.openDiscardChangesDialog(); } else { return true; } }; vm.navClicked = function (navUrl) { $rootScope.$broadcast('leftNavClicked', { url: navUrl }); $q.when(navigateCallback()).then( function (result) { $window.location.href = navUrl; } ); }; }; leftNavCtrl.$inject = ['$rootScope', '$state', '$scope', '$q', '$window', 'dialogService', 'navigationService']; angular .module('commsPayment.controllers') .controller('leftNavCtrl', leftNavCtrl); })()
Any area that is using this mechanism only needs to render the left-nav partial view within its primary layout. Using MVC’s URL rendering capabilities, we simply define ng-click events that are defined with a common naming (navClicked) convention that pass in the URL involved in the redirection.
@{ var paymentUrl = Url.Action(MVC.Payment.Home.ActionNames.Index, MVC.Payment.Home.Name.ToLower(), new { area = "payment" }); } <div class="left-sidebar hide-for-small-only" ng-controller="leftNavCtrl as ln"> <ul class="property-nav"> <li> <a @Html.Raw(string.Format("ng-click=\"ln.navClicked('{0}')\"", paymentUrl))><img alt="Patterntap dark" src="@Links.Content.images.icon_circles_png">Payment</a> </li> </ul> </div>
Note that if the current state controller indicates that it’s “dirty,” then the leftNavCtrl will use promises to display a dialog to the user indicating whether or not they want to stay on the current page or navigate away. If they choose to navigate away, we use the $window service to set the browser’s URL. This effectively will refresh the browser and redirect to the proper area application (SPA).
You may have also noticed that I have a ‘headerCtrl’ define as well. This follows the same basic premise that I described above for the left-navigation.
So far, this approach is working very well for our development process. There is already another developer that is working on an independent “area app” without affecting any of the work in my own application.
hi, this is awesome…I was looking for a way to do this for days and wasting time searching. What im trying to do is create a bunch of mini applications inside one huge application for my portfolio. This way, instead of showing people all my work in separate sites and source code I can have it all in one huge application. is there a beta source code you could share just so I can get better idea of how to structure this?
I currently use this concept in a production app, but can’t share that source code. I’ll put together a demo, though, during the course of the week, and put it on github.