Get Around the Single ng-app Limitation: Dynamically Load Angular Applications in a Single App Shell

Part 1

tl;dr – Allow your users to select which app to load! Ditch the ng-app in your index.html and use angular.bootstrap(document, [“yourApp”]); instead based on user selection.

In a typical Angular single page web application, commonly referred to as a SPA, a single application context is loaded into a shell when the index page loads.  Angular only allows for having a single ng-app tag where the application is loaded within the html.  If there is more than one ng-app, the first one takes precedence and becomes the active Angular application.  According to the Angular documentation, “Only one AngularJS application can be auto-bootstrapped per HTML document. The first ngApp found in the document will be used to define the root element to auto-bootstrap as an application.” See the angular documentation for more information about this: Angular ngApp.

So normally you may have the following in your index file:

[code language=”javascript”]
<body ng-app="app">
….
</body>
….
<script src="/bower_components/angular/angular.js"></script>
[/code]

In addition to the angular script tag, you will also either have your bundled script tag, or many individual script tags for your specific angular application or whatever framework based application you are using.

Upon page load your app will be bootstrapped and active.  As mentioned before, if you were to try to have two separate ng-app tags, only the first application will load properly upon document ready.  The following only loads app1 as it comes first.

[code language=”javascript”]
<body>
<div ng-app="app1">
</div>
<div ng-app="app2">
</div>
</body>
[/code]

To get around this, remove ngApp from your index.html file altogether, and instead we will manually bootstrap our application based on the user selecting an application.  We will also remove script tags related to Angular along with any related to the specific applications we want to load within the shell.  Instead we will dynamically load js application bundles based on which application is loaded.

In a recent application I dynamically loaded from a configuration source a list of applications that were used to generate a list of menu items to allow dynamic application switching.  The loading and format of the configuration file is kept out for now, and what follows is a simplified handler that created the menu items that could launch specific applications:

[code language=”javascript”]
var setupApplications = function (_appManifests) {

appManifests = _appManifests;
_.each(_appManifests, function(appInstance){
var key = appInstance.Environment + ‘-‘ + appInstance.Application + ‘-‘ + appInstance.ApplicationInstance;
$(‘#available-application-nav’).append
(
$(‘<li>’).attr(‘id’, ‘app-launch-sm-‘ + key).append
(
$(‘<a>’).attr(‘class’, ‘fw600’).attr(‘title’, appInstance.InstanceLabel).append
(
$(‘<span>’).attr(‘class’, ‘text-primary’).append(
$(‘<img>’).attr(‘src’, ‘shell/custom/img/’ + appInstance.InstanceIconSmall)
.attr(‘alt’, appInstance.InstanceLabel)
)
).append( $(‘<span>’).attr(‘class’, ‘sidebar-title’).append(appInstance.InstanceLabel))
)
);
});

//attach click handler to each menu item
$( ‘[id*="app-launch-sm"]’ ).click(function(elem) {
var clickedApp = elem.currentTarget.id.split(‘-launch-sm-‘)[1];
var appCtx = _.findWhere(appManifests, { ‘Application’: clickedApp });
if(appCtx) {
callWebStart(appCtx);
}
});
};
[/code]

Within the configuration file, there is also information for each application about the endpoint that the application lives at, and the method to call when the callWebStart method is located.  JQuery is used to load the bundled js for the specific application selected, and then call the webstart method within the application.  The nice thing about this is that the application being loaded could be Angular, or it could be another framework altogether that will be loaded into the shell region.  In the full application, the shell attach point IDs are passed into the application’s web start method also in the appSections object.

[code language=”javascript”]
var callWebStart = function (appCtx, token) {
var webStartScriptPath = appCtx.Urls.webStart;
var appSections = {
leftContainer: ‘#sidebar_left’,
leftContent: ‘#app-shell-left-sidebar’,
mainContent: ‘#content_wrapper’,
rightContainer: ‘#sidebar_right’,
rightContent: ‘#app-shell-right-sidebar’
};

$.getScript( webStartScriptPath, function( data, textStatus, jqxhr ) {
return webAppStart(appCtx, appSections);
});
}
[/code]

JQuery dynamically loads the bundle from the endpoint in the configuration file, and then calls the webAppStart method, which is the only requirement for any bundle within this app shell to properly load.  Aside from the webstart function, in the case of our angular application selection, the bundle also includes any services, directives, angular.js, and any other js scripts that need to be loaded for our application.  The following is an example of a simple webstart that will load the scripts and then bootstrap our selected Angular application.

[code language=”javascript”]
var webAppStart = function (appManifest, token, uiSections) {

var appJsBundle = appManifest.Urls.appBundle;
var base = appManifest.Urls.base;

//load app specific css files
$(‘head’).append( $(‘<link rel="stylesheet" type="text/css" />’).attr(‘href’, base + ‘app/custom/css/custom.css’) );

//load app specific js files
$.getScript( appJsBundle, function( data, textStatus, jqxhr ) {

//the UI sections passed in from the app shells js are used to set the attach points for the ng-view and also a side menu directive
$(uiSections.mainContent).append(‘<div id="content" class="ng-view" autoscroll="true"></div>’);
$(uiSections.leftContent).append(‘<app1-nav></app1-nav>’);

//if your bundles are loacted on different endpoints, you will have to add the URLs to the whitelist within Angular,
//as angular security by default prevents script loading from cross origins
app.config(function($sceDelegateProvider) {
$sceDelegateProvider.resourceUrlWhitelist([
// Allow same origin resource loads.
‘self’,
base + ‘**’
]);
});

//need to wait for the document to be ready
angular.element(document).ready(function() {
var app = angular.bootstrap(document, ["app1"]);

//we can now use any application services at this point!
app.get("anApp1Service").someServiceMethod();
});
});
};
[/code]

At this point, our application is now loaded, Angular and “app1” are now active and fully functioning.  When the visitor first visited the base site, there was no overhead of loading this or multiple other applications that the user may not require or even have access to!

For Part 2, I will cover a few more details related to the shell and Angular specifics.  Also up next will be how to handle switching between the applications, and how to handle storing bookmarks to a specific application.