Multiple Angular.js Versions on the Same Page

We found out today that the following doesn’t work if your app uses JSONP because angular expects the handlers to exist in the global window.angular object. YMMV.

I’ll update if we come up with a fix.

i.e. angular.noConflict()

In some instances you may need to include multiple versions of angular.js within the same page. This can happen when writing a third-party widget that you want your customers to embed onto their pages. Part of being a good citizen is not stomping all over their global variables and overwriting their version of angular.js.

Trying to load two incompatible versions of angular can also cause errors that take down both your widget and your customer’s site.

Imagine that we are writing a widget that depends on the latest angular.js (as of the time of this writing, 1.2.7). But our widget is going to be embedded onto a page that already utilizes a much older version (1.0.4). How can we go about loading our script and our dependencies without interfering with the existing page’s scripts?

The answer is not terribly difficult if we are using some sort of build script to concatenate all our javascript into one payload. We also have to be willing to deploy angular.js and any standard modules as part of that same payload.

The technique works by wrapping all of our code (including the angular.js library) in a immediately-invoking function that declares a local angular variable. Then, through the magic of closures, any references in our code to angular will resolve to that local variable rather than the global window.angular.

To wrap up all our code and dependencies, we’re going to prepend an intro.js file that begins the self-invoking function, and then append an outro.js that closes up and calls that function.

If we’re using grunt and grunt-contrib-concat, the concatenation settings might look something like this:

Gruntfile.js
1
2
3
4
5
6
7
8
9
10
11
12
...
concat: {
  src: [
    'src/intro.js',
    'lib/angular/angular.js',
    'lib/angular/angular-sanitize.js',
    'src/my-widget.js',
    'src/outro.js'
    ],
  dest: 'dist/my-widget.js'
}
...

We’ll look at each part in turn.

Before our script loads, the environment of the webpage looks something like this:

Angular.js version 1.0.4 is already loaded and has saved itself into the global window.angular variable. We don’t want to override that property, so when we load the newer angular.js 1.2.7, we need to trick it into saving itself into a variable we control.

src/intro.js
1
2
3
(function() {
  var existingWindowDotAngular = window.angular;
  var angular = (window.angular = {});

We do this by saving the current window.angular into a local variable for later restoration and then creating a new window.angular that is simply an empty object. We also stash a reference to the new window.angular in a local angular variable.

At this point, our environment looks something like this:

existingWindowDotAngular points at the 1.0.4 version of angular.js that was previously loaded onto the page. Both window.angular and the local angular variable point to the same empty object.

Now we can go ahead and load the version of angular.js that our widget requires. The angular.js library does not overwrite window.angular if it’s already defined, it merely creates it’s data and functions as properties on that object.

Notice how angular.js 1.2.7 has injected itself into the empty object.

After loading angular and whatever modules we require, we’ll be able to load our code as normal. Any references in our code to angular, will resolve to the local angular variable we made via closure. Note: in order for this to work, we should never refer to window.angular directly. Always simply use angular so that we correctly resolve to the closed over variable rather than the global.

src/my-app.js
1
2
3
4
angular.module('MyWidget', ['ngSanitize']); //refer to angular directly

//never use the following:
//window.angular.module('MyWidget', ['ngSanitize']);

At the end of your file, we concatenate an outro.js that replaces the window.angular field we saved initially saved off.

outro.js
1
2
3
4
5
6
  angular.element(document).ready(function() {
    angular.bootstrap(document.getElementById('my-app'), ['MyApp']);
    window.angular = existingWindowDotAngular;
  });

})();

We also need to manually bootstrap our app. After restoring window.angular to 1.0.4, it will be that version that encounters the ng-app="MyWidget" attribute. But since MyWidget was declared as part of a different angular.js instance, it will not be able to find and boot it.

You may find that you have to restore the existingWindowDotAngular inside the ready event because the $httpBackend service unfortunately accesses $window.angular directly. This does open up a small window for potential conflict, but the only other option is to patch angular directly.

After loading our widget’s payload, the global window.angular has been restored to 1.0.4 and all our code refers to 1.2.7 via the closed over angular variable. The two angular.js instances are now completely separated.

Its a good idea to use this technique on any widget that is intended to be injected into arbitrary webpages (even if there is no existing angular.js app there). For the safety of our own app, and the protection of the host page’s scripts, we should always avoid polluting the global namespace with data specific to our module.

TL;DR

my-widget.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
(function() {
  // Save a copy of the existing angular for later restoration
  var existingWindowDotAngular = window.angular;

  // create a new window.angular and a closure variable for 
  // angular.js to load itself into
  var angular = (window.angular = {});

  /*
   * Copy-paste angular.js and modules here. They will load themselves into
   * the window.angular object we just created (which is also aliased as
   * "angular")
   */

  ..

  // notice this refers to the local angular variable declared above, not
  // window.angular
  angular.module('MyWidget', ['ngSanitize']);

  ...

  //Manually bootstrap so the old angular version doesn't encounter
  //ng-app='MyWidget' and blow up
  angular.element(document).ready(function() {
    angular.bootstrap(document.getElementById('my-widget', ['MyWidget']);

    // restore the old angular version
    window.angular = existingWindowDotAngular;
  });
});