define([
  'angular', 
  'uiRouter',
  'ocLazyLoad',
  'ocLazyLoad-uiRouterDecorator'
], function (angular) {
  
  var app = angular.module('app', ['ui.router', 'oc.lazyLoad', 'oc.lazyLoad.uiRouterDecorator']);
   
  app.config(function($stateProvider, $locationProvider, $ocLazyLoadProvider) {
    
    /**
     * module: string module id
     * files: a single file (string), or an array of files to load
     * templateUrl: view template
     * 
     * returns an array where the first element is the templateProvider
     * and the second element is the resolve function
     */
    
    $ocLazyLoadProvider.config({
      loadedModules: ['app'],
      asyncLoader: require
    });
    
    $stateProvider
      .state('home', {
        url: "/",
        template: "<p>Hello {{name}}. Would you like to... <a href='#lazy'>load lazy</a>?</p>",
        controller: 'mainCtrl'
      })
      .state('lazy', {
        url: "/lazy",
        lazyModule: 'app.lazy',
        lazyFiles: 'lazy',
        lazyTemplateUrl: 'lazy.html',
        controller: 'lazyCtrl'
      });
  
    $locationProvider.html5Mode(true);
  });
  
  app.controller('mainCtrl', function($scope) {
    $scope.name = 'World';
  });
  
});
<!DOCTYPE html>
<html>

  <head>
    <meta charset="utf-8" />
    <title>AngularJS Plunker</title>
    <script>document.write('<base href="' + document.location + '" />');</script>
    <link rel="stylesheet" href="style.css" />
    <script data-main="main.js" data-require="requirejs@*" data-semver="2.1.9" src="//cdnjs.cloudflare.com/ajax/libs/require.js/2.1.9/require.js"></script>
  </head>

  <body>
    <h2>Lazy-loading with ocLazyLoad+requirejs</h2>
    <div ui-view></div>
  </body>

</html>
[ui-view] {
  border: 1px solid red;
  min-height:100px;
}

require.config({
	paths: {
        angular: 'https://code.angularjs.org/1.2.16/angular',
        uiRouter: 'http://angular-ui.github.io/ui-router/release/angular-ui-router',
        ocLazyLoad: 'vendor/ocLazyLoad',
        'ocLazyLoad-uiRouterDecorator': 'vendor/ocLazyLoad-uiRouterDecorator',
        app: 'app'
	},
	shim: {
		'angular': {
			exports: 'angular'
		},
		'ngRoute': ['angular'],
		'app': ['angular']
	}
});


require(['angular', 'app'], function(angular) {
	'use strict';
	angular.bootstrap(document, ['app']);
});
define(['angular', 'lazy-partials'], function (angular) {
  var appLazy = angular.module('app.lazy', ['app.lazy.partials']);

  appLazy.controller('lazyCtrl', function ($scope, $compile, $templateCache) {
    $scope.data = 'my data';
  });
});
// Imagine that this file was actually compiled with something like grunt-html2js
// So, what you actually started with was a bunch of .html files which were compiled into this one .js file...
define(['angular'], function (angular) {
  angular.module('app.lazy.partials', [])
    .run(function($templateCache) {
      $templateCache.put('lazy.html', '<p>This is lazy content! and <strong>{{data}}</strong> <a href="#">go back</a></p>');
    });
});
/**
 * ocLazyLoad - Load modules on demand (lazy load) with angularJS
 * @version v0.3.0
 * @link https://github.com/ocombe/ocLazyLoad
 * @license MIT
 * @author Olivier Combe <olivier.combe@gmail.com>
 */
(function() {
	'use strict';
	var regModules = ['ng'],
		regInvokes = [],
		filesLoaded = [],
		ocLazyLoad = angular.module('oc.lazyLoad', ['ng']),
		broadcast = angular.noop;

	ocLazyLoad.provider('$ocLazyLoad', ['$controllerProvider', '$provide', '$compileProvider', '$filterProvider', '$injector', '$animateProvider',
		function($controllerProvider, $provide, $compileProvider, $filterProvider, $injector, $animateProvider) {
			var modules = {},
				templates = [],
				providers = {
					$controllerProvider: $controllerProvider,
					$compileProvider: $compileProvider,
					$filterProvider: $filterProvider,
					$provide: $provide, // other things
					$injector: $injector,
					$animateProvider: $animateProvider
				},
				anchor = document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0],
				jsLoader, cssLoader, templatesLoader,
				debug = false,
				events = false;

			// Let's get the list of loaded modules & components
			init(angular.element(window.document));

			this.$get = ['$timeout', '$log', '$q', '$templateCache', '$http', '$rootElement', '$rootScope', function($timeout, $log, $q, $templateCache, $http, $rootElement, $rootScope) {
				var instanceInjector;

				if(!debug) {
					$log = {};
					$log['error'] = angular.noop;
					$log['warn'] = angular.noop;
				}

				// Make this lazy because at the moment that $get() is called the instance injector hasn't been assigned to the rootElement yet
				providers.getInstanceInjector = function() {
					return (instanceInjector) ? instanceInjector : (instanceInjector = $rootElement.data('$injector'));
				};

				broadcast = function broadcast(eventName, params) {
					//					console.log('broadcast');
					if(events) {
						//						console.log(arguments);
						$rootScope.$broadcast(eventName, params);
					}
				}

				/**
				 * Load a js/css file
				 * @param type
				 * @param path
				 * @returns promise
				 */
				var buildElement = function buildElement(type, path) {
					var deferred = $q.defer(),
						el, loaded;

					// Switch in case more content types are added later (for example a native JS loader!)
					switch(type) {
						case 'css':
							el = document.createElement('link');
							el.type = 'text/css';
							el.rel = 'stylesheet';
							el.href = path;
							break;
						case 'js':
							el = document.createElement('script');
							el.src = path;
							break;
						default:
							deferred.reject(new Error('Requested type "' + type + '" is not known. Could not inject "' + path + '"'));
							break;
					}
					el.onload = el['onreadystatechange'] = function(e) {
						if((el['readyState'] && !(/^c|loade/.test(el['readyState']))) || loaded) return;
						el.onload = el['onreadystatechange'] = null
						loaded = 1;
						if(filesLoaded.indexOf(path) === -1) {
							filesLoaded.push(path);
							broadcast('ocLazyLoad.fileLoaded', path);
						}
						deferred.resolve();
					}
					el.onerror = function(e) {
						console.log('error');
						deferred.reject(new Error('Unable to load '+path));
					}
					el.async = 1;
					anchor.insertBefore(el, anchor.lastChild);

					return deferred.promise;
				}

				/**
				 * jsLoader function
				 * @type Function
				 * @param paths array list of js files to load
				 * @param callback to call when everything is loaded. We use a callback and not a promise
				 * because the user can overwrite jsLoader and it will probably not use promises :(
				 */
				jsLoader = jsLoader || function(paths, callback) {
					var promises = [];
					angular.forEach(paths, function loading(path) {
						promises.push(buildElement('js', path));
					});
					$q.all(promises).then(function success() {
						callback();
					}, function error(err) {
						callback(err);
					});
				}

				/**
				 * cssLoader function
				 * @type Function
				 * @param paths array list of css files to load
				 * @param callback to call when everything is loaded. We use a callback and not a promise
				 * because the user can overwrite cssLoader and it will probably not use promises :(
				 */
				cssLoader = cssLoader || function(paths, callback) {
					var promises = [];
					angular.forEach(paths, function loading(path) {
						promises.push(buildElement('css', path));
					});
					$q.all(promises).then(function success() {
						callback();
					}, function error(err) {
						callback(err);
					});
				}

				/**
				 * templatesLoader function
				 * @type Function
				 * @param paths array list of css files to load
				 * @param params object config parameters for $http
				 * @param callback to call when everything is loaded. We use a callback and not a promise
				 * because the user can overwrite templatesLoader and it will probably not use promises :(
				 */
				templatesLoader = templatesLoader || function(paths, params, callback) {
					if(angular.isString(paths)) {
						paths = [paths];
					}
					if(angular.isFunction(params)) {
						callback = params;
						params = {};
					}
					if(angular.isUndefined(params)) {
						params = {};
					}
					var promises = [];
					angular.forEach(paths, function(url) {
						var deferred = $q.defer();
						promises.push(deferred.promise);
						if(templates.indexOf(url) === -1 || (angular.isDefined(params) && params.cache === false)) {
							$http.get(url, params).success(function(data) {
								angular.forEach(angular.element(data), function(node) {
									if(node.nodeName === 'SCRIPT' && node.type === 'text/ng-template') {
										$templateCache.put(node.id, node.innerHTML);
									}
								});
								templates.push(url);
								deferred.resolve();
							}).error(function(data) {
								var err = 'Error load template "' + url + '": ' + data;
								$log.error(err);
								deferred.reject(new Error(err));
							});
						} else {
							deferred.resolve();
						}
					});
					return $q.all(promises).then(function success() {
						callback();
					}, function error(err) {
						callback(err);
					});;
				}

				var filesLoader = function(paths, params) {
					var cssFiles = [],
						templatesFiles = [],
						jsFiles = [],
						promises = [];

					angular.forEach(paths, function(path) {
						if(/\.css[^\.]*$/.test(path)) {
							cssFiles.push(path);
						} else if(/\.(htm|html)[^\.]*$/.test(path)) {
							templatesFiles.push(path);
						} else {
							jsFiles.push(path);
						}
					});

					if(cssFiles.length > 0) {
						var cssDeferred = $q.defer();
						cssLoader(cssFiles, function(err) {
							if(angular.isDefined(err)) {
								$log.error(err);
								cssDeferred.reject(err);
							} else {
								cssDeferred.resolve();
							}
						});
						promises.push(cssDeferred.promise);
					}

					if(templatesFiles.length > 0) {
						var templatesDeferred = $q.defer();
						templatesLoader(templatesFiles, params, function(err) {
							if(angular.isDefined(err)) {
								$log.error(err);
								templatesDeferred.reject(err);
							} else {
								templatesDeferred.resolve();
							}
						});
						promises.push(templatesDeferred.promise);
					}

					if(jsFiles.length > 0) {
						var jsDeferred = $q.defer();
						jsLoader(jsFiles, function(err) {
							if(angular.isDefined(err)) {
								$log.error(err);
								jsDeferred.reject(err);
							} else {
								jsDeferred.resolve();
							}
						});
						promises.push(jsDeferred.promise);
					}

					return $q.all(promises);
				}

				return {
					getModuleConfig: function(name) {
						if(!modules[name]) {
							return null;
						}
						return modules[name];
					},

					setModuleConfig: function(module) {
						modules[module.name] = module;
						return module;
					},

					getModules: function() {
						return regModules;
					},

					// deprecated
					loadTemplateFile: function(paths, params) {
						return filesLoader(paths, params);
					},

					load: function(module, params) {
						var self = this,
							config = null,
							moduleCache = [],
							deferredList = [],
							deferred = $q.defer(),
							moduleName,
							errText;

						// If module is an array, break it down
						if(angular.isArray(module)) {
							// Resubmit each entry as a single module
							angular.forEach(module, function(m) {
								deferredList.push(self.load(m));
							});

							// Resolve the promise once everything has loaded
							$q.all(deferredList).then(function() {
								deferred.resolve(module);
							});

							return deferred.promise;
						}

						moduleName = getModuleName(module);

						// Get or Set a configuration depending on what was passed in
						if(typeof module === 'string') {
							config = self.getModuleConfig(module);
							if(!config) {
								config = {
									files: [module]
								};
								moduleName = null;
							}
						} else if(typeof module === 'object') {
							config = self.setModuleConfig(module);
						}

						if(config === null) {
							errText = 'Module "' + moduleName + '" is not configured, cannot load.';
							$log.error(errText);
							deferred.reject(new Error(errText));
						} else {
							// deprecated
							if(angular.isDefined(config.template)) {
								if(angular.isUndefined(config.files)) {
									config.files = [];
								}
								if(angular.isString(config.template)) {
									config.files.push(config.template);
								} else if(angular.isArray(config.template)) {
									config.files.concat(config.template);
								}
							}
						}

						moduleCache.push = function(value) {
							if(this.indexOf(value) === -1) {
								Array.prototype.push.apply(this, arguments);
							}
						};

						// If this module has been loaded before, re-use it.
						if(angular.isDefined(moduleName) && moduleExists(moduleName) && regModules.indexOf(moduleName) !== -1) {
							moduleCache.push(moduleName);

							// if we don't want to load new files, resolve here
							if(angular.isUndefined(config.files)) {
								deferred.resolve();
								return deferred.promise;
							}
						}

						var loadDependencies = function loadDependencies(module) {
							var moduleName,
								loadedModule,
								requires,
								promisesList = [];

							moduleName = getModuleName(module);
							if(moduleName === null) {
								return $q.when();
							} else {
								try {
									loadedModule = getModule(moduleName);
								} catch(e) {
									var deferred = $q.defer();
									$log.error(e.message);
									deferred.reject(e);
									return deferred.promise;
								}
								requires = getRequires(loadedModule);
							}

							angular.forEach(requires, function(requireEntry) {
								// If no configuration is provided, try and find one from a previous load.
								// If there isn't one, bail and let the normal flow run
								if(typeof requireEntry === 'string') {
									var config = self.getModuleConfig(requireEntry);
									if(config === null) {
										moduleCache.push(requireEntry); // We don't know about this module, but something else might, so push it anyway.
										return;
									}
									requireEntry = config;
								}

								// Check if this dependency has been loaded previously or is already in the moduleCache
								if(moduleExists(requireEntry.name) || moduleCache.indexOf(requireEntry.name) !== -1) {
									if(typeof module !== 'string') {
										// The dependency exists, but it's being redefined, not inherited by a simple string reference, raise a warning and ignore the new config.
										// TODO: This could be made smarter. There's no checking here yet to determine if the configurations are actually different.
										$log.warn('Module "', moduleName, '" attempted to redefine configuration for dependency "', requireEntry.name, '"\nExisting:', self.getModuleConfig(requireEntry.name), 'Ignored:', requireEntry);
									}
									return;
								} else if(typeof requireEntry === 'object') {
									if(requireEntry.hasOwnProperty('name') && requireEntry['name']) {
										// The dependency doesn't exist in the module cache and is a new configuration, so store and push it.
										self.setModuleConfig(requireEntry);
										moduleCache.push(requireEntry['name']);
									}

									// CSS Loading Handler
									if(requireEntry.hasOwnProperty('css') && requireEntry['css'].length !== 0) {
										// Locate the document insertion point
										angular.forEach(requireEntry['css'], function(path) {
											buildElement('css', path);
										});
									}
									// CSS End.
								}

								// Check if the dependency has any files that need to be loaded. If there are, push a new promise to the promise list.
								if(requireEntry.hasOwnProperty('files') && requireEntry.files.length !== 0) {
									if(requireEntry.files) {
										promisesList.push(filesLoader(requireEntry.files).then(function() {
											return loadDependencies(requireEntry)
										}));
									}
								}
							});

							// Create a wrapper promise to watch the promise list and resolve it once everything is done.
							return $q.all(promisesList);
						}

						filesLoader(config.files).then(function success() {
							if(moduleName === null) {
								deferred.resolve(module);
							} else {
								moduleCache.push(moduleName);
								loadDependencies(moduleName).then(function success() {
									try {
										register(providers, moduleCache);
									} catch(e) {
										$log.error(e.message);
										deferred.reject(e);
										return;
									}
									$timeout(function() {
										deferred.resolve(module);
									});
								}, function error(err) {
									$timeout(function() {
										deferred.reject(err);
									});
								});
							}
						}, function error(err) {
							deferred.reject(err);
						});

						return deferred.promise;
					}
				};
			}];

			this.config = function(config) {
				jsLoader = config.jsLoader || config.asyncLoader;

				if(angular.isDefined() && !angular.isFunction(jsLoader)) {
					throw('The js loader needs to be a function');
				}

				if(angular.isDefined(config.cssLoader)) {
					cssLoader = config.cssLoader;
				}

				if(angular.isDefined(config.templatesLoader)) {
					templatesLoader = config.templatesLoader;
				}

				// for bootstrap apps, we need to define the main module name
				if(angular.isDefined(config.loadedModules)) {
					var addRegModule = function(loadedModule) {
						if(regModules.indexOf(loadedModule) < 0) {
							regModules.push(loadedModule);
							angular.forEach(angular.module(loadedModule).requires, addRegModule);
						}
					};
					angular.forEach(config.loadedModules, addRegModule);
				}

				// If we want to define modules configs
				if(angular.isDefined(config.modules)) {
					if(angular.isArray(config.modules)) {
						angular.forEach(config.modules, function(moduleConfig) {
							modules[moduleConfig.name] = moduleConfig;
						});
					} else {
						modules[config.modules.name] = config.modules;
					}
				}

				if(angular.isDefined(config.debug)) {
					debug = config.debug;
				}

				if(angular.isDefined(config.events)) {
					events = config.events;
				}
			};
		}]);

	ocLazyLoad.directive('ocLazyLoad', ['$http', '$log', '$ocLazyLoad', '$compile', '$timeout', '$templateCache', '$animate',
		function($http, $log, $ocLazyLoad, $compile, $timeout, $templateCache, $animate) {
			return {
				restrict: 'A',
				terminal: true,
				priority: 401, // 1 more than ngInclude
				transclude: 'element',
				controller: angular.noop,
				compile: function(element, attrs) {
					return function($scope, $element, $attr, ctrl, $transclude) {
						var childScope,
							evaluated = $scope.$eval($attr.ocLazyLoad),
							onloadExp = evaluated && evaluated.onload ? evaluated.onload : '';

						/**
						 * Destroy the current scope of this element and empty the html
						 */
						function clearContent() {
							if(childScope) {
								childScope.$destroy();
								childScope = null;
							}
							$element.html('');
						}

						/**
						 * Load a template from cache or url
						 * @param url
						 * @param callback
						 */
						function loadTemplate(url, callback) {
							var view;

							if(typeof(view = $templateCache.get(url)) !== 'undefined') {
								callback(view);
							} else {
								$http.get(url)
									.success(function(data) {
										$templateCache.put('view:' + url, data);
										callback(data);
									})
									.error(function(data) {
										$log.error('Error load template "' + url + '": ' + data);
									});
							}
						}

						$scope.$watch($attr.ocLazyLoad, function(moduleName) {
							if(angular.isDefined(moduleName)) {
								$ocLazyLoad.load(moduleName).then(function(moduleConfig) {
									$transclude($scope, function cloneConnectFn(clone) {
										$animate.enter(clone, null, $element);
									});
								});
							} else {
								clearContent();
							}
						}, true);
					};
				}
				/*link: function($scope, $element, $attr) {

				 }*/
			};
		}]);

	/**
	 * Get the list of required modules/services/... for this module
	 * @param module
	 * @returns {Array}
	 */
	function getRequires(module) {
		var requires = [];
		angular.forEach(module.requires, function(requireModule) {
			if(regModules.indexOf(requireModule) === -1) {
				requires.push(requireModule);
			}
		});
		return requires;
	}

	/**
	 * Check if a module exists and returns it if it does
	 * @param moduleName
	 * @returns {boolean}
	 */
	function moduleExists(moduleName) {
		try {
			return angular.module(moduleName);
		} catch(e) {
			if(/No module/.test(e) || (e.message.indexOf('$injector:nomod') > -1)) {
				return false;
			}
		}
	}

	function getModule(moduleName) {
		try {
			return angular.module(moduleName);
		} catch(e) {
			// this error message really suxx
			if(/No module/.test(e) || (e .message.indexOf('$injector:nomod') > -1)) {
				e.message = 'The module "'+moduleName+'" that you are trying to load does not exist. ' + e.message
			}
			throw e;
		}
	}

	function invokeQueue(providers, queue) {
		if(!queue) {
			return;
		}

		var i, len, args, provider;
		for(i = 0, len = queue.length; i < len; i++) {
			args = queue[i];
			if(angular.isArray(args)) {
				if(providers.hasOwnProperty(args[0])) {
					provider = providers[args[0]];
				} else {
					throw new Error('unsupported provider ' + args[0]);
				}
				if(registerInvokeList(args[2][0])) {
					provider[args[1]].apply(provider, args[2]);
				}
			}
		}
	}

	/**
	 * Register a new module and load it
	 * @param providers
	 * @param registerModules
	 * @returns {*}
	 */
	function register(providers, registerModules) {
		if(registerModules) {
			var k, moduleName, moduleFn, runBlocks = [];
			for(k = registerModules.length - 1; k >= 0; k--) {
				moduleName = registerModules[k];
				if(typeof moduleName !== 'string') {
					moduleName = getModuleName(moduleName);
				}
				if(!moduleName) {
					continue;
				}
				moduleFn = angular.module(moduleName);
				if(regModules.indexOf(moduleName) === -1) { // new module
					regModules.push(moduleName);
					register(providers, moduleFn.requires);
					runBlocks = runBlocks.concat(moduleFn._runBlocks);
				}
				invokeQueue(providers, moduleFn._invokeQueue);
				invokeQueue(providers, moduleFn._configBlocks);
				broadcast('ocLazyLoad.moduleLoaded', moduleName);
				registerModules.pop();
			}
			var instanceInjector = providers.getInstanceInjector();
			angular.forEach(runBlocks, function(fn) {
				instanceInjector.invoke(fn);
			});
		}
	}

	/**
	 * Register an invoke
	 * @param invokeList
	 * @returns {*}
	 */
	function registerInvokeList(invokeList) {
		var newInvoke = false;
		if(angular.isString(invokeList)) {
			if(regInvokes.indexOf(invokeList) === -1) {
				newInvoke = true;
				regInvokes.push(invokeList);
				broadcast('ocLazyLoad.componentLoaded', invokeList);
			}
		} else if(angular.isObject(invokeList)) {
			angular.forEach(invokeList, function(invoke) {
				if(angular.isString(invoke) && regInvokes.indexOf(invoke) === -1) {
					newInvoke = true;
					regInvokes.push(invoke);
				}
			});
		} else {
			return true;
		}
		return newInvoke;
	}

	function getModuleName(module) {
		if(module === null) {
			return null;
		}
		var moduleName = null;
		if(typeof module === 'string') {
			moduleName = module;
		} else if(typeof module === 'object' && module.hasOwnProperty('name') && typeof module.name === 'string') {
			moduleName = module.name;
		}
		return moduleName;
	}

	/**
	 * Get the list of existing registered modules
	 * @param element
	 */
	function init(element) {
		var elements = [element],
			appElement,
			module,
			names = ['ng:app', 'ng-app', 'x-ng-app', 'data-ng-app'],
			NG_APP_CLASS_REGEXP = /\sng[:\-]app(:\s*([\w\d_]+);?)?\s/;

		function append(elm) {
			return (elm && elements.push(elm));
		}

		angular.forEach(names, function(name) {
			names[name] = true;
			append(document.getElementById(name));
			name = name.replace(':', '\\:');
			if(element.querySelectorAll) {
				angular.forEach(element.querySelectorAll('.' + name), append);
				angular.forEach(element.querySelectorAll('.' + name + '\\:'), append);
				angular.forEach(element.querySelectorAll('[' + name + ']'), append);
			}
		});

		//TODO: search the script tags for angular.bootstrap
		angular.forEach(elements, function(elm) {
			if(!appElement) {
				var className = ' ' + element.className + ' ';
				var match = NG_APP_CLASS_REGEXP.exec(className);
				if(match) {
					appElement = elm;
					module = (match[2] || '').replace(/\s+/g, ',');
				} else {
					angular.forEach(elm.attributes, function(attr) {
						if(!appElement && names[attr.name]) {
							appElement = elm;
							module = attr.value;
						}
					});
				}
			}
		});

		if(appElement) {
			(function addReg(module) {
				if(regModules.indexOf(module) === -1) {
					// register existing modules
					regModules.push(module);
					var mainModule = angular.module(module);

					// register existing components (directives, services, ...)
					var queue = mainModule._invokeQueue,
						i, len, args;
					for(i = 0, len = queue.length; i < len; i++) {
						args = queue[i];
						if(angular.isArray(args)) {
							registerInvokeList(args[2][0]);
						}
					}

					// register config blocks (angular 1.3+)
					if(angular.isDefined(mainModule._configBlocks)) {
						var queue = mainModule._configBlocks,
							i, len, args;
						for(i = 0, len = queue.length; i < len; i++) {
							args = queue[i];
							if(angular.isArray(args)) {
								registerInvokeList(args[2][0]);
							}
						}
					}

					angular.forEach(mainModule.requires, addReg);
				}
			})(module);
		}
	}
})();
define([
  'angular', 
  'uiRouter',
  'ocLazyLoad'
], function (angular) {
  
  
  angular.module('oc.lazyLoad.uiRouterDecorator', ['ui.router']).config(function($stateProvider) {
    
    /**
     * module: string module id
     * files: a single file (string), or an array of files to load
     * templateUrl: view template
     * 
     * returns an object with the following keys: values
     *  - templateProvider: (function)
     *  - resolve: (array) minification-safe injection function array
     */
    function lazyBundle(module, files, templateUrl) {
      var lazyDeferred;
      return {
        templateProvider: function lazyTemplateProvider() { return lazyDeferred.promise; },
        resolve: ['$templateCache', '$ocLazyLoad', '$q', function lazyResolve($templateCache, $ocLazyLoad, $q) {
          lazyDeferred = $q.defer();
          return $ocLazyLoad.load({
            name: module, 
            files: angular.isArray(files) ? files : [files]
          }).then(function() {
            lazyDeferred.resolve(templateUrl && $templateCache.get(templateUrl));
          });
        }]
      };
    }
    
    /**
     * Add the following properties to your $stateProvider.state definition (in a top-level, or view object):
     *  lazyModule: 'app.lazy'
     *  lazyFiles: 'lazy'
     *  lazyTemplateUrl: 'lazy.html'  [optional]
     * 
     * Automatically adds to each state with lazyModule, lazyFiles, and lazyTemplateUrl properties:
     *  - a templateProvider which resolves with value $templateCache.get(lazyTemplateUrl) after all 
     *    of the lazyFiles are loaded (only if a lazyTemplateUrl property is defined)
     *  - a resolve function named $lazyLoader which resolves after all of the lazyFiles are loaded. 
     *    Before resolving, $ocLazyLoad({ name: lazyModule, files: lazyFiles }) will be called
     */
    $stateProvider.decorator('views', function ($state, parent) {
      var result = {},
          views = parent($state);
      
      angular.forEach(views, function (config, name) {
        if (config.lazyModule && config.lazyFiles && config.lazyTemplateUrl) {
          bundle = lazyBundle(config.lazyModule, config.lazyFiles, config.lazyTemplateUrl);
          config.resolve.$lazyLoader = bundle.resolve;
          if (config.lazyTemplateUrl) config.templateProvider = bundle.templateProvider;
        }
        result[name] = config;
      });
      return views;
    });
    
  });
});