Angular 7 Creating dynamic modules with compileModuleAndAllComponentsAsync aot build issue - javascript

Problem :
I have to render a dynamic html which generates at run-time as a string, so for that purpose I've used dynamic components approach as innerHTML just not cater angular attributues like *ngIf, formGroup, router-link etc, but getting following error after deploying on local server.
Error :
Error: Unexpected value 'function(){}' imported by the module 'function(){}'. Please add a #NgModule annotation.
I tried creating the dynamic module but was getting error when making aot build because it was inside a non lazy-loaded module and according to a solution I found that aot will not work for dynamic components generating in a lazy-loaded module.
Now when I have moved it in a non lazy-loaded module I am not getting any error locally when generating aot build but getting this error after deployment however deployment is successful and nothing is breaking there.
DYNAMIC MODULE
export function createCompiler(compilerFactory: CompilerFactory) {
return compilerFactory.createCompiler();
}
#NgModule({
declarations: [StepComponent],
imports: [
CommonModule
],
exports: [StepComponent],
entryComponents: [StepComponent],
providers: [
{ provide: COMPILER_OPTIONS, useValue: {}, multi: true },
{ provide: CompilerFactory, useClass: JitCompilerFactory, deps: [COMPILER_OPTIONS] },
{ provide: Compiler, useFactory: createCompiler, deps: [CompilerFactory] }
]
})
export class DynamicStepModule { }
STEP COMPONENT
export class StepComponent implements OnInit, OnChanges {
#Input() step: Steps;
#ViewChild('dynamicComponent', { read: ViewContainerRef }) container: ViewContainerRef;
constructor(private compiler: Compiler) { }
ngOnInit() { }
ngOnChanges(change: SimpleChanges) {
if (this.step && change.step.currentValue !== change.step.previousValue) {
this.addComponent(this.step);
}
}
addComponent(step: Steps) {
const cmp = Component({ template: step.formattedText })(class DynamicComponent {
stepFormGroup: FormGroup;
constructor(
) {
// Some Component related code
}
});
const meta = NgModule({
imports: [
CommonModule,
FormsModule,
MaterialModule,
ReactiveFormsModule,
LibSharedModule,
BrowserModule,
BrowserAnimationsModule
],
declarations: [cmp]
})(class DynamicModule {
});
this.compiler
.compileModuleAndAllComponentsAsync(meta)
.then(factories => {
const factory = factories.componentFactories.find(component => component.componentType === cmp);
this.container.remove();
this.container.createComponent(factory);
});
}
}
Locally when generating aot build not getting error but after deploying on a local server getting this issue:
ERROR Error: Unexpected value 'function(){}' imported by the module 'function(){}'. Please add a #NgModule annotation.

Related

Exporting variable in angular AoT compiler

I tried to implement dynamic configuration as can be seen in this post.
Everything works in JiT compiler, but I get
ERROR in Error during template compile of 'environment'
Function calls are not supported in decorators but 'Environment' was called.
when trying to build with the AoT compiler.
This is my environment.ts (note class Environment is exported):
export class Environment extends DynamicEnvironment {
public production: boolean;
constructor() {
super();
this.production = false;
}
}
export const environment = new Environment();
I would still like to use the environment in the standard way some.component.ts:
import { environment } from '../environments/environment';
console.log(environment.config.property);
Don't. Seriously, stay away from those two files (environment.ts and environment.prod.ts). Those are NOT about the DevOps meaning of the word "environment", they are about debug constants.
If you need to know if you're running a debug build, import isDevMode:
import { isDevMode } from '#angular/core';
If you need dynamic configuration, just read a Json from somewhere or have the server side inject it as a script tag, then read it directly or via Dependency Injection (it's not that hard to do).
But don't mess with those files. Trust me, you'll thank me later ;)
Solved this by creating config.module.ts and config.service.ts. Config module declares providers:
#NgModule({
providers: [
ConfigService,
{
provide: APP_INITIALIZER,
useFactory: (appConfigService: ConfigService) => () => appConfigService.loadAppConfig(),
deps: [ConfigService],
multi: true,
},
],
})
export class ConfigModule {}
Usage of config service in some.component.ts:
#Component(...)
export class SomeComponent {
constructor(private configService: ConfigService) { }
private myMethod() {
console.log(this.configService.get.property);
}
}
For tests, json testing config file is imported:
import { default as appTestConfig } from '../../../../assets/app-config.test.json';
and set directly on config service:
TestBed.configureTestingModule({
...,
imports: [
ConfigModule,
...
],
providers: [
{
provide: APP_INITIALIZER,
useFactory: (appConfigService: ConfigService) => () => appConfigService.setConfig(appTestConfig),
deps: [ConfigService],
multi: true,
},
]
}).compileComponents();

Routing Module loads before APP_INITIALIZER

I have a value that is from of a config file from static AppConfigService.
Described below:
reference code/article: https://blogs.msdn.microsoft.com/premier_developer/2018/03/01/angular-how-to-editable-config-files/
import { Injectable } from '#angular/core';
import { AppConfig } from './app-config';
import { HttpClient } from '#angular/common/http';
import { environment } from 'src/environments/environment';
#Injectable()
export class AppConfigService {
static settings: AppConfig;
constructor(private http: HttpClient) { }
load() {
console.log('is this getting fired before routing module check?');
const jsonFile = `assets/config/config.${environment.name}.json`;
return new Promise<void>((resolve, reject) => {
this.http.get(jsonFile)
.toPromise()
.then((response: AppConfig) => {
AppConfigService.settings = <AppConfig>response;
console.log(AppConfigService.settings);
resolve();
})
.catch((response: any) => {
reject(`Could not load file '${jsonFile}':
${JSON.stringify(response)}`);
});
});
}
}
This config gets loaded in my APP_INITIALIZER in the app.module.ts
providers: [
AppConfigService,
{
provide: APP_INITIALIZER,
useFactory: (appConfigService: AppConfigService) => () => {appConfigService.load() },
deps: [AppConfigService], multi: true
}
],
but my routing module, named AppRoutingModule is reading something out of my AppConfigService.settings variable which is crazy enough, UNDEFINED. My application crashes. I expect the APP_INITIALIZER to fire BEFORE AppRoutingModule but this is not the case:
Uncaught TypeError: Cannot read property 'oldUrl' of undefined
oldUrl is a property of AppConfigService.settings. I checked if AppConfigService.settings is set, it IS, properly AFTER routing module is fired but this is not what I want.
I checked some other sources for help. I used the following already as maybe a fix: https://github.com/angular/angular/issues/14615 and https://github.com/angular/angular/issues/14588
#component({})
class App {
constructor(router: Router, loginService: LoginService) {
loginService.initialize();
router.initialNavigation();
}
}
#NgModule({
imports: [
BrowserModule,
RouterModule.forRoot(routes, {initialNavigation: false})
],
declarations: [ App ],
bootstrap: [ App ],
providers: [ Guard, LoginService ]
})
export class AppModule {
}
Unfortunately, the above solution is not fixing my problem. I also tried to put in AppModule but alas, that didn't help either.
Any help is very welcome.
I've solved my App Initialization and Routing with NgRx listening the central state to know when the system is Loaded and activating the route Guards after that.
But for a direct solution, you need to add a Route Guard checking when your service is loaded. So, add a loaded: boolean flag in your Service, and check it from a Guard like this:
https://github.com/angular/angular/issues/14615#issuecomment-352993695
This is better handled with Observables tho, and I'm wiring all with NgRx in my Apps using Facades to facilitate everything:
https://gist.github.com/ThomasBurleson/38d067abad03b56f1c9caf28ff0f4ebd
Best regards.

Error when using AngularJS component inside Angular app: "Error: Trying to get the AngularJS injector before it being set."

I'm working through Angular's upgrade guide to learn how to embed AngularJS components in an Angular app. I've created a bare-bones Angular app using the Angular CLI and added a simple AngularJS module as a dependency.
When I run ng serve, the application compiles with no errors. However, at runtime, I get this message in the console:
Error: Trying to get the AngularJS injector before it being set.
What is causing this error, and how can I avoid it? I haven't deviated from the steps detailed in the upgrade guide.
Here's how I'm upgrading my AngularJS component inside my Angular app:
// example.directive.ts
import { Directive, ElementRef, Injector } from '#angular/core';
import { UpgradeComponent } from '#angular/upgrade/static';
// this is the npm module that contains the AngularJS component
import { MyComponent } from '#my-company/module-test';
#Directive({
selector: 'my-upgraded-component'
})
export class ExampleDirective extends UpgradeComponent {
constructor(elementRef: ElementRef, injector: Injector) {
// the .injectionName property is the component's selector
// string; "my-component" in this case.
super(MyComponent.injectionName, elementRef, injector);
}
}
And here's my app.module.ts:
// app.module.ts
import { BrowserModule } from '#angular/platform-browser';
import { NgModule } from '#angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { UpgradeModule } from '#angular/upgrade/static';
import { ExampleDirective } from './example.directive';
import { myModuleName } from '#my-company/module-test';
#NgModule({
declarations: [AppComponent, ExampleDirective],
imports: [BrowserModule, AppRoutingModule, UpgradeModule],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule {
constructor(private upgrade: UpgradeModule) {}
ngDoBootstrap() {
this.upgrade.bootstrap(document.body, [myModuleName], {
strictDi: true
});
}
}
I'm using Angular 5.2.0.
I faced the same issue, and finally solved it. There are some steps to follow before bootstrap an hybrid Angular/angularjs application.
Install the UpgradeModule npm install #angular/upgrade
Wrap your "CompanyModule" (the module where all your company components are registered) into a new angularjs module (for instance: Ng1Shared). If you not have a module for your company components, it must be created. Than downgrade AppComponent as shown below.
const MyCompanyModule = angular
.module('MyCompanyModule', [])
.component('myComponent', MyComponent)
.name;
const Ng1Shared = angular
.module('Ng1Shared', [MyCompanyModule])
.directive('appRoot', downgradeComponent({ component: AppComponent }))
.name;
Configure AppModule with basic imports (BrowserModule, CommonModule, UpgradeModule). Provide the angularjs' Injector to Angular; declare an "entryComponent" and remove the default bootstrap for AppComponent.
#NgModule({
imports: [BrowserModule, CommonModule, UpgradeModule],
declarations: [AppComponent],
providers: [{provide: '$scope', useExisting: '$rootScope'}], // REQUIRED
entryComponents: [AppComponent], // ADD AN ENTRY COMPONENT
// bootstrap: [AppComponent] MUST BE REMOVED
})
Set angularjs globally with a function provided by UpgradeModule itself and manually bootstrap Angular with DoBootstrap method provided by #angular/core.
export class AppModule implements DoBootstrap {
constructor(private upgrade: UpgradeModule) { }
public ngDoBootstrap(app: any): void {
setAngularJSGlobal(angular);
this.upgrade.bootstrap(document.body, [Ng1Shared], { strictDi: false });
app.bootstrap(AppComponent);
}
}
Create a wrapper directive for every angularjs component and add it to AppModule's declaration array.
#Directive({
selector: 'my-component'
})
export class MyComponentWrapper extends UpgradeComponent {
#Input() title: string;
constructor(elementRef: ElementRef, injector: Injector) {
super('myComponent', elementRef, injector);
}
}
I wrote a simple example available on stackblitz.
For example purposes I added angularjs MyCompanyModule to another angularjs module, called Ng1Module. As you can see also property binding between angularjs and angular component works fine.
I hope it can be useful.
https://github.com/angular/angular/issues/23141#issuecomment-379493753
you cannot directly bootstrap an Angular component that contains
upgraded components before bootstrapping AngularJS. Instead, you can
downgrade AppComponent and let it be bootstrapped as part of the
AngularJS part of the app:
https://stackblitz.com/edit/angular-djb5bu?file=app%2Fapp.module.ts
try to add an entryComponents to your AppModule like this :
...
#NgModule({
declarations: [AppComponent, ExampleDirective],
imports: [BrowserModule, AppRoutingModule, UpgradeModule],
entryComponents: [
AppComponent // Don't forget this!!!
],
providers: [],
// bootstrap: [AppComponent] // Delete or comment this line
})
...

Angular passing dynamic entry components into module and then passing them again into another module

I'm making a modal component for a component library. I made a 3rd party modal library that I'm using within my component library. A main feature is being able to pass a component via a service and dynamically adding it to the modal.
My modal lib has a static method that allows you to add your component to the module's entry components. It looks like:
export class A11yModalModule {
static withComponents(components: any[]) {
return {
ngModule: A11yModalModule,
providers: [{
provide: ANALYZE_FOR_ENTRY_COMPONENTS,
useValue: components,
multi: true
}]
};
}
}
Cool, that works. I can pass components into it when I import the module like this: A11yModalModule.withComponents([ModalContentComponent])
My problem occurs when I abstract this out another level. So now instead of 2 modules I have 3. I need to pass a component like I did above from the lib consumer's module, to my component module, and then into the modal module.
How can I pass components from the lib module to the modal module?
I think I'm getting close. Here are my 3 modules
// app module
#NgModule({
declarations: [AppComponent, ModalContentComponent],
imports: [
LibModalModule.withComponents([ModalContentComponent])
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
// lib module
#NgModule({
imports: [CommonModule],
declarations: [LibModal],
providers: [LibModalService],
exports: []
})
export class LibModalModule {
static withComponents(components: any[]) {
return {
ngModule: LibModalModule,
imports: [CommonModule, A11yModalModule.withComponents(components)]
};
}
}
// a11y modal module
#NgModule({
imports: [CommonModule],
declarations: [ModalComponent],
exports: [],
providers: [ModalService, DomService],
entryComponents: [ModalComponent]
})
export class A11yModalModule {
static withComponents(components: any[]) {
return {
ngModule: A11yModalModule,
providers: [{
provide: ANALYZE_FOR_ENTRY_COMPONENTS,
useValue: components,
multi: true
}]
};
}
}
withComponents method should return ModuleWithProviders object which is just wrapper around a module that also includes the providers.
It can't have imports property or something else because it doesn't understand those properties. Here's an excerpt from angular source code that is responsible from reading metadata from ModuleWithProviders:
else if (importedType && importedType.ngModule) {
const moduleWithProviders: ModuleWithProviders = importedType;
importedModuleType = moduleWithProviders.ngModule;
if (moduleWithProviders.providers) {
providers.push(...this._getProvidersMetadata(
moduleWithProviders.providers, entryComponents,
`provider for the NgModule '${stringifyType(importedModuleType)}'`, [],
importedType));
}
}
As we can see angular compiler takes providers from the object that will returned in withComponents method.
So, in order to merge your modules you can either use your approach(provide ANALYZE_FOR_ENTRY_COMPONENTS in LibModalModule.withProviders) or reuse A11yModalModule.withComponents like:
#NgModule({
imports: [CommonModule, A11yModalModule],
providers: [LibModalService],
exports: []
})
export class LibModalModule {
static withComponents(components: any[]) {
return {
ngModule: LibModalModule,
providers: A11yModalModule.withComponents(components).providers
};
}
}
(Tested with AOT)
Also A11yModalModule has to be imported in LibModalModule if we want its providers to be included in our root module injector (And i suppose you're going to use ModalService and DomService that are declated in A11yModalModule). The reason of this is that angular includes all providers from transitive module in root module injector.
See also:
Avoiding common confusions with modules in Angular
What you always wanted to know about Angular Dependency Injection tree
I had a bug that was giving me a false flag. Turns out you can just add the same withComponents method to the component library module and it passes the component through. I'd love an explanation on how this works if anyone knows.
// app module
#NgModule({
declarations: [AppComponent, ModalContentComponent],
imports: [
LibModalModule.withComponents([ModalContentComponent])
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
// lib module
#NgModule({
imports: [CommonModule, A11yModalModule],
declarations: [LibModal],
providers: [LibModalService],
exports: []
})
export class LibModalModule {
static withComponents(components: any[]) {
return {
ngModule: LibModalModule,
providers: [{
provide: ANALYZE_FOR_ENTRY_COMPONENTS,
useValue: components,
multi: true
}]
};
}
}
// a11y modal module
#NgModule({
imports: [CommonModule],
declarations: [ModalComponent],
exports: [],
providers: [ModalService, DomService],
entryComponents: [ModalComponent]
})
export class A11yModalModule {
static withComponents(components: any[]) {
return {
ngModule: A11yModalModule,
providers: [{
provide: ANALYZE_FOR_ENTRY_COMPONENTS,
useValue: components,
multi: true
}]
};
}
}

Angular condition in type provider with AOT

I have an Angular project which I compile with AOT. I want to be able to register ClassProvider that is resolved dynamically according to configuration. Simplified code I use is this:
const isMock = Math.random() > 0.5;
#NgModule({
// ...
providers: [
{ provide: MyServiceBase, useClass: (isMock) ? MyServiceMock : MyService },
],
bootstrap: [AppComponent]
})
export class AppModule { }
The problem is when I compile this with AOT I always get the same service. I would expect to get different service while hitting F5 (because of the randomness on the first line). When compiling without AOT it behaves as I expect.
Here is the whole code example on github: https://github.com/vdolek/angular-test/tree/aot-conditioned-provider-problem. It behaves differently with ng serve and ng serve --aot.
How can I achieve this? I know I could use FactoryProvider, but then I would have to duplicate the services dependencies (parameters of the factory function and deps property on the FactoryProvider).
To achieve the dynamic nature of your requirement, you need to use factory providers, via the useFactory attribute.
I've forked your repository, and amended your app.module.ts as follows, to work in AOT.
Amend app.module.ts as follows
export let myServiceFactory = () => {
const isMock = Math.random() > 0.5;
return isMock ? new MyServiceMock() : new MyService();
};
#NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule
],
providers: [
{provide: MyServiceBase, useFactory: myServiceFactory},
],
bootstrap: [AppComponent]
})
export class AppModule {
}
In the case that your service is dependent on other services, which, most likely it will, you can use the deps argument, to pass on the required dependencies.
Let's say that MyServiceBase is dependent on two services, MyService1 and MyService2... Your factory function will look as follows :
export let myServiceFactory = (service1:MyService1, service2:MyService2) => {
const isMock = Math.random() > 0.5;
return isMock ? new MyServiceMock(service1, service2) : new MyService(service1, service2);
};
and your providers decleration would look as follows
providers: [
{
provide: MyServiceBase,
useFactory: myServiceFactory,
deps: [MyService1, MyService2]
},
]
This guide contains further detail on the various ways of achieving dependency injection in Angular.
I think as #jeanpaul-a said you don't have any choice other than to use factory. But managing dependencies could be not very clean. But what you could use is the Injector. I'll go with something like:
#NgModule({
imports: [ BrowserModule, FormsModule ],
declarations: [ AppComponent, HelloComponent ],
providers: [
Dep1Service,
Dep2Service,
{ provide: MyServiceBase, useFactory: createService, deps: [Injector] }
],
bootstrap: [ AppComponent ]
})
export class AppModule { }
export function createService(injector: Injector) {
const isMock = Math.random() > 0.5;
if (mock) {
return new MyService1(injector.get(Dep2Service));
} else {
return new MyService2(injector.get(Dep1Service));
}
}
What you could also do is to set MyServiceBase as an interface and use InjectionToken.
You will find a working example here (not your class name however).

Categories