21

With angular 5.0 the upgrade module now has the option of using downgradeModule which runs angularjs outside of the angular zone. While experimenting with this I have run into a problem with using downgradeInjectable.

I am receiving the error:

Uncaught Error: Trying to get the Angular injector before bootstrapping an Angular module.

Bootstrapping angular in angular js works fine

import 'zone.js/dist/zone.js';
import * as angular from 'angular';
/**
 * Angular bootstrapping
 */
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { decorateModuleRef } from 'src/environment';
import { AppModule } from 'src/app/app.module';
import { downgradeModule } from '@angular/upgrade/static';

export const bootstrapFn = ( extraProviders ) => {
    const platformRef = platformBrowserDynamic( extraProviders );
    return platformRef
        .bootstrapModule( AppModule )
        .then( decorateModuleRef );
};

angular.module( 'app.bootstrap', [
    downgradeModule( bootstrapFn ),
] );

However...

Since the bootstrapping takes place after angularjs has been initialized I can no longer get the downgrade injectable working.

Service to be downgraded

import { Injectable, Inject, OnInit } from '@angular/core';

@Injectable()
export class MobileService implements OnInit{
    constructor(
        @Inject( 'angularjsDependency1' ) public angularjsDependency1 : any,
        @Inject( 'angularjsDependency2' ) public angularjsDependency2 : any,
    ) {}

}

Downgrade injectable attempt

import * as angular from 'angular';
import { downgradeInjectable } from '@angular/upgrade/static';
import { MyService } from 'src/services/myService/myService';

export const myServiceDowngraded = angular.module( 'services.mobileService', [
    angularjsDependency1,
    angularjsDependency2,
] )

.factory(
    'mobileService',
    downgradeInjectable( MyService ),
).name;

When "downgradeInjectable( MyService ) runs the angular injector is not available yet since angular hasn't been bootstrapped. Hence the error:

Uncaught Error: Trying to get the Angular injector before bootstrapping an Angular module.

Does anyone have an idea how I might fix this?

Rohith K P
  • 3,233
  • 22
  • 28
theOwl
  • 431
  • 1
  • 3
  • 9
  • I have the same issue. Perhaps add a GitHub issue – TJF Nov 09 '17 at 07:29
  • I have the same issue because of using `downgradeModule` and `UpgradeModule` together. After removing `downgradeModule`, `downgradeInjectable` works as expected. – hankchiutw Oct 29 '21 at 08:38

6 Answers6

17

Answers in this thread helped me find a solution, but none contains the holy grail:

  1. Creating a service-boostrap component aside the app's code does not work, because Angular is loaded asynchronously, unlike AngularJS. This gives the same error Trying to get the Angular injector before bootstrapping an Angular module.
  2. Creating a service-bootstrap component wrapping the AngularJS code kind of worked, but then I experienced issues with change detection inside Angular composants, as described in this issue on github.
  3. In the github issue, someone suggested to edit @angular/upgrade source code to change a false to true to force components to be created in the Zone. But in this case it seems to cause performance issues (it seemed to launch ngZone's code multiple times on user events)
  4. In order for the app to work correctly, I needed :
    1. Not to have ng components containing AngularJS components containing Angular components. We need to only have AngularJS containing Angular components.
    2. Make sure that AngularJS components using Angular services are created after a first angular component, named service-bootstrap

To acheive this, I created a slightly modified service-bootstrap component:

import { Component, Output, EventEmitter, AfterViewInit } from "@angular/core";

@Component({
    selector: 'service-bootstrap',
    template: ``
})
export class ServiceBootstrapComponent implements AfterViewInit{
    @Output()
    public initialized: EventEmitter<void> = new EventEmitter();

    public ngAfterViewInit(){
        this.initialized.emit();
    }
}

Declared this component as entryComponent in the Angular module and called downgradeComponent to register it in AngularJS:

import { downgradeModule, downgradeInjectable, downgradeComponent } from '@angular/upgrade/static';

const bootstrapFn = (extraProviders: StaticProvider[]) => {
    const platformRef = platformBrowserDynamic(extraProviders);
    return platformRef.bootstrapModule(AppModule);
};

const downgradedModule = downgradeModule(bootstrapFn);

const app = angular.module('ngApp', [
    downgradedModule,
    'app'
]);

app.directive('serviceBootstrap', downgradeComponent({ component: ServiceBootstrapComponent }));

Then (and the magic happens here), I created a new AngularJS component:

angular.module("app")
    .directive('ng1App', ng1AppDirective);

function ng1AppDirective(){
    return {
        template: `
            <service-bootstrap (initialized)="onInit()"></service-bootstrap>
            <section ng-if="initialized">
              <!-- Your previous app's code here -->
            </section>
        `,
        controller: ng1AppController,
        scope: {},
    };
}

ng1AppController.$inject = ['$scope'];
function ng1AppController($scope){
    $scope.onInit = onInit;
    $scope.initialized = false;

    function onInit(){
        $scope.initialized = true;
    }
}

Then, my index.html only referenced this component

<body>
  <ng1-app></ng1-app>
</body>

With this approach, I'm not nesting AngularJS components inside Angular components (which breaks change detection in Angular components), and still I ensure that a first Angular component is loaded before accessing the Angular providers.

ghusse
  • 3,200
  • 2
  • 22
  • 30
  • This one saved my day in the end, thank you sir. I was able use (renamed) ng1App component (made a component) like a temporary "decorator" for ng1 components which need ng2 downgraded injectables. – sneakyfildy Oct 24 '18 at 15:51
  • 1
    #2 has been fixed in 7.2.0. – Klaster_1 Нет войне Jan 10 '19 at 08:58
  • 1
    I am trying to use this method, but running into a snag. My AngularJS app depends on the low-level service I have migrated to Angular and am attempting to downgrade and inject into it. So I have AngularJS bootstrap, load an Angular directive and its module first, so I can downgrade a migrated service and use it in AngularJS. But wait -- the AngularJS app itself depends on this service being available, so how can it bootstrap properly? Ultimately, my app depends on all services bc they are used in the app. Can I migrate any of them this way? – DragonMoon Sep 29 '19 at 01:02
  • 1
    this method solved it for me with one gotcha. i was using ui-router and it had resolves in it that depended on downgraded services. This caused the same injector before bootstrap problem (silently) inside the router resolve. To fix it, I had to add this to my hybrid js module: `.config(['$urlRouterProvider', ($urlRouterProvider) => $urlRouterProvider.deferIntercept()])` to defer the router trying to resolve, and then in the `ng1AppDirective`, inject `$urlRouter` and call `$urlRouter.listen(); $urlRouter.sync();` in the `onInit()` function. this guaranteed the services being available. – bryan60 Apr 28 '20 at 19:03
12

Note: The answer below follows the convention of calling angular 1.x as angularjs and all angular 2+ versions as simply angular.

Expanding on JGoodgive's answer above, basically, if you're using downgradeModule, then angular module is bootstrapped lazily by angularjs when it needs to render the first angular component. Until then, since the angular module isn't initialised, if you are accessing any angular services inside angularjs using downgradeInjectable, those services aren't available too.

The workaround is to force bootstrapping of the angular module as early as possible. For this, a simple component is needed:

import {Component} from '@angular/core';

@Component({
  selector: 'service-bootstrap',
  template: ''
})
export class ServiceBootstrapComponent {}

This component doesn't do anything. Now, we declare this component in the top level angular module.

@NgModule({
  // ...providers, imports etc.
  declarations: [
    // ... existing declarations
    ServiceBootstrapComponent
  ],
  entryComponents: [
    // ... existing entry components
    ServiceBootstrapComponent
  ]
})
export class MyAngularModule {}

Next, we also need to add a downgraded version of this component to angularjs module. (I added this to the top level angularjs module I had)

angular.module('MyAngularJSModule', [
  // ...existing imports
])
.directive(
  'serviceBootstrap',
  downgradeComponent({ component: ServiceBootstrapComponent }) as angular.IDirectiveFactory
)

Finally, we throw in this component in our index.html.

<body>
  <service-bootstrap></service-bootstrap>
  <!-- existing body contents -->
</body>

When angularjs finds that component in the markup, it needs to initialise angular module to be able to render that component. The intended side effect of this is that the providers etc. also get initialised and are available to be used with downgradeInjectable, which can be used normally.

developer033
  • 24,267
  • 8
  • 82
  • 108
Akash
  • 5,153
  • 3
  • 26
  • 42
  • any idea on how make this work in tests? I'm getting the same error when testing a controller which uses a downgraded service :) – lucassp Apr 13 '18 at 12:38
  • 2
    Great, thanks. In my case I made mine a container using `ng-content` which also serves as the bootstrap element for the AngularJS app. – ach Jun 08 '18 at 15:27
  • This sounds reasonable, but as coded, it does not work in an Angular14/AngularJS1.x hybrid application. I even tried adding the ServiceBootstrapComponent to the boostrap list in the top module, but stills somehow Angular is wanting to use the angular service itself before Angular is bootstrapped. Could it be that ng-controller is still assigned on my top HTML tag? – jessewolfe Oct 04 '22 at 20:47
10

This was pointed out to me in an angular github thread.

https://github.com/angular/angular/issues/16491#issuecomment-343021511

George Kalpakas's response:

Just to be clear: You can use downgradeInjectable() with downgradeModule(), but there are certain limitations. In particular, you cannot try to inject a downgraded injectable until Angular has been bootstrapped. And Angular is bootstrapped (asynchronously) the first time a downgraded component is being rendered. So, you can only safely use a downgraded service inside a downgraded component (i.e. inside upgraded AngularJS components).

I know this is limiting enough that you might decide to not use downgradeInjectable() at all - just wanted to make it more clear what you can and can't do.

Note that the equivalent limitation is true when using an upgraded injectable with UpgradeModule: You cannot use it until AngularJS has been bootstrapped. This limitation usually goes unnoticed though, because AngularJS is usually bootstrapped in the Angular module's ngDoBootstrap() method and AngularJS (unlike Angular) bootstraps synchronously.

theOwl
  • 431
  • 1
  • 3
  • 9
  • What's been your experience with ngUpgrade? – eflat Nov 18 '17 at 05:13
  • 5
    eflat, it has been generally good since switching to the downgradeModule() approach. The upgradeModule() approach did not work for us as the performance was terrible given our bottom up approach to upgrading. When upgrade module was used on a very large application the excessive digests that were fired made the application unusable. Everything has been pretty smooth since switching to the downgradeModule except for the issue posed in this question which we have since worked around. – theOwl Nov 20 '17 at 18:39
4

I had the same issue, and the reasons are explained in the above answer.

I fixed this by dynamically injecting the downgraded angular service using $injector.

Steps

  • Register your downgraded service to angularjs module

     angular.module('moduleName', dependencies)    
     angular.factory('service', downgradeInjectable(Service));
    
  • Inject $injector to your controller and use this to get the downgraded service

    const service = this.$injector.get('service');
    service.method();
    
developer033
  • 24,267
  • 8
  • 82
  • 108
Rohith K P
  • 3,233
  • 22
  • 28
  • I assume the "get the downgraded service" is happening in Angular code, right? Won't you still have the problem that the service won't be available if Angular didn't get booted up yet? – jessewolfe Aug 29 '22 at 20:14
  • I figured it out -- you are getting the downgraded service in your AngularJS controller or other service. This is an elegant solution - much easier than the service bootstrap (which I actually could not get to work as expected). Thanks, @developer033 – jessewolfe Aug 30 '22 at 19:35
  • I didn't understand why this works, but it saved my day :) – tkarls Mar 28 '23 at 14:32
0

I had the same issue and it sucked up several hours before finding this. My workaround was to create a ServiceBootstrapComponent that does nothing but injects all the services that we need to downgrade.

I then downgrade that component, mark it as en entry in @NgModule and add it to index.html.

Works for me.

JGoodgive
  • 1,068
  • 10
  • 20
  • Hi @JGoodgive can you clarify what "mark it as an entry in @NgModule" means? Which .ts file module are you adding it to? – jessewolfe Aug 02 '22 at 03:35
0

I was getting the same error in our hybrid app. We are using the following versions:

  • AngularJS 1.7.x
  • Angular 7.3.x

As mentioned in this answer, I also used a dummy component called <ng2-bootstrap> to force boostrapping of Angular. And then, I created an AngularJS service which checks if Angular has been bootstrapped:

// tslint:disable: max-line-length
/**
 * This service can be used in cases where Angular fails with following error message:
 *
 * `Error: Trying to get the Angular injector before bootstrapping the corresponding Angular module.`
 *
 * Above error occurs because of how `downgradeModule` works.
 */

/*@ngInject*/
export class Ng2BootstrapDetectionService {
  private bootstrapDone = false;
  constructor(private $q: ng.IQService) {}

  public whenBootstrapDone(): ng.IPromise<void> {
    if (this.bootstrapDone) {
      return this.$q.resolve();
    }

    const deferred = this.$q.defer<void>();

    angular.element(document).ready(() => {
      const intervalId = setInterval(() => {
        const el = document.querySelector('ng2-bootstrap');
        if (el && el.outerHTML.includes('ng-version=')) {
          this.bootstrapDone = true;
          clearInterval(intervalId);
          deferred.resolve();
        }
      }, 500);
    });

    return deferred.promise;
  }
}

Ng2BootstrapDetectionService can be used like below:

import {NotificationService} from 'ng2-app/notification.service';

// This can be used in cases where you get following error:
// `Error: Trying to get the Angular injector before bootstrapping the corresponding Angular module.`

// You will need access to following
// $injector: AngularJS Injector
// Ng2BootstrapDetectionService: our custom service to check bootsrap completion
this.Ng2BootstrapDetectionService
  .whenBootstrapDone()
  .then(() => {
    const notificationService = this.$injector
      .get<NotificationService>('ng2NotificationService');
    notificationService.notify('my message!');
  });

You can find more details about this solution at the end of this blog post.

pankaj28843
  • 2,458
  • 17
  • 34