Issue
Background:
I’ve followed multiple tutorials to load a module remotely in order to attempt to create a plugin architecture using Angular. In particular:
- I’m using Angular 10 for the main application
- angular builder to build the plugins
- Rollup to generate a UMD module.
- SystemJS as a module loader
Issue at hand:
- I can successfully load the remotely defined modules and the remote modules can successfully use common services (by common I mean known by the main or core application and the plugin)
- I cannot dynamically load a component defined in that module even though the component is defined in the plugin module declarations, exports and as an entry component in the module itself.
Here’s the code:
https://github.com/rickszyr/angular-plugins/
How to run it:
- npm install
- npm run build:init //this compiles the common services
- npm run build:plugins // generates umd bundles for two plugins
- npm run start:all // launches server and client
- click on "Load" with the default field values
- get an error.
The error:
What I found out is that for some reason that components host view does not have the _lview value initialized. But i’m not sure what to do with that information or how to make sure it does have that value properly set.
The lines that fail are in app.component.ts when trying to create the component and insert it into the dynamic component loader.
Thank you very much in advance
Main components:
app.component.ts
import { Compiler, Component, ComponentFactoryResolver, Injector, NgModuleFactory, ViewChild, ViewContainerRef } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { IPlugin, PluginCatalogService } from "interfaces";
import * as ngCore from "@angular/core";
import * as ngCommon from "@angular/common";
import * as ngBrowser from "@angular/platform-browser";
import * as commonInterfaces from "interfaces";
import { ModuleLoader } from "./remote-module-loader.service";
import { DynamicComponentDirective } from "./directives/dynamic-component.directive";
@Component({
selector: "app-root",
templateUrl: "app.component.html",
styles: [],
})
export class AppComponent {
title = "plugins";
loader: ModuleLoader;
@ViewChild('putStuffHere', {read: ViewContainerRef}) putStuffHere: ViewContainerRef;
constructor(
public pluginService: PluginCatalogService,
private injector: Injector,
private factoryResolver: ComponentFactoryResolver,
private compiler: Compiler,
public viewContainer: ViewContainerRef
) {
this.loader = new ModuleLoader();
}
loadModule(modulePath: string, moduleName: string) {
this.loader.register({
"@angular/core": ngCore,
"@angular/common": ngCommon,
"interfaces": commonInterfaces
}).then(ml => ml.load(modulePath).then(m => {
const moduleFactory: NgModuleFactory<any> = <NgModuleFactory<any>>m.default[moduleName+ "NgFactory"];
const moduleReference = moduleFactory.create(this.injector);
moduleReference.componentFactoryResolver.resolveComponentFactory((<IPlugin>moduleReference.instance).mainComponent);
var compFactory = moduleReference.componentFactoryResolver.resolveComponentFactory(this.getEntryComponent(moduleFactory));
this.putStuffHere.createComponent(compFactory); // <<< this fails
var component = compFactory.create(this.injector);
this.putStuffHere.insert(component.hostView);// <<< this fails
}));
}
getEntryComponent(moduleFactory: any):any {
var existModuleLoad = (<any>moduleFactory.moduleType).decorators[0].type.prototype.ngMetadataName === "NgModule"
if (!existModuleLoad) return null;
return moduleFactory.moduleType.decorators[0].args[0].entryComponents[0];
}
}
app.component.html
<h1>Welcome!</h1>
<p>
<label>Path Remote</label><input #pathRemote value="http://localhost:3000/plugin2.module.umd.js">
</p>
<p>
<label>Remote Name</label><input #remoteName value="Plugin2Module">
</p>
<p>
<button (click)="loadModule(pathRemote.value, remoteName.value)">Load</button>
</p>
<ol>
<li *ngFor="let module of pluginService.installedPlugins">{{ module.name}}</li>
</ol>
<ng-container #putStuffHere></ng-container>
compiled plugin code:
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@angular/core'), require('@angular/common'), require('interfaces')) :
typeof define === 'function' && define.amd ? define(['exports', '@angular/core', '@angular/common', 'interfaces'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Plugin2Module = {}, global.i0, global.i3, global.i4));
}(this, (function (exports, i0, i3, i4) { 'use strict';
/**
* @fileoverview added by tsickle
* Generated from: lib/plugin2.component.ts
* @suppress {checkTypes,constantProperty,extraRequire,missingOverride,missingRequire,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
class Plugin2Component {
constructor() {
this.title = "Nada";
}
/**
* @return {?}
*/
ngOnInit() {
}
}
Plugin2Component.decorators = [
{ type: i0.Component, args: [{
selector: 'lib-plugin2',
template: `
<p>
plugin2 works!
</p>
`
}] }
];
/** @nocollapse */
Plugin2Component.ctorParameters = () => [];
Plugin2Component.propDecorators = {
title: [{ type: i0.Input }]
};
/**
* @fileoverview added by tsickle
* Generated from: lib/plugin2.module.ts
* @suppress {checkTypes,constantProperty,extraRequire,missingOverride,missingRequire,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
class Plugin2Module {
/**
* @param {?} pluginService
*/
constructor(pluginService) {
console.log("Se registro Plugin 2");
pluginService.installedPlugins.push(this);
}
/**
* @return {?}
*/
get name() {
return "Plugin 2";
}
/**
* @return {?}
*/
get mainComponent() {
return Plugin2Component;
}
}
Plugin2Module.decorators = [
{ type: i0.NgModule, args: [{
declarations: [Plugin2Component],
imports: [i3.CommonModule],
exports: [Plugin2Component],
entryComponents: [Plugin2Component]
},] }
];
/** @nocollapse */
Plugin2Module.ctorParameters = () => [
{ type: i4.PluginCatalogService }
];
/**
* @fileoverview This file was generated by the Angular template compiler. Do not edit.
*
* @suppress {suspiciousCode,uselessCode,missingProperties,missingOverride,checkTypes,extraRequire}
* tslint:disable
*/
var styles_Plugin2Component = [];
var RenderType_Plugin2Component = i0.ɵcrt({ encapsulation: 2, styles: styles_Plugin2Component, data: {} });
function View_Plugin2Component_0(_l) { return i0.ɵvid(0, [(_l()(), i0.ɵeld(0, 0, null, null, 1, "p", [], null, null, null, null, null)), (_l()(), i0.ɵted(-1, null, [" plugin2 works! "]))], null, null); }
function View_Plugin2Component_Host_0(_l) { return i0.ɵvid(0, [(_l()(), i0.ɵeld(0, 0, null, null, 1, "lib-plugin2", [], null, null, null, View_Plugin2Component_0, RenderType_Plugin2Component)), i0.ɵdid(1, 114688, null, 0, Plugin2Component, [], null, null)], function (_ck, _v) { _ck(_v, 1, 0); }, null); }
var Plugin2ComponentNgFactory = i0.ɵccf("lib-plugin2", Plugin2Component, View_Plugin2Component_Host_0, { title: "title" }, {}, []);
/**
* @fileoverview This file was generated by the Angular template compiler. Do not edit.
*
* @suppress {suspiciousCode,uselessCode,missingProperties,missingOverride,checkTypes,extraRequire}
* tslint:disable
*/
var Plugin2ModuleNgFactory = i0.ɵcmf(Plugin2Module, [], function (_l) { return i0.ɵmod([i0.ɵmpd(512, i0.ComponentFactoryResolver, i0.ɵCodegenComponentFactoryResolver, [[8, [Plugin2ComponentNgFactory]], [3, i0.ComponentFactoryResolver], i0.NgModuleRef]), i0.ɵmpd(4608, i3.NgLocalization, i3.NgLocaleLocalization, [i0.LOCALE_ID]), i0.ɵmpd(1073742336, i3.CommonModule, i3.CommonModule, []), i0.ɵmpd(1073742336, Plugin2Module, Plugin2Module, [i4.PluginCatalogService])]); });
exports.Plugin2ModuleNgFactory = Plugin2ModuleNgFactory;
Object.defineProperty(exports, '__esModule', { value: true });
})));
tsconfig.lib.json for the plugin:
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../out-tsc/lib",
"target": "es2015",
"declaration": true,
"declarationMap": true,
"inlineSources": true,
"types": [],
"lib": [
"dom",
"es2018"
]
},
"angularCompilerOptions": {
"enableIvy": false,
"skipTemplateCodegen": false,
"strictMetadataEmit": true,
"annotateForClosureCompiler": true,
"enableResourceInlining": true
},
"exclude": [
"src/test.ts",
"**/*.spec.ts"
]
}
Solution
You have disabled the Ivy compiler in the plugins, but forgot to disable it in the main project.
Adding the following in the main tsconfig.json
will fix the issue
"angularCompilerOptions": {
"enableIvy": false
}
Thanks to @DWhitSlaya for mentioning this here: https://www.reddit.com/r/angular/comments/ih7hib/how_to_use_viewcontainerref_with_dynamic/g30hpzv