marcoslin/angularAMD

Ability to have templateUrl request happen after controllerUrl resolution

Opened this issue · 11 comments

I'm nearing the end of creating a code structure and build process for my Angular app using Angular AMD to lazy load sub-programs within a main app, and it's all being managed via requireJS dependencies.

|-main-app
|-programs
  |--program1
  |--program2

When I'm running in dev mode, everything is now working fine, and program1/program2 and so on are only loaded when I navigate to their states via code that is built at runtime (because this list of programs is dynamic):

 var state =  {
   url:  prog,  
   authenticate: item.authenticate,
   views: {
      "program@":angularAMD.route({
         templateUrl: "programs/" + prog + "/templates/" + prog + ".html", 
         controllerUrl: "programs/" + prog + "/main.js",
         controller: prog+"Ctrl"
   })
   }
 };

All good in dev.

Now, when I build my application via gulp using the requirejs optimizer and various plugins, I'm building all my templates into my code via the gulp-angular-templatecache. Within the main app, this works fine. I insert the generated template code into a module, which in dev doesn't do much other than be defined:

define(["angular"],function(angular){
    console.log("Template Module");
    var templates = angular.module("templates",[]);

    <!-- inject:templates -->
    <!-- Contents of html templates will be injected here via gulp task-->
    <!-- endinject -->

    return templates;
});

But for each of my programs, I don't want them touching that, they can just have their template insertion code be appended to their concatenated main file, because they can know the module already exists:

angular.module("templates").run(["$templateCache", function($templateCache) {$templateCache.put("programs/programs1/templates/edit.html" ... and so on...

Which is all fine. Except for the fact that when I navigate to a state defined by one of these programs, the controllerUrl is placed within a resolve statement like I want, and so is only loaded once I try to go to it... that controller file is actually a file that defines the module and everything to do with that program (and also the controller as defined by prog+"Ctrl")... this file also has those template cache insertions.

However the templateUrl is resolved before any of that loads in, and so the templateCache has not been populated, and it tries to load from the actual URL, which doesn't exist in production.

Sooo, by way of a long example, is there a way for the templateUrl to only be resolved ONCE the controllerUrl is resolved and loaded?

TL;DR: Can I only load the templateUrl after the controllerUrl code has loaded?

Sorry, not sure if I fully understand. Basically, you turn all your view files into a single templates module so in prod there is not more HTML files? If that is the case, isn't this about loading the templates module on startup so all templates are available when your controller loads?

Yes, that would be the case for a system where the 'programs' weren't dynamic, and quite likely never loaded by a given client. So, you have the main app. And it's states do indeed have its templates loaded at startup.

The problem is, all the programs. We might have hundreds of these, and a given user might only use 2 in a session, so we only want to load all of their templates ahead of time within the main app. Also, that would make the main app be affected by changes to the programs, which is also bad for change management.

So, what I'm trying to do is have the templates be inserted into the template cache when the program is routed to (when it's state is activated).

The whole system works fine if I leave the templates as html files per program, they only get requested on loading of the state... it's just my wish to have them in the cache instead of that.

I think I am starting to better understand your setup. Isn't this as simple as setting your program template module as a dependency to your controller?

Paraphrasing your example:

views: {
      "programA":angularAMD.route({
         templateUrl: "programs/" + prog + "/templates/" + prog + ".html", 
         controllerUrl: "programs/" + prog + "/main.js",
         controller: prog+"Ctrl"
     })
}

I am assuming that you would have something like:

define(["angular"],function(angular){
    console.log("Template Module");
    var templates = angular.module("templates_programA",[]);

    <!-- inject:templates -->
    <!-- Contents of html templates will be injected here via gulp task-->
    <!-- endinject -->

    return templates;
});

You would do in your main.js of your programA:

define(['app', 'templates_programA'], function (app) {
    ....
})

If this is not your setup, can you elaborate on how do you define program specific template? You example so far shows me only one generic templates module.

Hi again, Thanks for taking the time to reply.

Your suggestion gave me an idea as to having insertion points per each of my programs for the template code, so that's neatened things up a bit, which is excellent, thanks.

However I'm still stuck with the order of execution problem.

My console log like this when running the distribution build:

$stateChangeStart FROM:app TO: app.staff: Object {name: "$stateChangeStart", targetScope: Object, defaultPrevented: false, currentScope: Object}
app.js:28294 RESOLVING [programs/program1/main.js]
app.js:10413 GET http://localhost:8080/dist/programs/program1/templates/program1.html 404 (Not Found)(anonymous function) @ app.js:10413sendReq @ app.js:10232$get.serverRequest @ app.js:9944processQueue @ app.js:14454(anonymous function) @ app.js:14470$get.Scope.$eval @ app.js:15719$get.Scope.$digest @ app.js:15530$get.Scope.$apply @ app.js:15824(anonymous function) @ app.js:23095eventHandler @ app.js:3247
app.js:28296 REQUIRE GOT: localRequire(deps, callback, errback)
Program 1 Module!
main.js:484 Program 1 CONTROLLER START?  Object {_invokeQueue: Array[0], _configBlocks: Array[0], _runBlocks: Array[0], requires: Array[4], name: "program1_module"}
main.js:527 Program 1 main :
.... and so on

Point being is that 'REQUIRE GOT: ' line I have in there is a console log I've put in your AngularAMD.prototype.route function:

 // If controller needs to be loaded, append to the resolve property
        if (load_controller) {
            console.log("LOAD CONTROLLER =",load_controller);
            var resolve = config.resolve || {};
            resolve['__AAMDCtrl'] = ['$q', '$rootScope', function ($q, $rootScope) { // jshint ignore:line
                var defer = $q.defer();
                console.log("RESOLVING ["+load_controller+"]");
                require([load_controller], function (ctrl) {
                    console.log("REQUIRE GOT:",ctrl);
                    defer.resolve(ctrl);
                    $rootScope.$apply();
                });
                return defer.promise;
            }];
            config.resolve = resolve;
        }

And before that even resolves, the client has already tried and failed to get the template.html... because that html file doesn't exist, it's inserted into the template cache during the firing up of the program1 code (which is triggered by requesting the r.js optimised file, which has a 'require' at the end which requires the main.js defined module... this is a little different to dev, where the 'controller' file, the one that controllerURL points to is of the form:

/**
 * Entry point for Program 1
 */
define(["./src/controllers/program1"], function(controller) {
    console.log("Program1 Main :");

    return controller;
});

That dependency on the controller (which is the controller for the 'state' we've just routed to), then depends on the module, and the module depends on all other controllers/factories/whatever is required for this program)

I wish I could put up a piece of working code to highlight this issue, but I'd take a fair bit of work to pull it apart and make a generic demonstration case for the web... I've just been changing the names of things here to 'program1' for the sake of simplicity :)

It boils down to, the templateUrl and controllerUrl are both requested at the same time. The controllerUrl has been turned into a resolve block, so it gets loaded before coming back, but the templateUrl is requested immediately, and so is requested too early. Effectively I guess I need to make the templateUrl only be requested after the controllerUrl code has actually run, because then it'll resolve to the template just defined in the cache, not on disc.

Oooh, actually, here's a bigger problem, and maybe gets to the root of why it works in dev but not when r.js optimized.

If I ignore the templates being the cache for now, but just have the build process copy them across, then I still get an issue with lazy loading my program.

In dev, when I navigate to a state that lazy loads, I get this as the order of things in the console :

stateChangeStart FROM:app TO: app.program1: Object {name: "$stateChangeStart", targetScope: Object, defaultPrevented: false, currentScope: Object}
RESOLVING [programs/program1/main.js]
module.js:2 Program 1 Module!
program1.js:8 Propgram1 CONTROLLER START?  Object {_invokeQueue: Array[0], _configBlocks: Array[0], _runBlocks: Array[0], requires: Array[4], name: "program1_module"}
main.js:5 Program 1 Main :
angularAMD.js:156 REQUIRE GOT: Object {_invokeQueue: Array[0], _configBlocks: Array[0], _runBlocks: Array[0], requires: Array[4], name: "program1_module"}
program1.js:10 Program 1 CONTROLLER!

And all is happy, it loads up fine.

Now, loading the same state (main app all running fine) in production mode, with the program files all optimized into a single file under the program1 folder called main.js:

$stateChangeStart FROM:app TO: app.program1: Object {name: "$stateChangeStart", targetScope: Object, defaultPrevented: false, currentScope: Object}
app.js:28294 RESOLVING [programs/program1/main.js]
app.js:28296 REQUIRE GOT: undefined
app.js:12221 Error: [ng:areq] Argument 'program1Ctrl' is not a function, got undefined
http://errors.angularjs.org/1.4.0/ng/areq?p0=program1Ctrl&p1=not%20aNaNunction%2C%20got%20undefined
    at REGEX_STRING_REGEXP (http://localhost:8080/dist/main-app/app.js:68:12)
    at assertArg (http://localhost:8080/dist/main-app/app.js:1765:11)
    at assertArgFn (http://localhost:8080/dist/main-app/app.js:1775:3)
    at http://localhost:8080/dist/main-app/app.js:8921:9
    at compile (http://localhost:8080/dist/main-app/app.js:32668:28)
    at invokeLinkFn (http://localhost:8080/dist/main-app/app.js:8604:9)
    at nodeLinkFn (http://localhost:8080/dist/main-app/app.js:8113:11)
    at compositeLinkFn (http://localhost:8080/dist/main-app/app.js:7514:13)
    at publicLinkFn (http://localhost:8080/dist/main-app/app.js:7388:30)
    at updateView (http://localhost:8080/dist/main-app/app.js:32609:23)(anonymous function) @ app.js:12221$get @ app.js:9055invokeLinkFn @ app.js:8606nodeLinkFn @ app.js:8113compositeLinkFn @ app.js:7514publicLinkFn @ app.js:7388updateView @ app.js:32609(anonymous function) @ app.js:32571$get.Scope.$broadcast @ app.js:16038$state.transitionTo.$state.transition.resolved.then.$state.transition @ app.js:31961processQueue @ app.js:14454(anonymous function) @ app.js:14470$get.Scope.$eval @ app.js:15719$get.Scope.$digest @ app.js:15530$get.Scope.$apply @ app.js:15824(anonymous function) @ app.js:28298context.execCb @ require.js:1670Module.check @ require.js:874(anonymous function) @ require.js:1124(anonymous function) @ require.js:132(anonymous function) @ require.js:1174each @ require.js:57Module.emit @ require.js:1173Module.check @ require.js:925Module.enable @ require.js:1161Module.init @ require.js:782callGetModule @ require.js:1188context.completeLoad @ require.js:1584context.onScriptLoad @ require.js:1691
main.js:426 Program 1 Module!
main.js:482 Program 1 CONTROLLER START?  Object {_invokeQueue: Array[0], _configBlocks: Array[0], _runBlocks: Array[0], requires: Array[4], name: "program1_module"}
main.js:525 Program 1 Main :

So here the problem is that the attempted loading of of the controller by name is happening before it's actually been defined. Now, I think this is because in dev the return object (by the define of the main.js file) makes the AngularAMD code wait for the resolution? And because the r.js optimised file doesn't return anything, it just carries on before the code has loaded?

Maybe I should create a simple demonstration case of this, because I'm spending a fair bit of time on it :)

OK. I bit the bullet and actually made a plunker to demonstrate how I'm using things within my dev structure:

http://plnkr.co/edit/491WzrHPwHJwnQJUj30g?p=preview

It spits a bunch of stuff into the console to show what it's doing.

And I've put up a production build example, which shows the problem of the Program1 controller not being available when it should be:

http://plnkr.co/edit/dksmnFJVvy0gmbCxgBFT?p=preview

Following up this. I have managed to make my optimized files load the controller before it's requested now. I still have issues with the templates being requested before the code injecting them into the template cache runs, but if I skip the step of doing that with the templates, then the optimized version is working...

I added a wrapper around the optimized module files to return a promise:v

wrap: {
start: "define([],function() {",
end: "var defer = $.Deferred();"+
"require(['"+program+"'], function (ctrl) {"+
"defer.resolve(ctrl);});"+
"return defer;});"
}

('program' is a variable with the name of the module being built within the particular function of the gulp task)

This means that AngularAMD correctly resolves the dependency of the main program before trying to load the controller named in the state.

@spoco2 it's been another crazy week for me. Can you get a repo going with your code and I can try to take a look tonight?

OK... first time actually using github for putting code up! (Coming from svn and just started using Mercurial)..

I've put up the dev version:
https://github.com/spoco2/AngularAMD-DynamicModules

There's probably some issues around paths and things, as there are bits copied from the plunker and from my local version here.

OK. More updates. I actually have things working in a dist build now. I need to update my github/plunker examples to be the same... the things that have made this work are:

  • The mentioned wrap code above within the r.js config. That's a huge one, and allows the controller to be there when it's requested. So that's awesome, and a proper fix.
  • I discovered that my code that inserted the templates into the templateCache for the programs was never being run, because they were being placed in the runblock of my "templateStorage" module, which ran fine for the application templates, but was never run again (even though I was calling 'angularAMD.processQueue', which correctly processes the runblocks of the module for the dynamically added program.) So, modifying the way that the template injection inserted, so that it is placed within the runblock of the program's module itself, that lets the templates actually be inserted:
program1_module.run(["$templateCache",function($templateCache){
    /** @preserve inject:templates **/
    /** Contents of html templates will be injected here via gulp task **/
    /** @preserve endinject **/
    }] );

So, that's ok... a bit more stuff that has to be placed in files, and I can probably come up with a better way down the track so there is less stuff that needs to be thought about per module, but it works fine.

  • My final step is the hacky, crappy bit! :-/ Even with everything else in place, the templateUrl is requested before the js code is loaded and run when it loads program1, and so still tries to pull from disk rather than the cache, because it's not there yet. And so... I've done a nasty, nasty little hack for now, just for the code that defines the state for the parent 'program' controller/template, and only for those initial loads, because then after (and also for the initial application loads), everything is ok and in the template cache. Soooo, for these states, which are the ones added dynamically at loading of the app, by loading from a json file (in the example), I have this:
 var state =  {
    url:  prog,  //Don't Add / here, it adds one too many slashes
    auth: item.auth,
    views: {
        "program@":angularAMD.route({
            //templateUrl: "programs/" + prog + "/templates/" + prog + ".html", //This will end up in template cache, so url is ok
            controllerUrl: "programs/" + prog + "/main.js", //This will end up as the fully compiled file for the program, so this is good too :)
            controller: prog+"Ctrl",
            templateProvider: function($http,$templateCache,$q){
                console.log("templateProvider GET ["+prog+"] programs/" + prog + "/templates/" + prog + ".html");
                return $q(function(resolve,reject){
                   setTimeout(function(){
                       if($templateCache.get("programs/" + prog + "/templates/" + prog + ".html"))
                           resolve($templateCache.get("programs/" + prog + "/templates/" + prog + ".html"))
                       else
                           resolve( $http.get("programs/" + prog + "/templates/" + prog + ".html").then(function(response){return response.data;}))
                   },1000); //=-=-=-=-=-DODGY! DODGY! Just waiting so things have resolved... DODGY!!!=-=-=-=-
                });
            }
        })
    }
};

It's ugly, unwieldy, and introduces a second delay to wait for the code to be loaded. I hate just introducing time delays to make things work, it's not good at all... but it works for now. I can't find a way to wait until the controller is loaded or some other event (resolution of the controller), to then load the templateUrl.

I'll update the github and plunkers when I get some time, as they don't have this new stuff in place yet. And I'll also make them work and build using the gulpfile.

I've written a blog post on my approach now, might make my approach clearer, might confuse matters, not sure. http://simon.oconnorlamb.com/ria/2015/07/building-a-large-scale-lazy-loading-angular-application-using-angularamd/