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.
I would like to thank Rickard Lund for reviewing this article and giving me some good suggestions on improving the content.
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.
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.
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.
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.
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.
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:
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.
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.
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.
After getting all of this into place you should be able to build your RequireJS bundle by running
I have created a full example project that uses the aforementioned setup. You can find it at github.com/chriskevin/requirejs-and-angularjs-examples.