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;
});
});
});