How to lazy-load almost anything in AngularJS

I was asked to build a module web framework which would allow various teams to plug-in their parts as they become available with none or minimal affects on other existing parts. Each new part could be represented by a tab or a menu section. While each part relies on some common/shared services, frameworks or components, each also has unique dependencies. For example, all parts can rely on jquery, but the first part relies on Highcharts library for charting, while another part uses SlickGrid to represent data. Firstly, I had to pick libraries and frameworks to crate a foundation for the site. Foundation components would be either be used directly or relied upon by all the parts in the system. After careful evaluation, I picked the following

  • jQuery
  • AngularJS
  • RequireJS
  • BootstrapUI with Angular extensions

To wire up Angular and Require, I used the seed example found here: https://github.com/tnajdek/angular-requirejs-seed. Thanks to the configuration file used by RequireJS, my main index.html file only has a single script dependency:

<script data-main="app/js/main" src="app/lib/requirejs/require.js"></script>

The app/js/main.js file defines all the script dependencies required by the website. This file would need to be modified as dependencies get added by the parts of the system. For example, if a new part coming online requires another library, that library would have to be registered in this file along with its dependencies.

require.config({
	paths: {
		jquery: '../lib/{path to jquery}',
		angular: '../lib/{path to angular}',
		angularRoute: '../lib/{path to angular ui router}',
		angularBootstrap: '../lib/{path to bootstrap ui}',
		highcharts: '../lib/{path to highcharts}',
		'highcharts-export': '../lib/{path to highcharts export}',
		'highcharts-drilldown': '../lib/{path to highcharts drilldown}',
		'jquery-ui':'../lib/{path to jquery ui}',
		'jquery-drag-and-drop': '../lib/{path to jquery drag-n-drop}',
		'slickgrid-core': '../lib/{path to slickgrid core}',
		'slickgrid-cellselectionmodel': '../lib/{path to slickgrid cellselection model}',
		'slickgrid-rowmovemanager': '../lib/{path to slickgrid rowmovemanager}',
		'slickgrid-grid':'../lib/{path to slickgrid}'
	},

//Dependencies defined below
	shim: {
		'angular' : {
			'exports' : 'angular',
			'deps': ['jquery']
	},
		'angularRoute': ['angular'],
		'angularBootstrap': ['angular'],
		'highchart-theme': ['highcharts'],
		'highcharts-export': ['highcharts'],
		'highcharts-drilldown': ['highcharts'],
		'jquery-ui':['jquery'],
		'jquery-drag-and-drop':['jquery-ui'],
		'slickgrid-core':['jquery-drag-and-drop'],
		'slickgrid-cellselectionmodel':['slickgrid-core'],
		'slickgrid-rowmovemanager':['slickgrid-core'],
		'slickgrid-grid':['slickgrid-cellselectionmodel', 'slickgrid-rowmovemanager']
	},
        //Not sure about the significance of this piece below
	priority: [
		'angular'
	]
});

//After configuration, let's go running the app!

//http://code.angularjs.org/1.2.1/docs/guide/bootstrap#overview_deferred-bootstrap
window.name = 'NG_DEFER_BOOTSTRAP!';

require(['angular',
	'app',
	'app-config',
	'controllers'], function(angular, app) {
	'use strict';

	angular.element().ready(function() {
		angular.resumeBootstrap([app['name']]);
	});
});

So we start by requiring Angular, App, App-Config and Controllers, which initializes angular and runs it. Most of the standard angular code has been moved into modules according to AMD specs (https://github.com/amdjs/amdjs-api/wiki/AMD) used by RequireJS. The require() function call above first specifies that it needs to load angular module (defined above), app, app-config and controllers modules, which are not defined. If not explicitely defined, RequireJS will attempt to locate these modules in the same directory as this main.js file inside files whose names would same as names of the modules with ‘.js’ appended. So I must have these files:

  • app.js
  • app-config.js
  • controllers.js

Lets’ take a look at those: app.js

define ([
         'angular',
         'angularRoute',
         'angularBootstrap',
         ], function(angular) {

	'use strict';

	var myApp = angular.module('myApp', ['ui.bootstrap', 'ui.bootstrap.tpls', 'ui.router']);

	return myApp;
});

Notice that I am returning myApp from the app module. That allows me to reference it later. For example, if you look back to the main.js code, you’ll see that a parameter named app is being passed into function inside require() call (line 52). That app is the same as myApp being returned by the app module function (line 11). I need to have access to the app because I reference it from all other modules that depend on angular. The rest of the configuration is done inside the app-config module app-config.js

define(['app',
        'services/site-definition-service',
        'services/lazy-loader']
		, function(app) {

app.config(['$stateProvider', '$controllerProvider', '$urlRouterProvider', '$httpProvider', '$provide', '$compileProvider',
	                    function (stateProvider, cp, urlRouterProvider, httpProvider, provide, compProvider) {

		//These would be used later for lazy-loading controllers, directives and services
		app.$stateProvider = stateProvider;
		app.$controllerProvider = cp;
		app.$provide = provide;
		app.$compileProvider = compProvider;

		urlRouterProvider.otherwise('/');
...

The general function behind omitted code above is to declare states for your website. Each state would represent a part of the site that other developers would contribute. You can postpone defining all states until later by having a service retrieve your site configuration and register states based on configuration you specify in the service response. Look here for more info: . Controllers that you are declaring during normal bootstrapping of Angular site can simply be declared inside AMD modules such as this example below. Because controllers module is referenced as a dependency at startup, we can do this: controllers.js


define(['angular', 'app', './services/site-definition-service'], function(angular, app) {

	'use strict';

	app.controller('SomeController', ['$scope',
	                                 function (scope) {
		...
	}]);

Notice that the controller references something called site-definition-service. This is another javascript module which defines a service necessary to retrieve definition of the site. Such definition is a simple JSON structure which allows developers to configure states. As part of state configuration you can specify a resolve {} directive to resolve additional dependencies. Dependencies could be specified as an array of module names, which RequireJS would recognize when you configure them back in the main.js file. Here’s an example lf lazy-loading state registration:


define(['app',
        'services/lazy-loader'], function(app) {

//app.$stateProvider is used to lazy-register states
app.$stateProvider.state('stateName', {
			url: '/myPart',
			resolve: {
				'loadDependencies': function ($stateParams, LazyLoader) {
					return LazyLoader.loadDependencies('stateName');
				},
			},

and here’s what lazy loading service code would look like: lazy-loader.js

  
 define(['angular', 'app', 'require', './services/site-definition-service'], function (angular, app, requirejs) {

	'use strict';
	app.service('LazyLoader', ['$cacheFactory', '$http', '$rootScope', '$q', 'SiteDefinitionService',
                      function (cacheFactory, http, rootScope, q, siteDefService) {
             var self = this;

             this.loadDependencies = function(stateName) {            

               var deferred = q.defer();
               http.get('rest/sitedefinition/' + stateName).success(function (data, status, headers, config) {
                  var deps = data.dependencies; //array
                  if(deps &amp;&amp; deps instanceof Array) {
                     loadDependenciesFromArray(deps, deferred);
                  } else {
                     deferred.resolve();
                  }
               });

               return deferred.promise();
            }

		this.loadDependenciesFromArray = function(depArr, deferred){
			requirejs(depArr, function() {
				deferred.resolve();
			});

	}]);
});

Examples of lazy-loading dependencies

Directives

custom-directive.js

define(['app'], function(app) {

	'use strict';
	//app.$compileProvider is used to lazy-register directives
	app.$compileProvider.directive('customDirective', 
    ...

Services

part2-service.js

define(['app'], function(app) {
	'use strict';

	//app.$provide is used to lazy-register services
	app.$provide.service('Part2Service', 
    ...

Controllers

part2-controller.js

define(['app'], function(app) {

	'use strict';
	//app.$controllerProvider is used to lazy-register controllers.  Part2Service was loaded as a dependency earlier
	app.$controllerProvider.register('Part2Controller', ['$scope', 'Part2Service',
               function (scope, svc) {
    ...

To lazy load CSS files you’ll need a custom directive. I’ll cover this in another post. This post has gotten big and convoluted. If something is unclear, please comment, I’ll adjust the content as necessary. Thank you.

Advertisements

8 thoughts on “How to lazy-load almost anything in AngularJS

  1. magikmaker

    Interesting article. You wrote “You can postpone defining all states until later by having a service retrieve your site configuration and register states based on configuration you specify in the service response. Look here for more info: .” Could you please provide that link?

    Reply
  2. Rodrigo

    Hello Alex, why you use RequireJS instead of AngularJS module?
    AngularJS also works in a modular way, so you can create modular structure without using RequireJS
    I’m trying to implement modular structure with only AngularJS
    Congratulations on the birth of your son, I wish good health

    Reply
    1. alexfeinberg Post author

      AFAIK, out of the box, Angular does not provide a facility for downloading additional JS files on demand dynamically in a lazy-load fashion.

      In my setup, a state defined on the server has additional javascript dependencies. For example, if a partial uses highchars or slickgrid or another library, it would need to load one or more additional javascript files. RequireJS does it well, as well as allowing me to define additional directives, services and other Angular abstractions in a lazy way – after the main page was rendered.

      Thank you for your warm wishes.

      Reply

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s