Loading AngularJS Components With RequireJS After Application Bootstrap
Yesterday, I looked at loading AngularJS components after your AngularJS application has been bootstrapped. In that experiment I simply waited until DOM-Ready before I registered said components. That didn't really make them lazy-loaded, it only demonstrated that the components could be registered post-bootstrap. Today, I wanted to play around with using RequireJS in order to make the components truly lazy-loaded.
View this demo in my JavaScript-Demos project on GitHub.
When I think about a cohesive set of AngularJS components, I think about two things: the JavaScript parts and the HTML parts. In order for RequireJS to lazy-load some aspect of our AngularJS application, it will have to load both the JavaScript objects and the HTML templates. In this demo, I'm using the RequireJS plugin - text.js - to load said templates.
Because RequireJS is asynchronous in nature, the code that lazy-loads the AngularJS components must also be asynchronous. This means that it must operate within some callback-based workflow. Since AngularJS already has $q - a Deferred / Promise library - I decided to encapsulate the RequireJS behavior behind a $q promise. This has the added benefit that multiple sources can listen for the resolution of the lazy-loaded components.
Simply loading and executing the "lazy" JavaScript file will register the JavaScript components. The HTML templates, on the other hand, require a little bit more work; once they have been loaded by RequireJS, the Script tags (type="text/ng-template") have to be explicitly injected into the AngularJS template cache.
That said, the following demo is yesterday's demo, refactored to use RequireJS:
<!doctype html>
<html ng-app="Demo">
<head>
<meta charset="utf-8" />
<title>
Loading AngularJS Components With RequireJS After Application Bootstrap
</title>
<style type="text/css">
a[ ng-click ] {
cursor: pointer ;
user-select: none ;
-webkit-user-select: none ;
-moz-user-select: none ;
-ms-user-select: none ;
-o-user-select: none ;
text-decoration: underline ;
}
</style>
</head>
<body ng-controller="AppController">
<h1>
Loading AngularJS Components With RequireJS After Application Bootstrap
</h1>
<p>
<a ng-click="toggleSubview()">Toggle Subviews</a>
</p>
<!--
The "Before" subview doesn't need any additional assets;
however, the "After" subview relies on a number of assets
that will be loaded after the AngularJS application has been
bootstrapped.
-->
<div ng-switch="subview">
<div ng-switch-when="before">
<p>
Before app bootstrap.
</p>
</div>
<div ng-switch-when="after" ng-include=" 'after.htm' ">
<!-- To be poprulated with the Lazy module content. -->
</div>
</div>
<!-- Load jQuery and AngularJS. -->
<script type="text/javascript" src="../../vendor/jquery/jquery-2.0.3.min.js"></script>
<script type="text/javascript" src="../../vendor/require/require-2.1.9.min.js"></script>
<script type="text/javascript" src="../../vendor/angularjs/angular-1.0.7.min.js"></script>
<script type="text/javascript">
// Create an application module for our demo.
var app = angular.module( "Demo", [] );
// -------------------------------------------------- //
// -------------------------------------------------- //
// After the AngularJS has been bootstrapped, you can no longer
// use the normal module methods (ex, app.controller) to add
// components to the dependency-injection container. Instead,
// you have to use the relevant providers. Since those are only
// available during the config() method at initialization time,
// we have to keep a reference to them.
// --
// NOTE: This general idea is based on excellent article by
// Ifeanyi Isitor: http://ify.io/lazy-loading-in-angularjs/
app.config(
function( $controllerProvider, $provide, $compileProvider ) {
// Let's keep the older references.
app._controller = app.controller;
app._service = app.service;
app._factory = app.factory;
app._value = app.value;
app._directive = app.directive;
// Provider-based controller.
app.controller = function( name, constructor ) {
$controllerProvider.register( name, constructor );
return( this );
};
// Provider-based service.
app.service = function( name, constructor ) {
$provide.service( name, constructor );
return( this );
};
// Provider-based factory.
app.factory = function( name, factory ) {
$provide.factory( name, factory );
return( this );
};
// Provider-based value.
app.value = function( name, value ) {
$provide.value( name, value );
return( this );
};
// Provider-based directive.
app.directive = function( name, factory ) {
$compileProvider.directive( name, factory );
return( this );
};
// NOTE: You can do the same thing with the "filter"
// and the "$filterProvider"; but, I don't really use
// custom filters.
}
);
// -------------------------------------------------- //
// -------------------------------------------------- //
// I control the root of the application.
app.controller(
"AppController",
function( $scope, withLazyModule ) {
// I determine which view is rendered.
$scope.subview = "before";
// ---
// PUBLIC METHODS.
// ---
// I toggle between the two different subviews.
$scope.toggleSubview = function() {
if ( $scope.subview === "before" ) {
// Once the "lazy" module has been loaded,
// then show the corresponding view.
withLazyModule(
function() {
$scope.subview = "after";
}
);
// The lazy-load of the module returns a
// promise. This is here just to demonstrate
// that multiple bindings can listen for the
// resolution or rejection of the lazy module.
withLazyModule().then(
function() {
console.log( "Lazy module loaded." );
}
);
} else {
$scope.subview = "before";
}
}
}
);
// -------------------------------------------------- //
// -------------------------------------------------- //
// I load the "Lazy" module and resolve the returned Promise
// when the components and the relevant templates have been
// loaded.
app.factory(
"withLazyModule",
function( $rootScope, $templateCache, $q ) {
var deferred = $q.defer();
var promise = null;
function loadModule( successCallback, errorCallback ) {
successCallback = ( successCallback || angular.noop );
errorCallback = ( errorCallback || angular.noop );
// If the module has already been loaded then
// simply bind the handlers to the existing promise.
// No need to try and load the files again.
if ( promise ) {
return(
promise.then( successCallback, errorCallback )
);
}
promise = deferred.promise;
// Wire the callbacks into the deferred outcome.
promise.then( successCallback, errorCallback );
// Load the module templates and components.
// --
// The first dependency here is an HTML file which
// is loaded using the text! plugin. This will pass
// the value through as an HTML string.
require(
[
"../../vendor/require/text!lazy.htm",
"lazy.js"
],
function requrieSuccess( templatesHtml ) {
// Fill the template cache. The file content
// is expected to be a list of top level
// Script tags.
$( templatesHtml ).each(
function() {
var template = $( this );
var id = template.attr( "id" );
var content = template.html();
$templateCache.put( id, content );
}
);
// Module loaded, resolve deferred.
$rootScope.$apply(
function() {
deferred.resolve();
}
);
},
function requireError( error ) {
// Module load failed, reject deferred.
$rootScope.$apply(
function() {
deferred.reject( error );
}
);
}
);
return( promise );
}
return( loadModule );
}
);
</script>
</body>
</html>
As you can see, the controller that wants to make use of the lazy-loaded module must use the "withLazyModule()" function. This function returns a promise; however, it also accepts callbacks which will be automatically wired into the underlying deferred resolution. When the promise has been resolved, the lazy-loaded components have been injected into the AngularJS application and are ready to be consumed.
The two files that are loaded via RequireJS are the HTML templates:
<!--
This templates collection is intended to be a top-level list of
Script-tag based templates used to populate the AngularJS template
cache. Each of the script tags must have type[text/ng-template]
and an ID that matches the requested URL.
-->
<script type="text/ng-template" id="after.htm">
<p ng-controller="LazyController" bn-italics>
{{ message }}
</p>
</script>
... and the JavaScript components:
// This component collection is intended to be all the controllers,
// services, factories, and directives (etc) that are required to
// operate the "Lazy" module.
// --
// NOTE: We are not actually creating a "module" (as in angular.module)
// since that would not work after bootstrapping.
// Lazy-loaded controller.
app.controller(
"LazyController",
function( $scope, uppercase, util ) {
$scope.message = util.emphasize(
uppercase( "After app bootstrap." )
);
}
);
// Lazy-loaded service.
app.service(
"util",
function( emphasize ) {
this.emphasize = emphasize;
}
);
// Lazy-loaded factory.
app.factory(
"emphasize",
function() {
return(
function( value ) {
return( value.replace( /\.$/, "!!!!" ) );
}
);
}
);
// Lazy-loaded value.
app.value(
"uppercase",
function( value ) {
return( value.toString().toUpperCase() );
}
);
// Lazy-loaded directive.
app.directive(
"bnItalics",
function() {
return(
function( $scope, element ) {
element.css( "font-style", "italic" );
}
);
}
);
As you can see, the lazy-loaded components are registered in the same way that any of your AngularJS components are registered. We didn't use a "module" here (as in angular.module()), since the new module wouldn't populate the correct dependency injection container.
There's so much more to think about here, but this is pretty exciting! I love the idea of being able to lazy-load parts of your AngularJS application. It definitely requires some more thinking and more organization; but, it could really be great for load-times.
Want to use code from this post? Check out the license.
Reader Comments
Here's an article about something similar, using RequireJS to load controllers and their corresponding templates fmor angulr routes:
http://weblogs.asp.net/dwahlin/archive/2013/05/22/dynamically-loading-controllers-and-views-with-angularjs-and-requirejs.aspx
Often when I search for a key/important angular topic which I want to incorporate into my work, I come across this blog which touches up on many interesting and useful techniques and ideas in angular, unfortunately as soon as I open the page and glance through those aesthetically excruciating horrid code snippets with its formatting and indentation (or rather the lack of) I instantly press the back button in my browser. I'm not trying be funny or rude, but it seems such a shame to sped so much time and make such a great effort to write about all these great things you discover, not to mention your admirable willingness to share these discoveries with the community, only to have it ruined by such an elementary issue.
@Mike,
Excellent link. I've only done a bit of dabbling with the AngularJS / RequireJS stuff, so it's really helpful to see how other people are approaching it. The basic "plumbing" is very similar (since there's really only one way to expose the post-load "define" methods). But, it's interesting how he defines various components as actual AngularJS modules.
I definitely need to play around more with this kind of stuff.
@Mo,
I am not sure that I understand what you are saying? I am extremely regimented about my formatting and my indentation. If you are not seeing any indentation or formatting, then the GitHub CSS (I render my code sample's using GitHub Gists) may not be coming through properly for you.
If you are seeing formatting and whitespace, but just don't agree with my style of formatting (ie, too much white-space), then that's another story. If that's the case, I can assure you that you are not the first person to voice such concerns. Unfortunately, that's just the way that I write code.
Granted, when I write code in "production", it's typically wider (~90-100 characters). I keep it narrower here so that you don't have to scroll the code-samples horizontally (something that I personally don't enjoy doing when I read). Keeping it narrower for the blog does require me to make line-returns more than I would. But, for the most part, this is just how I write code. And, it's how I think about code.
When I look at code that is too tightly-packed, I find it hard to concentrate.
To each their own!
This looks very nice and clean, but what about AngularJS routing? Do you have an example of such combination (Angular + Routing + Require)? And in such situation should all routes be predefined at application bootstrap or can we also add new routes when modules are lazy loaded and they register additional routes related to their functionality?
@Robert,
Super interesting question. I had never considered lazy-loading actual route definitions. That might be a bit much for my brain to handle. The situation that immediately pops to mind is, what happens if you are in a deep-route that should be lazy-loaded, and then the user refreshes their page. At that point, there is not navigation change, but the app still needs to know how to route the request AND to lazy-load part of the internal wiring.
I'd have to noodle on that concept for a while :)
Hi, here is the approach I use : http://codrspace.com/thaiat/angularjs-requirejs/
let me know what u think...
Since RequireJS is going to be added to AngularJS and supported from core, I'm excited to know how the AngularJS team are going to hook everything up.
More details on RequireJS addition to AngularJS in my post: http://leog.me/log/making-sense-of-requirejs-with-angularjs/
Nice Article. Thanks for posting. If possible, I would like to see like link on the blog to see how many users are benefited from your post.
Please please stop adding so many empty lines to your code. I find that with all the lines your code is almost twice as long as it could be without the empty lines. I'd love to read and learn from your code but you are making it difficult for us. Please try checking it out yourself. You are one of the few people that does this and it's frustrating :-(
console.log( "Lazy module loaded." );
}
);
} else {
$scope.subview = "before";
}
}
}
What are you thinking with all the empty line ?
:-(
Just to add a bit of balance to the code formatting thing I found the code extremely well laid out and easy to read. White space is great for delineating code sections and logical grouping of concerns. It's a little unusual to see it padding out the if blocks and function blocks, but it doesn't hurt the readability and it gives the code some personal style.
On the subject this is a great technique and elegant implementation.
I agree with @Robert it would be great to consider how to combine with routing. Any chance you'd do a follow up to extend this code into routing as well? I think the ultimate would be to not have to custom code anything except in the LazyModuleRouteProvider, where you would be adding a parameter for whether to preload the route or lazy load it, and a list of the modules each route depends on. The lazy loader would then watch for route changes and dynamically load modules, controllers and templates as required.
Also with requirejs you could have just a single JS define() file for the lazy module, which would be its module definition, and that file could refer to the other files it needs such as template HTML files and internal JS files which would allow large modules to keep their concerns in multiple files. The minifier could also be run on this define() file and produce a single file including the JS and templates in one file for production.
> We didn't use a "module" here (as in angular.module()), since the new module wouldn't populate the correct dependency injection container.
So basically you're saying that with all the approaches we see using AMD or some other kind of dynamic loading we cannot use `angular.module()`? Do you potentially see a problem with that?
To be honest, I didn't yet grasp the real value of using angular modules. It's like defining a namespace but still they don't prevent you from having name clashes...
Hi, I am trying to unit test lazy loadable component using karma but unable to load lazy provider . app.config never executes properly. Please let me know If something is missing ?
Can we do this without click, i.e. on scroll? An example would me much helpful.
this is the worst tutorial ever!
@Manoj,
Hi Manoj,
I recently encounter the same issue.
Any luck with solving this ?