RequireJS plus AngularJS

Setting up RequireJS and AngularJS without the fuzz

Getting RequireJS and AngularJS to play along nicely together can be quite the hurdle for many of us. The main thing that throws us off track is this peculiar situation were you suddenly have two frameworks that both handle dependency injection. After spending a considerate amount of time debunking and refining, I managed to come up with a setup that is as clean as possible and still let’s the two frameworks coexist. Since this seems to be a recurring topic with a lack of good articles on how to solve it, I decided I would share my solution to the problem.

At the end of this article you can find an example project complete with runnable build and test tasks.

Acknowledgements

I would like to thank Rickard Lund for reviewing this article and giving me some good suggestions on improving the content.

Prerequisites

Write your AngularJS code based on John Papa’s AngularJS styleguide. This will make your transition to AMD modules a lot easier. It is also good if you have a basic understanding of AMD, RequireJS, AngularJS, Grunt and Karma. I would suggest that you start out by checking the references section if any of these things are unfamiliar to you.

Setup

Define your Angular module in a separate file and add all Angular module dependencies, config and register any third party libraries as services. Then wrap it up as an AMD module if you haven’t already done so and make sure to add Angular and any other libraries as Require dependencies. Make sure that you inject Angular and the other non Angular libraries. Injecting e.g. ngRoute through AMD would be pointless since we won’t be invoking it directly, but rather let Angular do it’s regular dependency handling.


define(['angular', 'jQuery'], function (angular) {
    'use strict';

    angular
        .module('exampleModule', [])
        .factory('jQuery', function () {
            return jQuery;
        });
});

config-angular-app.js

Create your Require configuration. I would recommend that you define some packages based on your folder structure. When it comes to the paths property, add all libraries and your Angular module AMD module. Yes, we are working with two kinds of modules now. It is no wonder that anyone might get confused trying to set this whole thing up. Anyhow add AngularJS and the other libraries to your shims (that is of course if they aren’t already AMD modules). Set up some dependencies, perhaps jQuery for Angular if you don’t want to use jqLite.


// Specify a RequireJS configuration.
require.config({

    // Define packages to use as path shortcuts to directories.
    packages: [
        { name: 'example', location: 'js/src' }
    ],

    // Paths to vendor scripts and separate files.
    paths: {
        'angular': 'bower_components/angular/angular',
        'jQuery': 'bower_components/jquery/dist/jquery',
        'exampleModule': 'js/config/config-angular-app'
    },

    // Create shims for vendor libraries that aren't AMD  modules and specify dependencies for them.
    shim: {
        'angular': {
            deps: ['jQuery'],
            exports: 'angular'
        },

        'jQuery': {
            exports: '$'
        }
    }
});

// Require angular and all scripts that angular parse from the markup. The RequireJS optimizer can't detect these by crawling and therefore they need to be specified manually.
require(['angular', 'example/exampleController'], function (angular) {
    angular.bootstrap(document, ['exampleModule']);
});

config-require.js

Revisit your Angular code and start wrapping it up in defines. Then comes the million dollar question. How do we make two frameworks dependency injection mechanics work together? If you followed the Papa guidelines then this should be a piece of cake.

There is a common dependency pattern which might not be obvious right now but will get clearer after AMD:ing your third/fourth Angular component.


define(['angular', 'exampleModule', 'example/exampleService'], function (angular) {
    'use strict';

    angular
        .module('exampleModule')
            .controller('exampleController', ExampleController);

    ExampleController.$inject = ['exampleService'];

    function ExampleController(exampleService) {
        ...
    }

    return ExampleController;
});

exampleController.js

The first necessary dependency is Angular. Using Papa’s setup you will actually call it directly since you are using the getter version of angular.module(<module-name>). Therefore it should be injected through the define callback.

Next up is your Angular module. You will register your Angular component directly on the returned module, so if it hasn’t been setup earlier then Angular will just say “I have no idea of what you are talking about?!”. So as long as it’s in your AMD dependency array then you should be fine. Also, since you are not directly invoking anything on the module, you won’t need to inject it through the define callback.

The last part of the common dependency pattern are all of your dependencies to your Angular component. That’s right, everything that is in your <Constructor>.$inject = [<dep1>, <dep2>] needs to be added to the AMD dependency array. This is perhaps the most redundant and bloated part of RequireJS and AngularJS. I have seen other solutions to this problem but they did not amount to much more than moving the mess to some obscure place in your repository. We cannot get away from the fact that we are dealing with double DI, either drop Angular or accept how it is.

Putting my ranting aside, let’s get back to the subject. The most common dependencies will be a lot of Angular Core services. Which we already have available since we are depending on Angular! Other than that it will be Angular modules like ngSanitize, ngResource or your own Angular components. The same principle applies for these, add them to the AMD dependency array but not to the define callback. Angular will handle all of the remaining dependency injection.

Unit Testing

Note that the following examples use Karma as the test runner and may require some modifications to work if you are using a different toolset.

First of all you will need to create a Karma config and add the required Karma plugins, karma-requirejs is mandatory. karma-jasmine is included based on my own personal preference, but you may use any other test framework that you prefer. In the end your config should look something like this:


module.exports = function (config) {
  config.set({

    basePath : '../../',

    // Files that Karma should include. Setting include:false tells Karma to make the files available in the base/ directory, but not include them in the generated HTML file using <script> tags.
    files : [
        { 'pattern': 'bower_components/jquery/dist/jquery.js', 'included': false },
        { 'pattern': 'bower_components/angular/angular.js', 'included': false },
        { 'pattern': 'bower_components/angular-mocks/angular-mocks.js', 'included': false },
        { 'pattern': 'js/src/**/*.js', 'included': false },
        { 'pattern': 'test/specs/**/*.spec.js', 'included': false },
        'test/config/config-require-test.js'
    ],

    singleRun: false,
    colors: true,
    autoWatch: true,
    logLevel: config.LOG_DISABLE,

    // Frameworks that Karma should include.
    frameworks: ['jasmine', 'requirejs'],

    // Browser to run tests in.
    browsers : ['Chrome'],

    // Karma plugins that should be available.
    plugins : [
        'karma-chrome-launcher',
        'karma-firefox-launcher',
        'karma-phantomjs-launcher',
        'karma-safari-launcher',
        'karma-jasmine',
        'karma-requirejs'
    ]
  });
};

config-karma.js

Something worth noting is that the files property contains objects with an included: false flag. Normally you should not have to add this, but since we are using RequireJS for fetching our scripts we need to make this adjustment. What it does is include the matching script in the /base directory of the server instance that Karma initiates. But the included: false flag tells Karma to not include them in the generated HTML page through <script> tags. If you for some reason forget to do this, RequireJS will throw a fit and complain that the file that it was supposed to retrieve was included prematurely and refuse to proceed.

The next step is to create another RequireJS config specifically for testing. What’s important with this config is that it should include every single one of your script files. It should also include this special part which fetches all the test spec files and sets them to the deps property and tells Karma to start when RequireJS has initialized them.


// Fetch the *.spec.js files that Karma has included through the files property.
var tests = [];
for (var file in window.__karma__.files) {
    if (window.__karma__.files.hasOwnProperty(file)) {
        if (/spec\.js$/.test(file)) {
            tests.push(file);
        }
    }
}

// Specify a separate RequireJS configuration to use for testing.
requirejs.config({

    // Karma serves files from '/base'
    baseUrl: '/base/',

    // Define packages to use as path shortcuts to directories.
    packages: [
        { name: 'example', location: 'js/src' }
    ],

    // Paths to vendor scripts and separate files.
    paths: {
        'angular': 'bower_components/angular/angular',
        'ngMocks': 'bower_components/angular-mocks/angular-mocks',
        'jQuery': 'bower_components/jquery/dist/jquery',
        'exampleModule': 'js/config/config-angular-app'
    },

    // Create shims for vendor libraries that aren't AMD  modules and specify dependencies for them.
    shim: {
        'angular': {
            deps: ['jQuery'],
            exports: 'angular'
        },

        'ngMocks': {
            deps: ['angular']
        },

        'jQuery': {
            exports: '$'
        }
    },

    // Make RequireJS load these files (all our tests)
    deps: ['angular', 'ngMocks'].concat(tests),

    // Start test run, once RequireJS is done
    callback: window.__karma__.start
});

config-require-test.js

Lastly you need to add your specs which should not feel to difficult by now. Since we are using Require even our test specs need to be AMD modules.


// Angular and ngMocks are mandatory, besides that you should only need to add your "unit under test" as a dependency.
define(['angular', 'ngMocks', 'example/exampleController'], function () {
    'use strict';

    describe('ExampleController', function () {
        var exampleController,
            mockExampleService,
            $scope;

        beforeEach(function () {
            mockExampleService = jasmine.createSpyObj('ExampleService', ['getStuff']);

            // Use angular-mocks to instantiate the module programmatically.
            module('exampleModule');

            // Use angular mocks to get hold of core services and components registered to the module.
            // Although any services can be injected through the inject functions callback,
            // it is cleaner to request the $injector service and call it's get method.
            inject(function ($injector) {
                // Request the rootScope and create a child scope.
                $scope = ($injector.get('$rootScope')).$new();

                // Request the $controller service used to instantiate the controller.
                var $controller = $injector.get('$controller');

                // Create an instance of the controller to test and inject it with mocked dependencies.
                exampleController = $controller('exampleController', {
                    $scope: $scope,
                    exampleService: mockExampleService
                });
            });
        });


        describe('#doStuff', function () {

            it('should call exampleService.getStuff once', function () {
                // Act
                exampleController.doStuff();

                // Assert
                expect(mockExampleService.getStuff).toHaveBeenCalled();
                expect(mockExampleService.getStuff.calls.count()).toEqual(1);
            });

        });
    });
});

exampleController.spec.js

What you should notice is that we depend on angular, ngMocks and the module to test. Nothing is injected since we will be using ngMocks global methods to instantiate and retrieve the module under test.

Building for production

Alright there are two ways to go about when building your bundle. The bad way is repeating all of your setup from the require config in your require build config. The good way is to use the mainConfigFile property and point to your require config. Using that approach you should get something like the following configuration.


module.exports = {
    options: {
        baseUrl: '.',

        // Wraps the shims in define declarations.
        wrapShim: true,

        // Tells the optimizer to crawl files to find other dependencies.
        findNestedDependencies: true,

        optimize: 'uglify2',
        generateSourceMaps: true,
        preserveLicenseComments: false
    },

    main: {
        options: {
            // Reuse the Require config for the build.
            mainConfigFile: 'js/config/config-require.js',

            // This is needed when outputing a single file (1:1 relation).
            include: ['js/config/config-require'],

            // The output directory and the name of the bundled file.
            out: 'dist/bundle.min.js'
        }
    }
};

requirejs.js

After getting all of this into place you should be able to build your RequireJS bundle by running grunt requirejs.

Examples

I have created a full example project that uses the aforementioned setup. You can find it at github.com/chriskevin/requirejs-and-angularjs-examples.

References

Posted on Categories ITTags , , , ,

Comments

Disqus Facebook