The problem
As soon as Angular is injected, it parses the DOM looking for an element with the ng-app
directive. If one is found Angular will bootstrap automatically. This becomes a problem when a page uses Angular itself, because (although they have separate JS execution contexts) the page and the content script share the same DOM.
The solution
You need to prevent your Angular instance (by "your" I mean the one injected by your extension as a content script) from automatically bootstrapping. Normally you would just omit the ng-app
directive and you would be good to go, but since you do not have control over the original DOM (nor do you want to break the page's functionality) this is not an option.
What you can do it use manual bootstrapping for your Angular app in conjunction with deferred bootstrapping (to prevent your Angular from trying to automatically bootstrap the page's Angular app).
At the same time, you need to "protect" (i.e. hide) your app's root element from the page's Angular instance. To achieve this, you can wrap your root element in a parent element with the ngNonBindable directive, so the page's Angular instance will leave it alone.
Summarizing the steps from the above docs, you need to do the following:
Prepend window.name
with NG_DEFER_BOOTSTRAP!
prior to injecting your Angular.
E.g. inject a tiny script (before angluar.js
) containing just one line:
window.name = 'NG_DEFER_BOOTSTRAP!' + window.name;
Wrap your app's root element in a parent element with the attribute ng-non-bindable
:
var wrapper = ... // <div ng-non-bindable></div>
wrapper.appendChild(lamp); // or whatever your root element is
document.body.appendChild(wrapper);
In your app's main script, manually bootstrap your Angular app:
var appRoot = document.querySelector('#<yourRootElemID');
angular.bootstrap(appRoot, ['genie-extension']);
Fine print: I haven't tested it myself, but I promise to do so soon !
UPDATE
The code below is intended as a proof of concept for the approach described above. Basically, it is a demo extension that loads an Angular-powered content-script into any http:
/https:
page whenever the browser-action button is clicked.
The extension takes all necessary precautions in order not to interfere with (or get broken by) the page's own Angular instance (if any).
Finally, I had to add a third requirement (see the updated solution description above) to protect/hide the content-script's Angular app from the page's Angular instance.
(I.e. I wrapped the root element in a parent element with the ngNonBindable directive.)
manifest.json:
{
"manifest_version": 2,
"name": "Test Extension",
"version": "0.0",
"background": {
"persistent": false,
"scripts": ["background.js"]
},
"browser_action": {
"default_title": "Test Extension"
// "default_icon": {
// "19": "img/icon19.png",
// "38": "img/icon38.png"
// },
},
"permissions": ["activeTab"]
}
background.js:
// Inject our Angular app, taking care
// not to interfere with page's Angular (if any)
function injectAngular(tabId) {
// Prevent immediate automatic bootstrapping
chrome.tabs.executeScript(tabId, {
code: 'window.name = "NG_DEFER_BOOTSTRAP!" + window.name;'
}, function () {
// Inject AngularJS
chrome.tabs.executeScript(tabId, {
file: 'angular-1.2.7.min.js'
}, function () {
// Inject our app's script
chrome.tabs.executeScript(tabId, {file: 'content.js'});
});
});
}
// Call `injectAngular()` when the user clicks the browser-action button
chrome.browserAction.onClicked.addListener(function (tab) {
injectAngular(tab.id);
});
content.js:
// Create a non-bindable wrapper for the root element
// to keep the page's Angular instance away
var div = document.createElement('div');
div.dataset.ngNonBindable = '';
div.style.cssText = [
'background: rgb(250, 150, 50);',
'bottom: 0px;',
'font-weight: bold;',
'position: fixed;',
'text-align: center;',
'width: 100%;',
''].join('
');
// Create the app's root element (everything else should go in here)
var appRoot = document.createElement('div');
appRoot.dataset.ngController = 'MyCtrl as ctrl';
appRoot.innerHTML = 'Angular says: {{ctrl.message}}';
// Insert elements into the DOM
document.body.appendChild(div);
div.appendChild(appRoot);
// Angular-specific code goes here (i.e. defining and configuring
// modules, directives, services, filters, etc.)
angular.
module('myApp', []).
controller('MyCtrl', function MyCtrl() {
this.message = 'Hello, isolated world !';
});
/* Manually bootstrap the Angular app */
window.name = ''; // To allow `bootstrap()` to continue normally
angular.bootstrap(appRoot, ['myApp']);
console.log('Boot and loaded !');
Fine print:
I have conducted some preliminary tests (with both Angular and non-Angular webpages) and everything seems to work as expected. Yet, I have by no means tested this approach thoroughly !
Should anyone be interested, this is what it took to "Angularize" Genie's lamp.