Inject module dynamically, only if required

56,944

Solution 1

I have been trying to mix requirejs+Angular for some time now. I published a little project in Github (angular-require-lazy) with my effort so far, since the scope is too large for inline code or fiddles. The project demonstrates the following points:

  • AngularJS modules are lazy loaded.
  • Directives can be lazy loaded too.
  • There is a "module" discovery and metadata mechanism (see my other pet project: require-lazy)
  • The application is split into bundles automatically (i.e. building with r.js works)

How is it done:

  • The providers (e.g. $controllerProvider, $compileProvider) are captured from a config function (technique I first saw in angularjs-requirejs-lazy-controllers).
  • After bootstraping, angular is replaced by our own wrapper that can handle lazy loaded modules.
  • The injector is captured and provided as a promise.
  • AMD modules can be converted to Angular modules.

This implementation satisfies your needs: it can lazy-load Angular modules (at least the ng-grid I am using), is definitely hackish :) and does not modify external libraries.

Comments/opinions are more than welcome.


(EDIT) The differentiation of this solution from others is that it does not do dynamic require() calls, thus can be built with r.js (and my require-lazy project). Other than that the ideas are more or less convergent across the various solutions.

Good luck to all!

Solution 2

Attention: use the solution by Nikos Paraskevopoulos, as it's more reliable (I'm using it), and has way more examples.


Okay, I have finally found out how to achieve this with a brief help with this answer.

As I said in my question, this has come to be a very hacky way. It envolves applying each function in the _invokeQueue array of the depended module in the context of the app module.

It's something like this (pay more attention in the moduleExtender function please):

define([ "angular" ], function( angular ) {
    // Returns a angular module, searching for its name, if it's a string
    function get( name ) {
        if ( typeof name === "string" ) {
            return angular.module( name );
        }

        return name;
    };

    var moduleExtender = function( sourceModule ) {
        var modules = Array.prototype.slice.call( arguments );

        // Take sourceModule out of the array
        modules.shift();

        // Parse the source module
        sourceModule = get( sourceModule );
        if ( !sourceModule._amdDecorated ) {
            throw new Error( "Can't extend a module which hasn't been decorated." );
        }

        // Merge all modules into the source module
        modules.forEach(function( module ) {
            module = get( module );
            module._invokeQueue.reverse().forEach(function( call ) {
                // call is in format [ provider, function, args ]
                var provider = sourceModule._lazyProviders[ call[ 0 ] ];

                // Same as for example $controllerProvider.register("Ctrl", function() { ... })
                provider && provider[ call[ 1 ] ].apply( provider, call[ 2 ] );
            });
        });
    };

    var moduleDecorator = function( module ) {
        module = get( module );
        module.extend = moduleExtender.bind( null, module );

        // Add config to decorate with lazy providers
        module.config([
            "$compileProvider",
            "$controllerProvider",
            "$filterProvider",
            "$provide",
            function( $compileProvider, $controllerProvider, $filterProvider, $provide ) {
                module._lazyProviders = {
                    $compileProvider: $compileProvider,
                    $controllerProvider: $controllerProvider,
                    $filterProvider: $filterProvider,
                    $provide: $provide
                };

                module.lazy = {
                    // ...controller, directive, etc, all functions to define something in angular are here, just like the project mentioned in the question
                };
                module._amdDecorated = true;
            }
        ]);
    };

    // Tadaaa, all done!
    return {
        decorate: moduleDecorator
    };
});

After this has been done, I just need, for example, to do this:

app.extend( "ui.codemirror" ); // ui.codemirror module will now be available in my application
app.controller( "FirstController", [ ..., function() { });

Solution 3

The key to this is that any modules your app module depends on also needs to be a lazy loading module as well. This is because the provider and instance caches that angular uses for its $injector service are private and they do not expose a method to register new modules after initialization is completed.

So the 'hacky' way to do this would be to edit each of the modules you wish to lazy load to require a lazy loading module object (In the example you linked, the module is located in the file 'appModules.js'), then edit each of the controller, directive, factory etc calls to use app.lazy.{same call} instead.

After that, you can continue to follow the sample project you've linked to by looking at how app routes are lazily loaded (the 'appRoutes.js' file shows how to do this).

Not too sure if this helps, but good luck.

Solution 4

There is a directive that will do this:

https://github.com/AndyGrom/loadOnDemand

example:

<div load-on-demand="'module_name'"></div>
Share:
56,944

Related videos on Youtube

gustavohenke
Author by

gustavohenke

Developer and entrepreneur of the web. I love to invest my time, knowledge and forces to help build a more open, innovative, functional and liberal web. You'll likely find me on javascript related tags, but I obviously love html5 and css3 as well!

Updated on July 09, 2022

Comments

  • gustavohenke
    gustavohenke almost 2 years

    I'm using Require.js in combination with Angular.js.

    Some controllers need huge external dependencies which others don't need, for example, FirstController requires Angular UI Codemirror. That's a extra 135 kb, at least:

    require([
      "angular",
      "angular.ui.codemirror" // requires codemirror itself
    ], function(angular) {
      angular.module("app", [ ..., "ui.codemirror" ]).controller("FirstController", [ ... ]);
    });
    

    I don't want to have to include the directive and the Codemirror lib everytime my page loads just to make Angular happy.
    That's why I'm right now loading the controller only when the route is hit, like what's done here.

    However, when I need something like

    define([
      "app",
      "angular.ui.codemirror"
    ], function(app) {
      // ui-codemirror directive MUST be available to the view of this controller as of now
      app.lazy.controller("FirstController", [
        "$scope",
        function($scope) {
          // ...
        }
      ]);
    });
    

    How can I tell Angular to inject ui.codemirror module (or any other module) in the app module aswell?
    I don't care if it's a hackish way to accomplish this, unless it involves modifying the code of external dependencies.

    If it's useful: I'm running Angular 1.2.0.

  • gustavohenke
    gustavohenke over 10 years
    Yep, this helps in some way. Thanks.
  • gustavohenke
    gustavohenke over 10 years
    Although I prefer the way I solved this problem (see answer), yours is also very good. Thanks.
  • gustavohenke
    gustavohenke over 10 years
    Great solution bro -- I'll study it the soon I have time. Thanks!
  • gustavohenke
    gustavohenke over 10 years
    Do you have plans to make Angular + Require.js testable via E2E?
  • Nikos Paraskevopoulos
    Nikos Paraskevopoulos over 10 years
    I will definitely make it testable some way. To be honest, I haven't tried Angular E2E testing yet.
  • gustavohenke
    gustavohenke over 10 years
    Let me know when you have done it testable, I'll be waiting :)
  • Freewind
    Freewind over 9 years
    Could you give an example what's inside module.lazy = { ... }?
  • gustavohenke
    gustavohenke over 9 years
    Go with the solution by Nikos. I'm no longer using this.
  • Freewind
    Freewind over 9 years
    Nikos' project is too comples, I can't get the idea :(
  • Nikos Paraskevopoulos
    Nikos Paraskevopoulos over 9 years
    Hi, I did some more updates, including testing with Karma. Code coverage support and detailed documentation is on the way!
  • Admin
    Admin over 9 years
    Could you say, can your solution dynamically unload modules? For example if I need some module for some page, and when that page was closed I want to clear loaded module in order to free the memory. Maybe you could advice something...
  • Nikos Paraskevopoulos
    Nikos Paraskevopoulos over 9 years
    @PashaTurok Unfortunately no, I have no suggestions on this (and it is a very interesting topic, as client side applications grow bigger).