2

I'm struggling to get a custom provider to work with dependencies injected into it. I'm following this blog and here is my latest version of the provider in my attempts to get it working.

define([
    'angular',
    'ngRoute',
    'require'
], function(angular) {
    return angular.module('pluggableViews', ['ngRoute'])
        .provider('$pluggableViews', function() {
                var providers = {};
                var $injector = angular.injector(['ng']);
                this.views = [];

                this.registerModule = function(moduleName) {
                    console.log(moduleName);
                    var module = angular.module(moduleName);

                    if (module.requires) {
                        for (var i = 0; i < module.requires.length; i++) {
                            this.registerModule(module.requires[i]);
                        }
                    }

                    angular.forEach(module._invokeQueue, function(invokeArgs) {
                        var provider = providers[invokeArgs[0]];
                        provider[invokeArgs[1]].apply(provider, invokeArgs[2]);
                    });
                    angular.forEach(module._configBlocks, function(fn) {
                        $injector.invoke(fn);
                    });
                    angular.forEach(module._runBlocks, function(fn) {
                        $injector.invoke(fn);
                    });
                };

                this.toTitleCase = function(str) {
                    return str.replace(/\w\S*/g, function(txt) {
                        return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
                    });
                };

                this.registerView = function(viewConfig) {

                    if (!viewConfig.viewUrl) {
                        viewConfig.viewUrl = '/' + viewConfig.ID;
                    }
                    if (!viewConfig.templateUrl) {
                        viewConfig.templateUrl = 'views/' + viewConfig.ID + '/' + viewConfig.ID + '.html';
                    }
                    if (!viewConfig.controller) {
                        viewConfig.controller = this.toTitleCase(viewConfig.ID) + 'Controller';
                    }
                    if (!viewConfig.navigationText) {
                        viewConfig.navigationText = this.toTitleCase(viewConfig.ID);
                    }
                    if (!viewConfig.requirejsName) {
                        viewConfig.requirejsName = viewConfig.ID;
                    }
                    if (!viewConfig.moduleName) {
                        viewConfig.moduleName = viewConfig.ID;
                    }
                    if (!viewConfig.cssId) {
                        viewConfig.cssId = viewConfig.ID + "-css";
                    }
                    if (!viewConfig.cssUrl) {
                        viewConfig.cssUrl = 'views/' + viewConfig.ID + '/' + viewConfig.ID + '.css';
                    }

                    this.views.push(viewConfig);

                    $route.when(viewConfig.viewUrl, {
                        templateUrl: viewConfig.templateUrl,
                        controller: viewConfig.controller,
                        resolve: {
                            resolver: ['$q', '$timeout', function($q, $timeout) {

                                var deferred = $q.defer();
                                if (angular.element("#" + viewConfig.cssId).length === 0) {
                                    var link = document.createElement('link');
                                    link.id = viewConfig.cssId;
                                    link.rel = "stylesheet";
                                    link.type = "text/css";
                                    link.href = viewConfig.cssUrl;
                                    angular.element('head').append(link);
                                }
                                if (viewConfig.requirejsConfig) {
                                    require.config(viewConfig.requirejsConfig);
                                }
                                require([viewConfig.requirejsName], function() {
                                    this.registerModule(viewConfig.moduleName);
                                    $timeout(function() {
                                        deferred.resolve();
                                    });
                                });
                                return deferred.promise;
                            }]
                        }
                    });

                };

                this.$get = [
                    '$controller',
                    '$compile',
                    '$filter',
                    // '$provide',
                    // '$injector',
                    '$route',
                    function(
                        $controller,
                        $compile,
                        $filter,
                        //$provide,
                        //$injector,
                        $route
                    ) {
                        providers.$controller = $controller;
                        providers.$compile = $compile;
                        providers.$filter = $filter;
                        providers.$route = $route;
                    }];
            });
});

And here is configuring the provider:

define([
    'angular',
    'ngRoute',
    'views/nav/nav',
    'scripts/providers/pluggableViews'
], function (angular) {
    var app = angular.module('app', ['ngRoute', 'pluggableViews', 'app.nav']);

    app.directive('navbar', function () {
        return {
            restrict: 'E',
            templateUrl: '../views/nav/nav.html'
        };
    });


    app.config([
        '$routeProvider', 
        '$locationProvider', 
        '$pluggableViewsProvider'
    ], function($routeProvider, $locationProvider, $pluggableViewsProvider){
        $pluggableViewsProvider.registerView({
            ID: 'home',
            moduleName: 'app.home',
            requirejsConfig: {paths: {'home': 'views/home/home'}},
            viewUrl: '/'
        });
    }]);

    return app;
});

So far I've determined that the original article was incorrectly injecting dependencies into the .provider() instead of the $get function. I've attempted to correct this but I am still getting a "Error: [$injector:modulerr]" for my "app" module when I inject the provider. If I remove the provider from my "app" config the error goes away. So I have determined it is in fact my provider that is in error.

Update

After more debugging and isolating code. I've updated the code above to reflect my new discoveries that injected providers should leave off the "Provider" at the end of their name. I've also discovered that $injector and $provide services are causing errors. Can you not inject these services into a provider? It seems right now things are erroring when my app tries to call the registerView function. I believe the $route.resolve isn't resolving correctly.

Nikordaris
  • 2,297
  • 5
  • 20
  • 30

2 Answers2

2

The original problem results from the fact that there are two injectors per application instance, belonging to 'config' and 'run' phases respectively. The one is for service provider (which is defined by provider), other service providers can be injected there (i.e. $controllerProvider). The other one is for service instance (its factory function is defined by factory or provider's $get), only service instances can be injected there (i.e. $controller).

For lazy controller registration in service instance it would be

app.provider('...', function($controllerProvider) {
  this.$get = function ($controller) {
    $controllerProvider.register('LazyController', ...);
    var lazyControllerInstance = $controller('LazyController', ...);
  };
})

The similar trick can be performed to define new routes lazily with $routeProvider and new directives with $compileProvider.

The thing you're trying to approach is not possible in Angular, on the other hand.

Once the app was bootstrapped and config phase has begun, new Angular modules can't be defined (technically they can, but they cannot be used within this app instance). Every module that has to be used within the app, has to be loaded when the module is defined. These can be dummy modules but they have to exist to be used, e.g.

angular.module('dummy', []);

angular.module('app', ['dummy'])
  .config(($provide, $controllerProvider) => {
    // these ones are necessary to register new items after config phase
    $provide.value('$controllerProvider', $controllerProvider);
    $provide.value('$provide', $provide);
  })
  .run(() => {
    require(['dummy'], ...);
  });

// dummy.js

angular.module('dummy').run(($provide, $controllerProvider) => {
  $provide.factory('lazy', ...)
  $controllerProvider.register('LazyController', ...);
});
Community
  • 1
  • 1
Estus Flask
  • 206,104
  • 70
  • 425
  • 565
  • Thanks for the detailed explanation. I'll have to look into this again some time when I have the time but as it is I've just discovered a module that will do what I was trying to accomplish for me. I'll answer my own question with more details on what I ended up doing. – Nikordaris Jan 20 '16 at 20:43
  • @Nick AngularAMD essentially does the same thing as described, but it patches `angular.module` (unlike ocLazyLoad). Which may result in broken code even for synchronous modules and still requires a good amount of understanding what's happening under Angular's hood. Asynchronous Angular modules can be rarely justified, bundled modules are almost always more performant and always easier to maintain. – Estus Flask Jan 20 '16 at 22:01
  • Ya, I was concerned that was what AngularAMD was doing under the hood. I may have to go back to doing it the way you suggested later on. As for now I'm pretty burned out on this part of the code and so I'll try to revisit this in a few weeks. I'll probably have follow on questions. I'll leave this question unresolved until I can look at your answer in more detail. – Nikordaris Jan 21 '16 at 13:25
0

After more research into trying to solve this problem I discovered a library that will solve the dynamic registration problem for me. AngularAMD works with AMDefine/RequireJS to enable dynamic registration of angular modules. It solves what I was trying to accomplish with this post (with less code from me!). Since this took me so long to figure it all out I went ahead and created a template for all the foundational stuff I was trying to do with my web app so others can see how I ended up doing it. You can find it here.

Update

@estus raised some valid concerns with AngularAMD. I'm going to have to look into this further.

Nikordaris
  • 2,297
  • 5
  • 20
  • 30