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.
UPDATE 2017-10-17: This article is over 3 years old, and is one of the most visited on my site, but I
don’t think I would stand behind this advice anymore. More modern builds of angular
probably don’t depend on setting the window.angular
global variable. By using
your favorite build system, you probably don’t need any of these shenanigans. I’m keeping it around because of
its pedagogic value for some complex javascript patterns.
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:
...
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.
(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.
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.
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.
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
(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;
});
});