I have an angular app that are built using Typescript and bundled together with webpack. Nothing unusual here. What i want to do is to allow plugins on runtime, which means that components and/or modules outside the bundle should be able to be registered in the app as well. So far I've tried to include another webpack bundle in index.html and using an implict array to push said module / component into that, and in my module import these.
See the imports are using an implict variable. This works for modules inside the bundle, but modules in the other bundle will not work.
@NgModule({
imports: window["app"].modulesImport,
declarations: [
DYNAMIC_DIRECTIVES,
PropertyFilterPipe,
PropertyDataTypeFilterPipe,
LanguageFilterPipe,
PropertyNameBlackListPipe
],
exports: [
DYNAMIC_DIRECTIVES,
CommonModule,
FormsModule,
HttpModule
]
})
export class PartsModule {
static forRoot()
{
return {
ngModule: PartsModule,
providers: [ ], // not used here, but if singleton needed
};
}
}
I've also tried creating a module and a component using es5 code, like below, and push the same thing to my modules array:
var HelloWorldComponent = function () {
};
HelloWorldComponent.annotations = [
new ng.core.Component({
selector: 'hello-world',
template: '<h1>Hello World!</h1>',
})
];
window["app"].componentsLazyImport.push(HelloWorldComponent);
Both approaches result in the following error:
ncaught Error: Unexpected value 'ExtensionsModule' imported by the module 'PartsModule'. Please add a @NgModule annotation.
at syntaxError (http://localhost:3002/dist/app.bundle.js:43864:34) [<root>]
at http://localhost:3002/dist/app.bundle.js:56319:44 [<root>]
at Array.forEach (native) [<root>]
at CompileMetadataResolver.getNgModuleMetadata (http://localhost:3002/dist/app.bundle.js:56302:49) [<root>]
at CompileMetadataResolver.getNgModuleSummary (http://localhost:3002/dist/app.bundle.js:56244:52) [<root>]
at http://localhost:3002/dist/app.bundle.js:56317:72 [<root>]
at Array.forEach (native) [<root>]
at CompileMetadataResolver.getNgModuleMetadata (http://localhost:3002/dist/app.bundle.js:56302:49) [<root>]
at CompileMetadataResolver.getNgModuleSummary (http://localhost:3002/dist/app.bundle.js:56244:52) [<root>]
at http://localhost:3002/dist/app.bundle.js:56317:72 [<root>]
at Array.forEach (native) [<root>]
at CompileMetadataResolver.getNgModuleMetadata (http://localhost:3002/dist/app.bundle.js:56302:49) [<root>]
at JitCompiler._loadModules (http://localhost:3002/dist/app.bundle.js:67404:64) [<root>]
at JitCompiler._compileModuleAndComponents (http://localhost:3002/dist/app.bundle.js:67363:52) [<root>]
Please note that if i try with a component instead of a module, i put them in declarations instead, which results in the corresponding error for the components saying i need to add a @pipe/@component annotation instead.
I feel this should be doable, but i don't know what I'm missing. Im using [email protected]
update 11/05/2017
So i decided to take a step back from this and start from scratch. Instead of using webpack I decided to try with SystemJS instead as i found a core component in Angular. This time i got it working using the following component and service to insert components:
typebuilder.ts
import { Component, ComponentFactory, NgModule, Input, Injectable, CompilerFactory } from '@angular/core';
import { JitCompiler } from '@angular/compiler';
import {platformBrowserDynamic} from "@angular/platform-browser-dynamic";
export interface IHaveDynamicData {
model: any;
}
@Injectable()
export class DynamicTypeBuilder {
protected _compiler : any;
// wee need Dynamic component builder
constructor() {
const compilerFactory : CompilerFactory = platformBrowserDynamic().injector.get(CompilerFactory);
this._compiler = compilerFactory.createCompiler([]);
}
// this object is singleton - so we can use this as a cache
private _cacheOfFactories: {[templateKey: string]: ComponentFactory<IHaveDynamicData>} = {};
public createComponentFactoryFromType(type: any) : Promise<ComponentFactory<any>> {
let module = this.createComponentModule(type);
return new Promise((resolve) => {
this._compiler
.compileModuleAndAllComponentsAsync(module)
.then((moduleWithFactories : any) =>
{
let _ = window["_"];
let factory = _.find(moduleWithFactories.componentFactories, { componentType: type });
resolve(factory);
});
});
}
protected createComponentModule (componentType: any) {
@NgModule({
imports: [
],
declarations: [
componentType
],
})
class RuntimeComponentModule
{
}
// a module for just this Type
return RuntimeComponentModule;
}
}
Dynamic.component.ts
import { Component, Input, ViewChild, ViewContainerRef, SimpleChanges, AfterViewInit, OnChanges, OnDestroy, ComponentFactory, ComponentRef } from "@angular/core";
import { DynamicTypeBuilder } from "../services/type.builder";
@Component({
"template": '<h1>hello dynamic component <div #dynamicContentPlaceHolder></div></h1>',
"selector": 'dynamic-component'
})
export class DynamicComponent implements AfterViewInit, OnChanges, OnDestroy {
@Input() pathToComponentImport : string;
@ViewChild('dynamicContentPlaceHolder', {read: ViewContainerRef})
protected dynamicComponentTarget: ViewContainerRef;
protected componentRef: ComponentRef<any>;
constructor(private typeBuilder: DynamicTypeBuilder)
{
}
protected refreshContent() : void {
if (this.pathToComponentImport != null && this.pathToComponentImport.indexOf('#') != -1) {
let [moduleName, exportName] = this.pathToComponentImport.split("#");
window["System"].import(moduleName)
.then((module: any) => module[exportName])
.then((type: any) => {
this.typeBuilder.createComponentFactoryFromType(type)
.then((factory: ComponentFactory<any>) =>
{
// Target will instantiate and inject component (we'll keep reference to it)
this.componentRef = this
.dynamicComponentTarget
.createComponent(factory);
// let's inject @Inputs to component instance
let component = this.componentRef.instance;
component.model = { text: 'hello world' };
//...
});
});
}
}
ngOnDestroy(): void {
}
ngOnChanges(changes: SimpleChanges): void {
}
ngAfterViewInit(): void {
this.refreshContent();
}
}
Now i can link to any given component like this:
<dynamic-component pathToComponentImport="/app/views/components/component1/extremely.dynamic.component.js#ExtremelyDynamicComponent"></dynamic-component>
Typescript config:
{
"compilerOptions": {
"target": "es5",
"module": "system",
"moduleResolution": "node",
"sourceMap": true,
"emitDecoratorMetadata": true,
"allowJs": true,
"experimentalDecorators": true,
"lib": [ "es2015", "dom" ],
"noImplicitAny": true,
"suppressImplicitAnyIndexErrors": true
},
"exclude": [
"node_modules",
"systemjs-angular-loader.js",
"systemjs.config.extras.js",
"systemjs.config.js"
]
}
And above my typescript config. So this works, however I'm not sure that I'm happy with using SystemJS. I feel like this should be possible with webpack as well and unsure if it's the way that TC compiles the files that webpack does not understand... I'm still getting the missing decorator exception if i try to run this code in a webpack bundle.
Best regards Morten