Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using Angular in extension on a page that already uses angular

I'm writing a Chrome Extension (here) for my library that uses angular for its UI. It works great on pages that don't use angular, but it causes issues with pages that do have angular. For example, on the Angular docs page:

Uncaught Error: [$injector:modulerr] Failed to instantiate module docsApp due to:
  Error: [$injector:nomod] Module 'docsApp' is not available! You either misspelled the module name or forgot to load it. If registering a module ensure that you specify the dependencies as the second argument.
  http://errors.angularjs.org/1.2.7/$injector/nomod?p0=docsApp
at chrome-extension://cfgbockhpgdlmcdlcbfmflckllmdiljo/angular.js:78:14
at chrome-extension://cfgbockhpgdlmcdlcbfmflckllmdiljo/angular.js:1528:19
at ensure (chrome-extension://cfgbockhpgdlmcdlcbfmflckllmdiljo/angular.js:1453:40)
at module (chrome-extension://cfgbockhpgdlmcdlcbfmflckllmdiljo/angular.js:1526:16)
at chrome-extension://cfgbockhpgdlmcdlcbfmflckllmdiljo/angular.js:3616:24
at Array.forEach (native)
at forEach (chrome-extension://cfgbockhpgdlmcdlcbfmflckllmdiljo/angular.js:302:13)
at loadModules (chrome-extension://cfgbockhpgdlmcdlcbfmflckllmdiljo/angular.js:3610:7)
at createInjector (chrome-extension://cfgbockhpgdlmcdlcbfmflckllmdiljo/angular.js:3550:13)
at doBootstrap (chrome-extension://cfgbockhpgdlmcdlcbfmflckllmdiljo/angular.js:1298:22)
http://errors.angularjs.org/1.2.7/$injector/modulerr?p0=docsApp&p1=Error%3A…xtension%3A%2F%2Fcfgbockhpgdlmcdlcbfmflckllmdiljo%2Fangular.js%3A1298%3A22) angular.js:78

The weird thing is that it seems to happen whether my extension actually uses angular or not. All I need to reproduce the issue is to include angular in my manifest.json as a content_script and this error is thrown. Any ideas of how I could make this work without messing up an angular site would be greatly appreciated.

Like I said, it doesn't matter whether I actually use angular or not, but this is all I'm doing to use it:

makeWishForAnchors(); // This just loads the global genie object. I don't believe it's related.

var lamp = '<div class="genie-extension"><div ux-lamp lamp-visible="genieVisible" rub-class="visible" local-storage="true"></div></div>';
$('body').append(lamp);

angular.module('genie-extension', ['uxGenie']);
angular.bootstrap($('.genie-extension')[0], ['genie-extension']);

Thanks!

like image 417
kentcdodds Avatar asked Jan 10 '14 16:01

kentcdodds


People also ask

Can you use Angular for Chrome extension?

If you want to add an extension in your chrome browser, just visit Chrome Store and add it to the browser. Chrome extensions are just javascript applications built on web technologies such as HTML, Javascript and CSS. In this blog, we are creating a basic Chrome extension using the Angular framework.

Can AngularJS be used with Angular?

In a hybrid application you run both versions of Angular at the same time. That means that you need at least one module each from both AngularJS and Angular. You will import UpgradeModule inside the NgModule, and then use it for bootstrapping the AngularJS module.

How do I debug Angular in Chrome?

You can find the JavaScript Debugger under the sources tab of the ChromeDev Tools. As you look for bugs in your Angular code, this tool allows you to set breakpoints and inspect your code at runtime. It also gives you the capability to obtain the value of your variables at different points in time.


1 Answers

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:

  1. 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;
    
  2. 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);
    
  3. 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('\n');

// 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.

like image 57
gkalpak Avatar answered Oct 13 '22 20:10

gkalpak