Upgrading AngularJS component to Angular: No provider for ElementRef exception - angularjs

We're in the process of getting Angular up and running in our AngularJS app, but I'm facing an issue with mixing upgraded and downgraded components.
Here's the structure of my current problem
The outermost layer is our main application.
The next layer is my new Angular component, which is downgraded, so that I can use it in the AngularJS part of my app. (It's loaded in a ui-router state).
The last layer is an AngularJS component, that has been upgraded to be used in the Angular component.
The last layer is what triggers the issue. When upgrading (following the docs on angular.io), it starts throwing this error:
No provider for ElementRef!
Here are some snippets that might provide the info needed to help out
AngularJS component:
export const ProductComponent: ng.IComponentOptions = {
template: require('./product.html'),
controller: ProductController,
bindings: {
channel: '<',
datastandardId: '<',
productFamily: '<'
}
};
...
someModule.component('lpProduct', ProductComponent);
Upgraded to Angular:
#Directive({
selector: 'lp-product-ng2'
})
export class ProductDirective extends UpgradeComponent implements OnInit {
#Input() productFamily;
#Input() channel;
#Input() datastandardId;
constructor(#Inject('ElementRef') elementRef: ElementRef, #Inject('Injector') injector: Injector) {
super('lpProduct', elementRef, injector);
}
ngOnInit() {
super.ngOnInit();
}
}
#NgModule({
imports: [
...
],
exports: [...],
declarations: [
...,
ProductDirective
],
entryComponents: [...],
providers: [
...
]
})
export class CategoryListViewModule {
}
AngularJS component used in the Angular component template
<lp-product-ng2
*ngIf="selectedProduct"
[productFamily]="selectedProduct"
[channel]="channel"
[datastandardId]="datastandardId">
</lp-product-ng2>
The *ngIf resolves on (click) of another element, so the exception does not throw until the element is in the DOM. If I remove the *ngIf, the exception is thrown immediately, but originating from another part of the code.
I fear that the issue lies within the nesting of components, but I've got no evidence.

it should be :
#Inject(ElementRef)
and
#Inject(Injector)

Related

#angular/router not working inside an angular.js application

I'm working on migrating little by little a big angular.js application (that uses ui-router) to angular and I opted by using the angular.js application as a base and migrate the different routes one at a time so that once I'm finished I switch everything at once to angular.
These are the steps I've followed:
Bootstap the angular.js application in my main.ts file:
export function bootstrapAngular(extra: StaticProvider[]): any {
setAngularJSGlobal(angular);
if (environment.production) {
enableProdMode();
}
return platformBrowserDynamic(extra)
.bootstrapModule(AppModule)
.catch(err => console.log(err));
}
const downgraded = angular
.module('downgraded', [downgradeModule(bootstrapAngular)])
.directive('appRoot', downgradeComponent({ component: RootComponent, propagateDigest: false }))
;
angular.bootstrap(document, ['app', downgraded.name]);
Inside my index.html
<app id="app"></app>
This works fine.
Inside my main angular.js component I add the tag of my downgraded main Angular component:
<div class="app__view" id="appContent">
<ui-view></ui-view>
<app-root></app-root>
</div>
This is how my main module is configured
const COMPONENTS = [
TestComponent,
RootComponent,
];
#NgModule({
declarations: COMPONENTS,
imports: [
BrowserModule,
NxModule.forRoot(),
RouterModule.forRoot(routes, {
initialNavigation: 'enabled',
useHash: true
})
],
providers: [
{ provide: APP_BASE_HREF, useValue: '/' }
],
entryComponents: COMPONENTS,
exports: COMPONENTS
})
export class AppModule {
ngDoBootstrap(): void { }
}
Everything works fine so far. I can see my angular component inside my angular.js application.
The problem comes when I add the to my main root component
I can see the router-outlet rendering but nothin next to it, eventhough the route matches.
export const routes: Route[] = [
{ path: 'dashboard', component: TestComponent }
];
When I point my browser to /#/dashboard this is the router tracing that I see:
And the test component just doesn't render.
I need some help, can't think of anything else to try.
First of all: if you want to go hybrid and start moving parts of ng1 to ngx, you need to bootstrap your ng1 app from ngx as you did, but not by downgrading:
platformBrowserDynamic().bootstrapModule(AppModule).then(platformRef => {
(<any>platformRef.instance).upgrade.bootstrap(document.body, ['nameOfNg1App']);
});
Than you should provide the entry point for ui-router within your app.component.html:
<div ui-view></div>
You also need to provide an url handling strategy to tell angular, which routes to handle. I had an AppRoutingModule, which was imported by the AppModule. And this one provided the handler:
#NgModule({
imports : [
RouterModule.forRoot(routes, {useHash: true})
],
exports : [
RouterModule
],
// provide custom UrlHandlingStrategy to separate AngularJs from Angular routes
providers: [
{
provide : UrlHandlingStrategy,
useClass: Ng1Ng2UrlHandlingStrategy
}
]
})
export class AppRoutingModule {
}
And the Handling strategy, I used path prefixes to separate ng1 from ngx routes, but you can choose a simpler separation if needed:
import { UrlHandlingStrategy } from '#angular/router';
export class Ng1Ng2UrlHandlingStrategy implements UrlHandlingStrategy {
private ng1Urls: string[];
constructor() {
this.ng1Urls = [
'prefix1',
];
}
shouldProcessUrl(url) {
url = url.toString();
return this.ng1Urls.findIndex(ng1Url => new RegExp(`^\/${ng1Url}([|\?|\/](.*)){0,1}$`).test(url)) === -1;
}
extract(url) {
return url;
}
merge(url, whole) {
return url;
}
}
Oh, and for some reasons I had to stick to # URLs, while running hybrid.
With this setup you start an ngx app, that has a container that runs the ui-router. Within your ng1 app you can then use downgraded ngx components and services.
To have ngx-routes and ng1 routes in parallel, your (ngx) app.component.html consists of
<div ui-view></div>
<router-outlet></router-outlet>
You find more details of this strategy here: https://blog.nrwl.io/upgrading-angular-applications-upgrade-shell-4d4f4a7e7f7b
The solution involved:
Getting rid of UrlHandlingStrategy completely
Disabling initialNavigation
Creating empty routes in both route configurations
Injecting the ng2 router in the ng1 application and adding the following logic
angular.module('app').config(['$stateProvider', '$urlRouterProvider', function($stateProvider, $urlRouterProvider) {
$stateProvider.state('ng2', { });
$urlRouterProvider.otherwise(($injector, $location) => {
const $state = $injector.get('$state');
const ng2Routes = ['dashboard'];
const ng2Router = $injector.get('ng2Router');
const url = $location.url();
if (ng2Routes.some(feature => url.startsWith('/' + feature))) {
$state.go('ng2');
ng2Router.navigate([url]);
} else {
$state.go('messages');
}
});
}]);

Angular UpgradeModule: can't get element to bind during app bootstrap

I have an old AngularJS app I'm trying to implement an upgrade-in-place using the Angular 6 UpgradeModule. I can get all the code to execute -- I'm logging out states as expected through both the Angular 6 and AngularJS apps.
The problem is that I'm failing utterly to bind anything to the DOM.
All the documentation and examples use NgDoBootstrap thus, inside the core AppModule of the new Angular 6 app:
this.upgrade.bootstrap(document.body, ['angularJS-app-name'], {strictDi: true});
I can execute that. I can see my AngularJS app bootstrapping (via console.logs) via the UpgradeModule. I can see my Angular 6 app bootstrapped (via console.logs). But nothing is bound to the DOM.
Logging out document gives me the HTML document I'd expect. I can manually examine that in the Chrome console, and see all the elements I would expect to. But all document methods and properties seem to be returning null.
document.body: null.
document.getElementById('an-elementId-I-can-see-when-logging-out-document'): null.
Tell me I'm just doing something dumb like not injecting something properly so that Angular/TS is interpreting document differently than vanilla JS does.
master.app.ts
import {APP_BASE_HREF} from "#angular/common";
import {Component, NgModule, Inject} from '#angular/core';
import {BrowserModule} from '#angular/platform-browser';
import {UpgradeModule} from '#angular/upgrade/static';
import {platformBrowserDynamic} from '#angular/platform-browser-dynamic';
import {RouterModule, Routes, UrlHandlingStrategy} from '#angular/router';
#Component({
selector: 'ng6-router-root',
template: '<router-outlet></router-outlet><div class="ng-view"></div>'
})
export class Ng6RouterRoot{}
export class HybridUrlHandlingStrategy implements UrlHandlingStrategy {
shouldProcessUrl(url: any) {return false;}
extract(url:any) {return url;}
merge(url:any, whole:any) {return url;}
}
#NgModule({
declarations: [
Ng6RouterRoot
],
imports: [
BrowserModule,
UpgradeModule,
RouterModule.forRoot([])
],
providers: [
{ provide: UrlHandlingStrategy, useClass: HybridUrlHandlingStrategy },
{ provide: APP_BASE_HREF, useValue: '/' }
]
})
export class AppModule {
constructor (private upgrade: UpgradeModule) {
}
ngDoBootstrap() {
console.log('master.app.ts ngDoBootstrap start', document);
console.log('document.body', document.body);
this.upgrade.bootstrap(document.getElementById('master'), ['angularJsApp'], {strictDi: true});
console.log('master.app.ts bootstrap end');
}
}
platformBrowserDynamic().bootstrapModule(AppModule);
console.log('master.app.ts end readyState', document.readyState);
relevant html
<div id="master">
<ng6-router-root>
</ng6-router-root>
</div>

Angular and AngularJS Hybrid Application Routing: Angular component as child state not rendering

first some short introduction to the project and general setup.
It is an Angular/Angular JS application. I integrated Angular couple of weeks ago. In contrast to many different tutorials using the UpgradeModule, I actually had to use the downgradeModule - The project is quite large and UpgradeModule caused a lot of performance issues.
There is an overall Parent State (called app) and I want a Angular Component to be a child of it. According to the docs this should be possible (https://github.com/ui-router/angular-hybrid#limitations)
Limitations:
We currently support routing either Angular (2+) or AngularJS (1.x) components into an AngularJS (1.x) ui-view. However, we do not support routing AngularJS (1.x) components into an Angular (2+) ui-view.
If you create an Angular (2+) ui-view, then any nested ui-view must also be Angular (2+).
Because of this, apps should be migrated starting from leaf states/views and work up towards the root state/view.
The general setup looks like this (simplification):
app.module.ng1.ts
import { AppModule } from './app.module';
const bootstrapFn: any = (extraProviders: Array<StaticProvider>): any => {
return platformBrowserDynamic(extraProviders).bootstrapModule(AppModule);
};
const downgradedModule: any = downgradeModule(bootstrapFn);
const appModule: angular.IModule = angular
.module('app', [
downgradedModule,
// other project modules
]);
app.module.ts
#NgModule({
imports: [
BrowserModule,
UIRouterUpgradeModule.forChild(),
],
declarations: [
AccountNg2Component,
],
providers: [
],
entryComponents: [
AccountNg2Component,
],
})
class AppModule {
public ngDoBootstrap(): void {}
}
export { AppModule };
TheAccountNg2Component is the one I actually want to go to. account.component.ts
#Component({
selector: 'account',
template,
})
class AccountNg2Component {
#Input() public user: any;
constructor() {}
}
export { AccountNg2Component };
There is a parent app state and I want the AccountNg2Component to be a child of it. The state configuration looks like this:
$stateProvider
.state({
parent: 'app',
name: 'account',
url: '/account',
component: AccountNg2Component,
});
Whatever I try it will also result in the following two Errors:
Transition Rejection($id: 0 type: 6, message: The transition errored, detail: TypeError: Cannot read property 'when' of undefined)
TypeError: Cannot read property 'when' of undefined
at Ng2ViewConfig.load (views.js:47)
at eval (views.js:19)
at Array.map (<anonymous>)
at loadEnteringViews (views.js:19)
at invokeCallback (transitionHook.js:104)
at TransitionHook.invokeHook (transitionHook.js:116)
at eval (transitionHook.js:58)
at processQueue (angular.js:17169)
at eval (angular.js:17217)
at Scope.$digest (angular.js:18352)
at Scope.$apply (angular.js:18649)
at eval (angular.js:18952)
at completeOutstandingRequest (angular.js:6428)
at eval (angular.js:6707)
at ZoneDelegate.invokeTask (zone.js:420)
at Object.onInvokeTask (core.js:4961)
at ZoneDelegate.invokeTask (zone.js:419)
at Zone.runTask (zone.js:187)
at ZoneTask.invokeTask (zone.js:495)
at ZoneTask.invoke (zone.js:484)
at timer (zone.js:2053)
I'm probably missing something in the configuration, but I'm not able to figure it out.
What I already tried:
I looked at the sample App (https://github.com/ui-router/sample-app-angular-hybrid) and tried to build it as similar as possible. But they are using the UpgradeModule instead of the downgrade - I don't know if this changes anything for the router.
I tried
Adding state configuration to UIRouterUpgradeModule.forChild() and UIRouterModule.forChild()
Created a "future state" according to https://github.com/ui-router/sample-app-angular-hybrid/blob/master/app/angularModule.ts#L10
Different ways to declare the Account State
Different ways to define the Account Component itself
The error stays always the same, because of that I think I'm just missing some piece in my configuration.
If my description does not help enough, I'll try to setup a jsfiddle or something similar
Update 1:
Ok, I removed the state declaration for the account state from the Angular 1 State Provider and instead only register it in the UIRouterModule. Now at least the error is gone, but the state is not loaded at all (when trying to access it, redirect to default state)
Ok I finally managed to solve the issue, thanks to a tip from a different article (https://stackoverflow.com/a/49568050/4243635)
Just gonna quote it here again:
The Angular bootstrap module needed a parameter of type "UIRouter" in the constructor, otherwise it would not bootstrap its states:
export class AppModule {
constructor(private router: UIRouter) {
// "router" needed in constructor to bootstrap angular states
}
You also need to import UpgradeModule and UIRouterUpgradeModule. So the entire app.module.ts looks like this:
import { NgModule } from '#angular/core';
import { BrowserModule } from '#angular/platform-browser';
import { ServiceBootstrapComponent } from '../../service-bootstrap';
import { AccountNg2Component } from '../../app/pages/account/account.ng2.component';
import { UIRouterUpgradeModule } from '#uirouter/angular-hybrid';
import { AccountState } from '../../app/pages/account/account.states';
import { CommonModule } from '#angular/common';
import { UIRouter, UIRouterModule } from '#uirouter/angular';
import { UpgradeModule } from '#angular/upgrade/static';
#NgModule({
imports: [
CommonModule,
BrowserModule,
UpgradeModule,
UIRouterUpgradeModule,
UIRouterModule.forChild({states: [AccountState]}),
],
declarations: [
ServiceBootstrapComponent,
AccountNg2Component,
],
providers: [
],
entryComponents: [
ServiceBootstrapComponent,
],
})
class AppModule {
constructor(private router: UIRouter) {}
public ngDoBootstrap(): void {}
}
export { AppModule };

Using services in Angular/AngularJS hybrid app (ng-upgrade)

Currently building a slimmed down version of the app for plunker so I can SHOW you my problem, but in case anyone has any tips off the top of their heads in the mean time, I will attempt to describe my problem first.
I'm using ngUpgrade to start bringing a large application from AngularJS to Angular. I've got a core application running using Angular. Briefly it's set up a little like this:
#Component({
selector: '[my-app]',
template: `
<app-main></app-main>
`
})
export class AppComponent {};
#NgModule({
imports: [
BrowserModule,
UpgradeModule,
],
bootstrap: [AppComponent],
declarations: [
AppComponent
],
schemas: [
CUSTOM_ELEMENTS_SCHEMA
]
})
export class AppModule {
constructor(public upgrade: UpgradeModule) { }
}
export const Ng1AppModule = angular.module(
'mainApp',
[
'feature.one'
]
);
platformBrowserDynamic().bootstrapModule(AppModule).then(ref => {
ref.instance.upgrade.bootstrap(document.body, [Ng1AppModule.name], {});
});
It successfully bootstraps itself and runs a root component which essentially loads the old AngularJS application. So far no major problems.
The AngularJS application has dependencies on a lot of custom feature modules which I now need to convert to Angular.
On one of the feature modules I want to convert a service. It's now an Angular #Injectable built in typescript and it is assigned to and AngularJS module like so:
export const Ng1FeatureModule = angular
.module('feature.one', ['ngCookies'])
.service('UpgradedService', downgradeInjectable(UpgradedService) as any);
This service requires a dependency from a service I have not even converted yet.
Example:
#Injectable()
export class UpgradedService{
public var1: string;
constructor(private nonconvertedNG1Service: NonconvertedNG1Service) {
this.var1 = nonconvertedNG1Service.get();
}
public getVar1() {
return this.var1;
}
}
How do I need to set things up so that my example app uses 'UpgradedService' and UpgradedService is able to use NonconvertedNG1Service?

Angular UpgradeComponent cannot find $scope

I have a hybrid angular-cli that roughly follows Victor Savkin's Lazy Loaded AngularJS guide. AngularJS is bootstraped in the constructor of a LazyLoaded Angular module. The main difference between my app and the guide is that I am trying to wrap the <ui-view> directive inside of some Angular components. Because of how my layout is structured the <ui-view> element will not be available when AngularJS is bootstrapped and may be added or removed at any time.
import { Component, Directive, ElementRef, Injector } from '#angular/core';
import { UpgradeComponent } from '#angular/upgrade/static';
import * as angular from 'angular';
#Component({
template: `
<layout-wrapper>
<my-toolbar></my-toolbar>
<layout-contents>
<ng2-ui-view>
<h3 class="text-center">AngularJS page not loaded</h3>
</ng2-ui-view>
</layout-contents>
</layout-wrapper>
`,
})
export class LegacyOutputComponent { }
#Directive({selector: 'ng2-ui-view'})
export class UpgradedUiViewComponent extends UpgradeComponent {
constructor(ref: ElementRef, inj: Injector) {
super('uiViewWrapper', ref, inj);
}
}
export const routerPatchModule = 'arcs.router.patch';
// We need to define a wrapper for ui-view because we can only upgrade
// components with only one definition. uiView cannot be automatically
// upgraded because its definition is too complex
angular.module(routerPatchModule, ['ui.router'])
.component('uiViewWrapper', { template: '<ui-view></ui-view>'})
When I run the code a Error: No provider for $scope! error is thrown. Checking the stack trace I can see that it is thrown in the UpgradeComponent super class. The injector tries to get $scope and
Alternative is to let Angular know that it needs to provide the $scope.
import { Injector } from '#angular/core';
// allow $scope to be provided to ng1
export const ScopeProvider = {
deps: ['$injector'],
provide: '$scope',
useFactory: (injector: Injector) => injector.get('$rootScope').$new(),
};
#Directive({
providers: [ ScopeProvider ],
selector: 'ng2-ui-view',
})
export class UpgradedUiViewComponent extends UpgradeComponent {
constructor(ref: ElementRef, inj: Injector) {
super('uiViewWrapper', ref, inj);
}
}
This setup will not work. AngularJS needs to be able to load in the root of your application in order for the scope to be defined correctly.
A better way to approach this problem is to use the <div ui-view> directive in the root of your application (as in the upgrade guide) and then to downgrade a layout component from Angular into AngularJS to wrap your content.

Resources