13
votes

I seem to have hit a wall I am not able to break down. I am using angular + typescript, and want to make it work with requirejs. There are multiple angular apps defined that are loaded dependent on a data-app-name attribute on the body.

Folder structure (simplified):

|- Libs
   |- angular
   |- some-plugin
   |- angular-some-plugin // Exposes an angular.module("ngSomePlugin")
   |- require.js
|- Common
   |- Common.ts // Exposes an angular.module("common")
|- App1
   |- Controllers
      |- SomeController.ts
      |- SomeOtherController.ts
   |- Services
      |- SomeService.ts
   |- Main.ts
   |- App.ts
|- App2
   // same as above
|- AppX
   // same as above
|- Index.html
|- Main.ts

Contents:

Index.html:

// All these attributes are set dynamically server-side
<body id="ng-app-wrapper" data-directory="App1" data-app-name="MyApp">
    <script src="Libs/require.js" data-main="Main"></script>
</body>

Main.ts:

console.log("Step 1: Main.js");

requirejs.config({
    paths: {
        "angular": "Libs/angular/angular",
        "common": "Common/common"
    },
    shim: {
        "angular": {
            exports: "angular"
        }
    }
});

require(["angular"], (angular: angular.IAngularStatic) => {
    angular.element(document).ready(function() {

        var $app = angular.element(document.getElementById("ng-app-wrapper"));
        var directory = $app.data("directory");
        var appName = $app.data("app-name");

        requirejs.config({
            paths: {
                "appMain": directory + "/Main"
            }
        });

        require([
            'common',
            'appMain'
        ], function () {
            console.log("Step 5: App1/Main.js loaded");
            console.log("Step 6: Bootstrapping app: " + appName);
            angular.bootstrap($app, [appName]);
        });
    });
});

Main.ts in App1:

console.log("Step 2: App1/Main.js");

requirejs.config({
    paths: {
        "app": "App1/App",
        "somePlugin": "Libs/some-plugin/some-plugin", // This is an AMD module
        "ngSomePlugin": "Libs/angular-some-plugin/angular-some-plugin"
    },
    shim: {
        "ngSomePlugin": {
            exports: "ngSomePlugin",
            deps: ["somePlugin"]
        }
    }
});

define([
    "app"
], () => {
    console.log("Step 4: App.js loaded");
});

App1/App.ts:

console.log("Step 3: App.js");

import SomeController = require("App1/Controllers/SomeController");
import SomeOtherController = require("App1/Controllers/SomeOtherController");
import SomeService = require("App1/Services/SomeService");

define([
    "angular",
    "ngSomePlugin"
], (angular: angular.IAngularStatic) => {

    // This isn't called, so obviously executed to late
    console.log("Defining angular module MyApp");

    angular.module("MyApp", ["common", "ngSomePlugin"])
        .controller("someCtrl", SomeController.SomeController)
        .controller("someOtherCtrl", SomeOtherController.SomeOtherController)
        .service("someService", SomeService.SomeService)
        ;
});

This however seems to break, with the good old error: Uncaught Error: [$injector:nomod] Module 'MyApp' is not available! You either misspelled the module name or forgot to load it.

Question 1:

What am I doing wrong here? How can I make sure the angular.module() call is done before I am bootstrapping my app?

This is the output of the console.logs, where you can see angular hasn't yet defined the module angular.module("MyApp"), so this is done to late obviously: Console log

UPDATE I can unwrap the angular calls in App.ts, so it doesn't require anything (except for the imports at the top). Then if I add App to the shim in App1/Main.ts, and lay the dependencies there it seems to work. Is this a good way to solve this?

UPDATE2 If I use require instead of define in App.ts, it does instantiate the angular module, but still after it tries to bootstrap it.

Question 2:

Is there any way to pass down custom configuration, for example the directory name where the Libs are? I tried the following which did not seem to work (Main.ts):

requirejs.config({
    paths: {
        "appMain": directory + "/Main"
    },
    config: {
        "appMain": {
            libsPath: "Libs/"
        },
        "app": {
            name: appName
        }
    }
});

App1/Main.ts:

define(["module"], (module) => {
    var libsPath = module.config().libsPath;
    requirejs.config({
        paths: {
            "somePlugin": libsPath + "somePlugin/somePlugin"
            // rest of paths
        }
    });

    define([ // or require([])
        "app"
    ], () => {});
});

App.ts:

define([
    "module"
    // others
], (module) => {
    angular.module(module.config().name, []);
});

But this way, logically, the angular.bootstrap() does not wait for App.ts to be loaded. Therefore the angular module isn't defined yet and it fails to bootstrap. It seems that you cannot do a nested define as in App1/Main.ts? How should I configure this?

2
I have doubts that the multiple layers of require.config() calls will work as expected. (Side-question: do they?) But since the data-directory/data-app-name are generated server-side, I'd suggest you generate the require.config() call dynamically too. You may even want to dynamically, server-side include the appropriate fragment directly into the Index.html. From there, you will need to tweak the paths in the AMD modules. And a sidenote: why aren't you using Typescript's native support for AMD, i.e. the import Foo = require('Foo') syntax?Nikos Paraskevopoulos
I do the import stuff on angular-app level (in App.ts and all other angular components). Is it better to use the import syntax everywhere if I'm using typescript? i don't think it will work on non-AMD third party libraries?devqon
And yes, the multiple layers of require.config() seems to work :)devqon
Can you add comments to the resolved dependencies (so we dont have to read back the config), draw a dependency diagram including the expected/needed/desired states or email me the project (A) xDEricG
I'm starting to feel like the imports might be blocking, I don't have a problem requiring the controllers (also skipped the plugin).EricG

2 Answers

1
votes

Question 1

Alexander Beletsky wrote a great article about tying requireJS and Angular together (and about why it's worth it). In it, I think he has the answer to your first question: Load angular from another module and then make the bootstrapping call. This forces requireJS to load dependencies for those modules before executing any code.

Assuming this is your main.ts

console.log("Step 1: Main.js");

requirejs.config({
    paths: {
        "angular": "Libs/angular/angular",
        "common": "Common/common".
        "mainT1": "t1/main.js"
    },
    shim: {
        "angular": {
            exports: "angular"
        }
    }
});

Add at the very end a call to your next module

requirejs(["app"], function(app) {
    app.init();
});

Here in t1's main, you make the actual app and have it load up all at once.

t1/main.ts

define("app-name", ['angular'], function(angular){

    var loader = {};

    loader.load = function(){

         var app = angular.module("app-name",[]);

         return app;
         }

 return loader;

}

Finally, let's say you a staging file here it carries you to called app.js. Here you'd set up the sequencing to get an object that has already finished angular's loading sequence. Once it's complete, THEN you call the bootstrapping in the init() function.

t1/app.js

define("app", function(require){
     var angular = require('angular');
     var appLoader = require('mainT1');

     var app = {}
     app.init = function(){
           var loader = new appLoader();
           loader.load();
           angular.bootstrap(document, ["app-name"]);
     }

}
-1
votes

Related to Question 1:

In App1/App.ts, shouldn't the function return the angular module for it to be injected.

eg.

define([
    "angular",
    "ngSomePlugin"
], (angular: angular.IAngularStatic) => {
    return angular.module("MyApp", ["common", "ngSomePlugin"])
        .controller("someCtrl", SomeController.SomeController)
        .controller("someOtherCtrl", SomeOtherController.SomeOtherController)
        .service("someService", SomeService.SomeService)
        ;
});