2

Having created a very basic prototype AngularJS project, I wanted to migrate it to use RequireJS to load the modules. I modified my app based on the AngularAMD and AngularAMD-sample projects.

Now, when I access my default route I get:

Uncaught TypeError: Cannot read property 'directive' of undefined

I've been scratching my head as to why the dependency on 'app' is not being satisfied. If anyone can spot what I'm obviously doing wrong, it'd be much appreciated.

I've put the source code of my project here on GitHub, but here's the key parts:

main.js

require.config({

    baseUrl: "js/",

  // alias libraries paths
    paths: {
        'angular': '../bower_components/angular/angular',
        'angular-route': '../bower_components/angular-route/angular-route',
        'angular-resource': '../bower_components/angular-resource/angular-resource',
        'angularAMD': '../bower_components/angularAMD/angularAMD',
        'ngload': '../bower_components/angularAMD/ngload',
        'jquery': '../bower_components/jquery/jquery'

    },

    // Add angular modules that does not support AMD out of the box, put it in a shim
    shim: {
        'angularAMD': ['angular'],
        'ngload': [ 'angularAMD' ],
        'angular-route': ['angular'],
        'angular-resource': ['angular']
    },

    // kick start application
    deps: ['app']
});

app.js

define(['angularAMD', 'angular-route', 'controller/login', 'controller/project_detail', 'controller/project_list'], function (angularAMD) {
  'use strict';

  var app = angular.module('cmsApp', ['ngRoute']);

  app.constant('REMOTE_BASE_URL', "/cms/v2/remote");

  app.constant('SERVER_ERROR_TYPES', {
    authentication: 'Authentication',
    application: 'Application',
    transport: 'Transport'
  });

  app.constant('AUTH_ERROR_TYPES', {
    invalidLogin: "INVALID_CREDENTIALS",
    invalidToken: "INVALID_TOKEN",
    noToken: "NO_TOKEN"
  });

  app.constant('AUTH_EVENTS', {
    loginSuccess: 'auth-login-success',
    loginFailed: 'auth-login-failed',
    logoutSuccess: 'auth-logout-success',
    notAuthenticated: 'auth-not-authenticated'
  });

  app.config(['$routeProvider',
    function($routeProvider) {
      $routeProvider.
        when('/login', {
          templateUrl: 'partials/login.html',
          controller: 'LoginCtrl'
        }).
        when('/projects', {
          templateUrl: 'partials/project-list.html',
          controller: 'ProjectListCtrl'
        }).
        when('/projects/:projectId', {
          templateUrl: 'partials/project-detail.html',
          controller: 'ProjectDetailCtrl'
        }).
        otherwise({
          redirectTo: '/projects'
        });
    }]);

  return angularAMD.bootstrap(app);
});

And the file which the exception is being raised in: login_form.js

define(['app'], function (app) {
   app.directive('loginForm', function (AUTH_EVENTS) {
      return {
        restrict: 'A',
        template: '<div ng-if="visible" ng-include="\'partials/login.html\'">',
        link: function (scope) {
          scope.visible = false;

          scope.$on(AUTH_EVENTS.notAuthenticated, function () {
            scope.visible = true;
          });

          scope.$on(AUTH_EVENTS.loginFailed, function () {
            alert("An error occured while trying to login. Please try again.")
            scope.visible = true;        
          });

          scope.$on(AUTH_EVENTS.logoutSuccess, function () {
            scope.visible = true;
          });
        }
      };
    });
});
DaveAlden
  • 30,083
  • 11
  • 93
  • 155

1 Answers1

6

You are loading 'controller/login' before the app itself was created.

Probably it is better to create a separate module like

define(['directive/login_form', 'service/authentication'], function () {
   'use strict';
   var loginModule = angular.module('loginModule', []);
   loginModule.controller('LoginCtrl', ...
   loginModule.directive('loginForm', ...

and then do something like

   var app = angular.module('cmsApp', ['ngRoute', 'loginModule']);

Does that make sense?

UPDATE:

I am just thinking of another solution. Just remove 'controller/login' from your app define. Using angularAMD your controller should not be loaded anyway before you navigate to the specified url. Just remove it and your controller gets loaded on demand. That way, app will be defined! (Although I would still suggest to create multiple modules. It feels better to not have everything in the app module but have multiple modules for different responsibilities. Also much better for testing.)

angularAMD.route({
    templateUrl: 'views/home.html',
    controller: 'HomeController',
    controllerUrl: 'scripts/controller'
})

Note the field controllerUrl.

Have a look here.

timtos
  • 2,225
  • 2
  • 27
  • 39
  • While combining logically-related components (controllers, directives, injectors, etc.) into a module within a single JS file makes sense, I don't think it solves my problem. If I do as you suggest above (I have tried it), I then get a similar error with the authentication service: `Uncaught TypeError: Cannot read property 'service' of undefined`. While I could also combine this into the loginModule JS file, the file would soon become very large. – DaveAlden Nov 05 '14 at 08:10
  • I still don't get why 'app' is not defined within my AMD modules. I'm following this [angularAMD example](https://github.com/marcoslin/angularAMD/blob/master/www/js/scripts/service/mapServices.js) for the authentication service. – DaveAlden Nov 05 '14 at 08:17
  • First of all, this doesn't have to be all in one file. You can still separate things and let requirejs load the files on demand. One just has to be careful putting things together. And of course, not everything should be put into the loginModule. Perhaps it makes sense to put your map service into a mapModule? I think restructuring your modules a little bit will solve your problem. Why is app not defined? Because requirejs first looks at the define. Then it will load these files and not before this it will start to run the code below the define line. That is the reason why app is not defined. – timtos Nov 05 '14 at 08:37
  • The load order: app.js: define([...'controller/login',... { var app = angular.module('cmsApp', ['ngRoute']); The define lets requirejs load the login controller: login.js: define(['app', 'directive/login_form'...], function (app) { The define lets requirejs load your login directive: login_form.js: define(['app'], function (app) { app.directive('loginForm', function (AUTH_EVENTS) { Now app is not defined, because the line var app = angular.module('cmsApp', ['ngRoute']); was not yet executed. – timtos Nov 05 '14 at 08:38
  • Please have a look at my updated answer. Using dynamically loaded controllers is perhaps the best solution for you. – timtos Nov 05 '14 at 08:59
  • Yes, you're right. The bit I was missing was I hadn't added angularAMD.route() to my routeProvider. The last problem I have is that I was using the "ng-controller" attribute to attach to login controller to a block of HTML: `
    `. If I remove it, the app works. I'm guessing it doesn't work with AMD because LoginCtrl is not defined at the point where this directive is being parsed. Is there a programmatic JS equivalent of the ng-controller HTML attribute that can be used to associate the controller with a DOM node?
    – DaveAlden Nov 05 '14 at 09:04
  • Concerning your last question: If the controllerUrl solution [where you combine a dynamically loaded html page with a .js file which contains the controller (see also above in the answer or at the little example at the end of the comment)] is not enough for you you can create your own login directive (https://docs.angularjs.org/guide/directive#!) or use a factory (https://docs.angularjs.org/guide/providers). But I would go for the dynmaic controller. Super easy: templateUrl: 'partials/login.html', controller: 'LoginCtrl' controllerUrl: 'partials/login.html' That way LoginCtrl will be defined – timtos Nov 05 '14 at 10:35