This problem has been taking up the last day or so.
I\'ve been trying to get my AngularJS application to load the script files for each state\'s components lazily. I\'m
Here's the code for an Angular module lazy
, depending on the ui.router
module. When it's included in your module's dependencies, the lazy loading functionality of the state's scripts will be enabled. I've included examples of the primary app module, a few lazy components, and my index.html
, sanitized for demonstration purposes. I'm using the Script.js library to actually handle the script loading.
angular-ui-router-lazy.js
/**
* Defines an AngularJS module 'lazy' which depends on and extends the ui-router
* module to lazy-load scripts specified in the 'scripts' attribute of a state
* definition object. This is accomplished by registering a $stateChangeStart
* event listener with the $rootScope, interrupting the associated state change
* to invoke the included $scriptService which returns a promise that restarts the
* previous state transition upon resolution. The promise resolves when the
* extended Script.js script loader finishes loading and inserting a new <script>
* tag into the DOM.
*
* Modules using 'lazy' to lazy-load controllers and services should call lazy.makeLazy
* on themselves to update the module API to inject references for the various $providers
* as the original methods are only useful before bootstrapping, during configuration,
* when references to the $providers are in scope. lazy.makeLazy will overwrite the
* module.config functions to save these references so they are available at runtime,
* after module bootstrapping.
* See http://ify.io/lazy-loading-in-angularjs/ for additional details on this concept
*
* Calls to $stateProvider.state should include a 'scripts' property in the object
* parameter containing an object with properties 'controllers', 'directives', 'services',
* 'factories', and 'js', each containing an array of URLs to JS files defining these
* component types, with other miscelleneous scripts described in the 'js' array.
* These scripts will all be loaded in parallel and executed in an undefined order
* when a state transition to the specified state is started. All scripts will have
* been loaded and executed before the 'resolve' property's promises are deferred,
* meaning services described in 'scripts' can be injected into functions in 'resolve'.
*/
(function() {
// Instantiate the module, include the ui.router module for state functionality
var lazy = angular.module('lazy',['ui.router']);
/**
* Hacking Angular to save references to $providers during module configuration.
*
* The $providers are necessary to register components, but they use a private injector
* only available during bootstrap when running config blocks. The methods attached to the
* Vanilla AngularJS modules rely on the same config queue, they don't actually run after the
* module is bootstrapped or save any references to the providers in this injector.
* In makeLazy, these methods are overwritten with methods referencing the dependencies
* injected at configuration through their run context. This allows them to access the
* $providers and run the appropriate methods on demand even after the module has been
* bootstrapped and the $providers injector and its references are no longer available.
*
* @param module An AngularJS module resulting from an angular.module call.
* @returns module The same module with the provider convenience methods updated
* to include the DI $provider references in their run context and to execute the $provider
* call immediately rather than adding calls to a queue that will never again be invoked.
*/
lazy.makeLazy = function(module) {
// The providers can be injected into 'config' function blocks, so define a new one
module.config(function($compileProvider,$filterProvider,$controllerProvider,$provide) {
/**
* Factory method for generating functions to call the appropriate $provider's
* registration function, registering a provider under a given name.
*
* @param registrationMethod $provider registration method to call
* @returns function A function(name,constructor) calling
* registationMethod(name,constructor) with those parameters and returning the module.
*/
var register = function(registrationMethod) {
/**
* Function calls registrationMethod against its parameters and returns the module.
* Analogous to the original module.config methods but with the DI references already saved.
*
* @param name Name of the provider to register
* @param constructor Constructor for the provider
* @returns module The AngularJS module owning the providers
*/
return function(name,constructor) {
// Register the provider
registrationMethod(name,constructor);
// Return the module
return module;
};
};
// Overwrite the old methods with DI referencing methods from the factory
// @TODO: Should probably derive a LazyModule from a module prototype and return
// that for the sake of not overwriting native AngularJS code, but the old methods
// don't work after `bootstrap` so they're not necessary anymore anyway.
module.directive = register($compileProvider.directive);
module.filter = register($filterProvider.register);
module.controller = register($controllerProvider.register);
module.provider = register($provide.provider);
module.service = register($provide.service);
module.factory = register($provide.factory);
module.value = register($provide.value);
module.constant = register($provide.constant);
});
// Return the module
return module;
};
/**
* Define the lazy module's star $scriptService with methods for invoking
* the extended Script.js script loader to load scripts by URL and return
* promises to do so. Promises require the $q service to be injected, and
* promise resolutions will take place in the Script.js rather than Angular
* scope, so $rootScope must be injected to $apply the promise resolution
* to Angular's $digest cycles.
*/
lazy.service('$scriptService',function($q,$rootScope) {
/**
* Loads a batch of scripts and returns a promise which will be resolved
* when Script.js has finished loading them.
*
* @param url A string URL to a single script or an array of string URLs
* @returns promise A promise which will be resolved by Script.js
*/
this.load = function(url) {
// Instantiate the promise
var deferred = $q.defer();
// Resolve and bail immediately if url === null
if (url === null) { deferred.resolve(); return deferred.promise; }
// Load the scripts
$script(url,function() {
// Resolve the promise on callback
$rootScope.$apply(function() { deferred.resolve(); });
});
// Promise that the URLs will be loaded
return deferred.promise;
};
/**
* Convenience method for loading the scripts specified by a 'lazy'
* ui-router state's 'scripts' property object. Promises that all
* scripts will be loaded.
*
* @param scripts Object containing properties 'controllers', 'directives',
* 'services', 'factories', and 'js', each containing an array of URLs to JS
* files defining those components, with miscelleneous scripts in the 'js' array.
* any of these properties can be left off of the object safely, but scripts
* specified in any other object property will not be loaded.
* @returns promise A promise that all scripts will be loaded
*/
this.loadState = function(scripts) {
// If no scripts are given, instantiate, resolve, and return an easy promise
if (scripts === null) { var d = $q.defer; d.resolve(); return d; }
// Promise that all these promises will resolve
return $q.all([
this.load(scripts['directives'] || null),
this.load(scripts['controllers'] || null),
this.load(scripts['services'] || null),
this.load(scripts['factories'] || null),
this.load(scripts['js'] || null)
]);
};
});
// Declare a run block for the module accessing $rootScope, $scriptService, and $state
lazy.run(function($rootScope,$scriptService,$state) {
// Register a $stateChangeStart event listener on $rootScope, get a script loader
// for the $rootScope, $scriptService, and $state service.
$rootScope.$on('$stateChangeStart',scriptLoaderFactory($scriptService,$state));
});
/**
* Returns a two-state function for handing $stateChangeStart events.
* In the first state, the handler will interrupt the event, preventing
* the state transition, and invoke $scriptService.loadState on the object
* stored in the state definition's 'script' property. Upon the resolution
* of the loadState call, the handler restarts a $stateChangeStart event
* by invoking the same transition. When the handler is called to handle
* this second event for the original state transition, the handler is in its
* second state which allows the event to continue and the state transition
* to happen using the ui-router module's default functionality.
*
* @param $scriptService Injected $scriptService instance for lazy-loading.
* @param $state Injected $state service instance for state transitions.
*/
var scriptLoaderFactory = function($scriptService,$state) {
// Initialize handler state
var pending = false;
// Return the defined handler
return function(event,toState,toParams,fromState,fromParams) {
// Check handler state, and change state
if (pending = !pending) { // If pending === false state
// Interrupt state transition
event.preventDefault();
// Invoke $scriptService to load state's scripts
$scriptService.loadState(toState.scripts)
// When scripts are loaded, restart the same state transition
.then(function() { $state.go(toState,toParams); });
} else { // If pending === true state
// NOOP, 'ui-router' default event handlers take over
}
};
};
})();
/** End 'lazy' module */
index.html
<!DOCTYPE html>
<html>
<head>
<title>Lazy App</title>
<script type='text/javascript' src='libs/script.js'></script>
<script type='text/javascript'>
$script.queue(null,'libs/angular/angular.min.js','angular')
.queue('angular','libs/angular/angular-ui-router.min.js','ui-router')
.queue('ui-router','libs/angular/angular-ui-router-lazy.js','lazy')
.queue('lazy',null,'libs-angular')
.queue('libs-angular','lazyapp/lazyapp.module.js','lazyapp-module');
$script.ready('lazyapp-module',function() { console.log('All Scripts Loaded.'); });
</script>
</head>
<body>
<div ui-view='mainView'></div>
</body>
</html>
Function Hacked into Script.js because I Prefer the Syntax
$script.queue = function(aQueueBehind,aUrl,aLabel) {
if (aQueueBehind === null) { return $script((aUrl === null?[null]:aUrl),aLabel); }
$script.ready(aQueueBehind,function() {
if (aUrl !== null)
$script(aUrl,aLabel);
else
$script.done(aLabel);
});
return $script;
}
lazyapp.module.js
(function() {
var lazyApp = angular && angular.module('lazyApp ',['lazy']);
lazyApp = angular.module('lazy').makeLazy(lazyApp);
lazyApp.config(function($stateProvider) {
$stateProvider.state({
name: 'root',
url: '',
views: {
'mainView': { templateUrl: '/lazyapp/views/mainview.html', controller: 'lazyAppController' }
},
scripts: {
'directives': [ 'lazyapp/directives/lazyheader/src/lazyheader.js' ],
'controllers': [ 'lazyapp/controllers/lazyappcontroller.js' ],
'services': [ 'lazyapp/services/sectionservice.js' ]
},
resolve: {
sections: function(sectionService) {
return sectionService.getSections();
}
}
});
});
angular.bootstrap(document,['lazyApp']);
})();
sectionservice.js
(function() {
var lazyApp = angular.module('lazyApp');
lazyApp.service('sectionService',function($q) {
this.getSections = function() {
var deferred = $q.defer();
deferred.resolve({
'home': {},
'news': {},
'events': {},
'involved': {},
'contacts': {},
'links': {}
});
return deferred.promise;
};
});
})();
lazyheader.js
(function() {
var lazyApp = angular.module('lazyApp ');
lazyApp.directive('lazyHeader',function() {
return {
templateUrl: 'lazyapp/directives/lazyheader/templates/lazyheader-main.html',
restrict: 'E'
};
});
})();
lazyappcontroller.js
(function() {
var lazyApp = angular.module('lazyApp ');
lazyApp.controller('lazyAppController',function(sections) {
// @TODO: Control things.
console.log(sections);
});
})();