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.
Related
I have an angularjs 1.7 component which I need to upgrade to angular 8 component. It has an external script, which I cannot modify. That script inserts an iframe into the div and it expects some settings from the component to customize the iframe.
The old component code:
angular.module('myApp.shared').component("userExternal", {
template: '<div id="userIframe"></div>',
controller: function ($window) {
this.scriptUrl = "//myurl/widget/addIframe.js";
this.$onInit = function () {
$window.UserSettings = [];
$window.UserSettings.push(['set', {
btn_color: '#008A00',
bg_color: 'white'
}]);
});
};
}
});
I have two problems here:
I don't know how to convert $widnow to angular 8 window object.
When I convert $window to angular 8 window, how can I add UserSettings array to it?
This is my angular 8 component, but my code did not work correctly.
HTML Template
<script src="//myurl/widget/addIframe.js"></script>
<div class="user_external></div>
TS Code
import { Component} from '#angular/core';
#Component({
selector: 'app-user',
templateUrl: './user-external.component.html'
})
export class UserExternalComponent {
constructor() {
}
ngOnInit() {
window.UserSettings = [];
window.UserSettings.push(['set', {
btn_color: '#008A00',
bg_color: 'white'
}]);
console.log(window);
}
}
Thank you
Following a combination of this tutorial for the window reference and this tutorial for upgrading from AngularJS to Angular in general, I created an injectable service that seems to be doing the job, at least so far in a downgraded context. (Next step is to start upgrading the modules that use the dependency, but I successfully replaced all AngularJS injections of $window with my new APIWindow class, and everything works as before with no breaking errors.)
Keeping in mind this is being used as a downgraded Angular class inside a currently mostly AngularJS app, the class looks like this:
// api.window.service.ts
import { Injectable } from '#angular/core'
import { downgradeInjectable } from '#angular/upgrade/static'
import * as angular from 'angular'
// You could change this to return any property on Window, but external is the one I use:
function _external (): any {
return window.external
}
#Injectable()
export class APIWindow {
get external (): any {
return _external()
}
}
angular
.module('APIModule')
.service('APIWindow', downgradeInjectable(APIWindow))
Hopefully this helps someone else with a similar situation following this upgrade path!
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>
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 };
The case
I am in the process of upgrading an AngularJS (i.e. Angular 1.6) application to Angular (i.e. Angular 4.1.3). I chose to do the incremental upgrade so currently both AngularJS and Angular are bootstrapped and running. So far so good, but: One of the AngularJS services that should be rewritten to Angular relies on a well known service $translate which is part of the 3rd party AngularJS (read Angular 1) module pascalprecht.translate. In other words, $translate service is injected to MyService which should become an Angular (read Angular 4) service.
MyService (stripped down) looks like this:
import { Injectable } from '#angular/core';
#Injectable()
export class MyService {
// Here comes the injection (how to do it?).
constructor(private $translate: $translate) {
}
foo() {
// Use the service
this.$translate('HELLO_WORLD');
}
}
It is located within MyModule:
import { NgModule } from '#angular/core';
import { MyService } from './my.service';
#NgModule({
providers: [
MyService
]
})
export class MyModule {
}
The problem
Now, how can I inject $translate into MyService when MyService resides within an Angular module while $translate is part of a 3rd party AngularJS module?
I know how to inject an AngularJS service into an Angular service if the AngularJS service is located within the same module (or at least the module is part of my own solution). It is explained in the official docs. Is there any way to handle a 3rd party service? Do I need to register that service within MyModule's providers?
import { NgModule } from '#angular/core';
import { MyService } from './my.service';
// How this line should look line then?
import { $translate } from 'node_modules/angular-translate/...';
#NgModule({
providers: [
MyService,
$translate
]
})
export class MyModule {
}
Or am I trying to achieve impossible?
Well, after a few hours of struggling I've finally found the solution. Here it is:
Upgrade the 3rd party service
First of all, follow the Angular's official guidelines and upgrade the provider of the 3rd party service - $translate. Like this:
ajs-upgraded-providers.ts
import { InjectionToken } from '#angular/core';
import { downgradeInjectable } from '#angular/upgrade/static';
import 'angular-translate';
// Variable 'i' holds the standard AngularJS injector
export function $translateFactory(i: any) {
return i.get('$translate');
};
// There is no class representing the good old $translate service so we have
// a non-class dependency. Therefore we use an InjectionToken (Angular 4) or
// OpaqueToken (Angular 2).
export let $translate = new InjectionToken('$translate');
// Finally create the upgraded provider
export const $translateProvider = {
provide: $translate,
useFactory: $translateFactory,
deps: ['$injector']
};
One thing to notice, the $translate service might be dependent on other old AngularJS services like (in my case) $translateStaticFilesLoader or $translateMessageFormatInterpolation. If this is also your case, be sure to extend the code above and make upgraded providers for those services as well (keep them in the same file).
Make sure the import statement works
The angular-translate is installed as a node module so the statement
import 'angular-translate';
works just fine if your tsconfig.json is set up to use moduleResolution: "node".
Then, of course, you need to ensure that the import statement will work even after the code is transpiled from TypeScript to ES5 (or whichever target you use) and picked up by a module loader (SystemJS in my case).
Notice that we imported the angular-translate script without getting anything from it, just to cause side effects. Basically, the import ensures that the script containing the desired service $translate is simply executed and registers $translate service for the AngularJS $injector.
Register the upgraded provider in Angular module
Now ensure that the new upgraded provider of $translate service is registered among other providers of MyModule.
my.module.ts
import { NgModule } from '#angular/core';
import { MyService } from './my.service';
import { $translateProvider } from './ajs-upgraded-providers';
#NgModule({
providers: [
MyService,
$translateProvider
]
})
export class MyModule {
}
Use it
Finally, use the $translate service in MyService.
my.service.ts
import { Injectable, Inject } from '#angular/core';
import { $translate } from './ajs-upgraded-providers';
#Injectable()
export class MyService {
// Notice the usage of InjectionToken
constructor(#Inject($translate) private $translate: any) {
}
foo() {
this.$translate('hello.world').then((translation: string) => {
console.log(translation);
});
}
}
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)