Dynamic loading of AngularJS components

Last week, I had the opportunity to speak about dynamic loading AngularJS modules and how can you achieve that using Webpack’s require.ensure method. The code and slides for the presentation can be found here. In the presentation I’ve only described how can you dynamically register router states. The basic ideea is to have some sort of mechanism to load application modules on demand and once they’re loaded, to register those new angular components in the main application in order to use them. Unfortunately, Angular register’s all of it’s components in the configuration phase so if you try to register a new directive or service after the configuration phase has ended, the component won’t be available for Angular to use.

Let’s take this simple example:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <!-- Include your required JS files here -->
        <!-- I'm using Webpack so everything is automatically included for me -->
        <title>Angular modular test app</title>
    </head>
<body>
    <!-- Use our application directive -->
    <app></app>
</body>
</html>
/**
* Create a test app.
*/
var app = angular.module('test', []);

/**
* Create an application directive
* I'm using the new Angular 1.5 syntax for components.
*/
app.component('app', {
    controller: function(){
        this.showNewDirective = false;

        // We only show the new directive after a button press.
        this.loadDirective = function(){
        this.showNewDirective = true;
        };
    },
    // In the directive template we are using an undefined directive "new-directive".
    template: '<button ng-click="$ctrl.loadDirective()" ng-if="!$ctrl.showNewDirective">Load new directive</button>
    ' +
    '<new-directive ng-if="$ctrl.showNewDirective">New directive not loaded</new-directive>'
});

The application looks like this:
Screen Shot 2016-02-29 at 12.25.57 PM
Once we press the button we will see the default “New directive not loaded” text because our “<new-directive>” was not yet defined.
Screen Shot 2016-02-29 at 12.25.45 PM

Now let’s modify our code to register the “<new-directive>” when we press the button.

app.component('app', {
    controller: function () {
        this.showNewDirective = false;

        // We only show the new directive after a button press.
        this.loadDirective = function () {
            // Register the new directive before showing it
            app.component('newDirective', {
                controller: function () {

                },
                template: '
<h1>New directive is here</h1>
'
                });
            this.showNewDirective = true;
        };
    },
    // In the directive template we are using an undefined directive "new-directive".
    template: '<button ng-click="$ctrl.loadDirective()" ng-if="!$ctrl.showNewDirective">Load new directive</button>
    ' +
    '<new-directive ng-if="$ctrl.showNewDirective">New directive not loaded</new-directive>'
});

Once you click the button again, you would expect that the new directive is showed because we registered it before we showed it. The actual result is this:

Screen Shot 2016-02-29 at 12.25.45 PM

This happens because once the config phase has ended, angular’s component method doesn’t use the same $compileProvider to register new components. With just this little piece of code added to the “App.js” file:

app.config(function ($compileProvider) {
    // Save the original $compileProvider to use it for dynamic registering
    app.component = function(name, object) {
        $compileProvider.component(name, object);
    return (this);
    };
});

If we press the button now we should see this:
Screen Shot 2016-02-29 at 3.44.58 PM

If we want this to work for all Angular’s components we need to change our config method into this:

app.config(function ($controllerProvider, $provide, $compileProvider, $filterProvider) {
    // Register directives handler
    app.component = function(name, object) {
        $compileProvider.component(name, object);
    return (this);
    };
    // Register controller handler
    app.controller = function( name, constructor ) {
        $controllerProvider.register( name, constructor );
    return( this );
    };
    // Register services handlers
    app.service = function( name, constructor ) {
        $provide.service( name, constructor );
    return( this );
    };
    app.factory = function( name, factory ) {
        $provide.factory( name, factory );
    return( this );
    };
    // Register filters handler
    app.filter = function( name, factory ) {
        $filterProvider.register( name, factory );
    return( this );
    };
});

Now you have a way to lazy load angular modules. Please don’t make your users download 5MB of javascript if they’re only going to use 2MB. Application modules like Admin, Settings, Profile, etc. should be loaded on demand.

One comment on “Dynamic loading of AngularJS components

  1. Rick says:

    This is interesting but in your example the user is still download everything. A better example would have been to dynamically get the javascript/html file from the server from within another component instead of putting the code for the component directly inside a component itself. How does one go about dynamically getting the js/html from the server for a component at the time it’s needed?

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s