Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at BFusion / BFLEX 2009 (Bloomington, Indiana) with: Simon Free

Loading AngularJS Components With RequireJS After Application Bootstrap

By Ben Nadel on

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.




Reader Comments

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.

Reply to this Comment

@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.

Reply to this Comment

@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!

Reply to this Comment

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?

Reply to this Comment

@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 :)

Reply to this Comment

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/

Reply to this Comment

Post A Comment

?
You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.