Using downgradeModule in conjunction with downgradeInjectable in an angular / angularjs hybrid application results in error

theOwl picture theOwl · Nov 8, 2017 · Viewed 9.1k times · Source

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?

Answer

ghusse picture ghusse · Jul 4, 2018

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.