主要需求:由於 Template 不是固定的,因此需要可以由後端指定 HTML,前台 angular 動態的載入 tempalte。
最主要的參考:Angular 2 dynamic template url with string variable?
這裡的 Component (DynamicTemplateComponent) 中,透過 template 指定 ng-container ,透過 ViewChild 可以動態指定:
<ng-container #dynamicTemplate></ng-container>
@ViewChild(‘dynamicTemplate’, {read: ViewContainerRef}) dynamicTemplate;
DynamicTemplateComponent 每次 routing 呼叫後,利用 ViewChild 自動生成對應的 dynamic component 與其 template 的頁面。 angular 提供 Dynamic Component Loader 可以達成,
但原始文章有一個主要問題在於無法透過 templateUrl,因為 angular cli 會直接解析內容。
解決方案是透過 template 指定 html 內容:
完整程式如下:
export class DynamicTemplateComponent implements AfterViewInit, OnInit {
@ViewChild('dynamicTemplate', { read: ViewContainerRef }) dynamicTemplate;
constructor(private compiler: Compiler, private injector: Injector, private ngModuleRef: NgModuleRef<any>,
private route: ActivatedRoute, private data: LhcService) { }
id: number;
ngOnInit() {
this.route.params.subscribe(p => {
this.id = p["id"];
})
}
ngAfterViewInit(): void {
this.data.getTemplate(this.id).subscribe(data => {
const tmpComponent = Component({ moduleId: module.id, template: data })
(class {
cancel() {
window.history.back();
}
});
const tmpModule = NgModule({ declarations: [tmpComponent] })(class { });
this.compiler.compileModuleAndAllComponentsAsync(tmpModule)
.then((factories) => {
const factory = factories.componentFactories[0];
const cmpRef = factory.create(this.injector, [], null, this.ngModuleRef);
cmpRef.instance.name = 'dynamic';
this.dynamicTemplate.insert(cmpRef.hostView);
})
});
}
這裡要額外宣告 module.id,是 angular 告訴 webpack or systemjs 如何 package module 的 metadata,因為我們使用 angular cli (webpack) ,會自動注入 module.id 到 component 中,唯一需要注意的是 typescript 要事先宣告避免無法編譯:
declare var module: {
id: string;
}
如果產生的 component 要加入 處理 *ngFor 等 browser 的內容,要在 NgModule 中,宣告 BrowserModule:
const tmpModule = NgModule({
declarations: [tmpComponent],
imports: [BrowserModule]
})(class { });
其次,在產生的 component 中,很難使用 constructor injection,最好的方式是產生時候,直接將服務對應到 class property 中:
const tmpComponent = Component({ moduleId: module.id, template: data })
(class {
private data: LhcService;
patients: Array<RegFile>;
getPatient() {
this.data.getAllPatients().subscribe(data => this.patients = data);
}
cancel() {
window.history.back();
}
});
…
this.compiler.compileModuleAndAllComponentsAsync(tmpModule)
.then((factories) => {
const factory = factories.componentFactories[0];
const cmpRef = factory.create(this.injector, [], null, this.ngModuleRef);
cmpRef.instance.name = 'dynamic';
cmpRef.instance.data = this.data;
cmpRef.instance.callback = this.interactive;
this.dynamicTemplate.insert(cmpRef.hostView);
})
其中 compRef.instance 就是代表 dynamic component 的函數物件,這裡將 parent 的 data (lch.service)對應到 child 中,讓他可以直接使用。
此外,也可以加入 callback function(cmpRef.instance.callback = this.interactive),與 patient controller 互動:
在 parnet 中宣告:
interactive(patient: RegFile): Observable<string> {
console.log("dynamic compoent callback, show reg_no: " + patient.RegNo);
return new Observable((observe) => {
observe.next(patient.Name);
observe.complete();
})
}
重點在於回傳 Observable 讓 child component 可以接收:
callback: Function;
clickPatient(patient: RegFile) {
this.callback(patient).subscribe(m => console.log("easy way from patient: " + m));
}
此外,class 的宣告也可使用繼承或者介面:
const tmpComponent = Component({ moduleId: module.id, template: data })
(class implements OnInit {
ngOnInit(): void {
console.log("ng onitit");
}
請注意,這裡的 class 不可以移出外部,原因是如果移出去,第一次會成功、但第二次就會造成以下的錯誤:
ERROR Error: Type InnerComponent is part of the declarations of 2 modules: class_1 and class_1! Please consider moving InnerComponent to a higher module that imports class_1 and class_1. You can also create a new NgModule that exports and includes InnerComponent then import that NgModule in class_1 and class_1.
此外,也要注意因為動態生成 component 功能畢竟有限,有些無法執行;例如 ngx-bootstrap 的 datepicker 就因為 module 匯入造成失敗,目前找不到原因,還好可以用 input type=’date’ 方式叫出瀏覽器預設的 date picker:
imports: [BrowserModule, FormsModule, BsDatepickerModule.forRoot()] ERROR TypeError: Cannot read property 'isDisabled' of undefined at Object.eval [as updateRenderer] (BsDatepickerDayDecoratorComponent_Host.ngfactory.js? [sm]:1)