0

Is it correct to say that invoking angular.bootstrap will create a new instance of AngularJS, together with a separate digest cycle, root scope, injector and so on?

If so, is it possible to nest such applications (i.e. invoke bootstrap on DOM nodes inside the DOM managed by another AngularJS app)? (I presume no!)

Ben Aston
  • 53,718
  • 65
  • 205
  • 331
  • please remove the `angular-ui-bootstrap` tag as your question has nothing to do with the Angular UI Bootstrap library. – icfantv Oct 06 '15 at 20:04

2 Answers2

1

There's only one instance of Angular. Each app has its own injector though. And since $rootScope is a service this means that every app gets its own instance and in turn its own digest cycle.

Bootstrapping involves compiling the DOM, which means nested Nodes would already be compiled when you try to bootstrap them.

a better oliver
  • 26,330
  • 2
  • 58
  • 66
1

It will create a new instance of Angular injector service, which is responsible for dependency injection and controls application life cycle. Consequently, it also creates new instances of the services which are used in the application (including the ones from ng module: $rootScope, $compile, etc).

You may think of it as of a new instance of the application (which is the collection of modules). Angular itself (angular object) isn't instantiated.

The thing that differs angular.bootstrap from angular.injector (the latter just creates a new injector instance) is that it links an injector instance to DOM element by means of $rootElement service. This way this element and its children are associated with specific injector instance (it can be acquired with angular.element(someElement).injector()).

It is not possible to bootstrap an app on bootstrapped element (or its children), Angular will protect it from being messed up.

However, Angular is not really fool-proof, it can be done by bootstrapping the apps in reverse:

angular.bootstrap(nestedAppElement, ['nestedApp']);
angular.bootstrap(appElement, ['app']);

Looks like we finally messed the things up. Nothing prevents app from compiling its own directives in <nested-app-container>, and it uses its own injector and scope (it is app root scope here) to compile them, not scope and injector that belong to current DOM element. And the things will become even more messy if nested app will be re-compiled by some parent app directive.

Bypassing Angular fool-proof bypass for double bootstrapping is quite straightforward. Since Angular uses inheritedData to get element's injector in hierarchical manner (when element.data('$injector') is undefined on bootstrapped element, it is automatically retrieved from parents), it has to be overwritten:

angular.module('app').directive('nestedAppContainer', function () {
  return {
    compile: function (element) {
      element.data('$injector', null);
    }
  };
});

Now the apps can be safely bootstrapped in any order. But the problem with keeping app away from <nested-app-container> is still there. It can be solved either with marking nestedAppContainer directive as terminal

angular.module('app').directive('nestedAppContainer', function () {
  return {
    priority: 100000,
    terminal: true,
    ...
  };
});

or by putting nested app into shadow DOM

nestedAppContainer = nestedAppContainer.createShadowRoot();

which is specifically intended to isolate DOM parts (natively supported by Chrome and polyfilled in other browsers).


TL;DR: the solution to nested apps in Angular

As it is shown here:

  <body>
    <div app-dir>
      <nested-app-container></nested-app-container>
    </div>
  </body>

and

var appElement = document.querySelector('body');

var nestedAppContainer = document.querySelector('nested-app-container');
// nestedAppContainer = nestedAppContainer.createShadowRoot();

var nestedAppElement = angular.element('<nested-app>')[0];
angular.element(nestedAppContainer).append(nestedAppElement);

angular.element(nestedAppElement)
  .append('<div app-dir>(app-dir)</div>')
  .append('<div nested-app-dir>(nested-app-dir)</div>');

angular.element().ready(function () {
  angular.bootstrap(appElement, ['app']);
  angular.bootstrap(nestedAppElement, ['nestedApp']);
});

angular.module('nestedApp', []).directive('nestedAppDir', function () {
  return {
    controller: function ($scope) {
      $scope.app = 'nested app';
    },
    compile: function (element) {
      element.prepend('nested app is "{{ app }}"');
    }
  }
});

angular.module('app', []).directive('appDir', function () {
  return {
    controller: function ($scope) {
      $scope.app = 'app';
    },
    compile: function (element) {
      element.prepend('app is "{{ app }}"');
    }
  }
});

angular.module('app').directive('nestedAppContainer', function () {
  return {
    priority: 100000,
    // 'terminal' is redundant with nestedAppContainer.createShadowRoot()
    terminal: true,
    compile: function (element) {
      // element injector is not 'undefined' anymore,
      // so it won't be inherited from parent elements
      element.data('$injector', null);
    }
  };
});

While it may look quite neat, proceed with care. As any other hack that exploits undocumented stuff, this one may conceal adverse side-effects or be broken by new framework release.

There are not so many cases that may be in need of hacks. Most times you have an urge to fight the framework, you're doing it wrong.

Estus Flask
  • 206,104
  • 70
  • 425
  • 565
  • So `angular.injector` can be used to create new injector instances, albeit not linked to the DOM. Does this mean that directive declarations in the DOM will only ever be instantiated using the directives registered with an injector associated with a `$rootElement`? In other words, can you choose to use an injector **not** associated with a DOM node when rendering directives? – Ben Aston Oct 05 '15 at 21:22
  • Yes, you can, and you may not be happy with results. Injector's own scope ($rootScope instance and its children) will be used to compile the directive and nested ones (the injectors may end up fighting over the same directive elements). See the updated answer, I've covered that. It should be more constructive now. – Estus Flask Oct 05 '15 at 23:56
  • Thanks a lot. Can you recommend any specific resources that have helped you get this level of understanding of AngularJS? – Ben Aston Oct 06 '15 at 09:13
  • Also, rather than pursuing nested AngularJS apps, and using custom injectors for directive rendering, another alternative is to wrap the module object used to configure the injector, such that injector registrations are prefixed with a "namespace". This approach also requires that when using the minification protection syntax `$inject = ['foo']` that the relevant dependencies use the "namespace" prefix. Could this approach be workable? – Ben Aston Oct 06 '15 at 09:16
  • If you're not certain that multiple injectors are a must then they are most likely not, it's better stick to more conventional ways. I'm not sure that I understand the last part, can you post a simple example of that? – Estus Flask Oct 06 '15 at 13:55
  • After you finished with the manual, check how the things are done in advanced ng extensions, (ui-*, ocLazyLoad) and don't afraid to get dirty and browse often through ng source code (including its own extensions) to figure out how it works. There are not so many articles that can help to get through ng internals, and their authors usually got their skills the hard way. – Estus Flask Oct 06 '15 at 13:58