Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Requirejs + (multi)angular + typescript

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?

like image 966
devqon Avatar asked Nov 03 '15 13:11

devqon


1 Answers

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"]);
     }

}
like image 153
prestonsmith Avatar answered Sep 22 '22 14:09

prestonsmith