Angular 2.1.0 动态创建子组件 [英] Angular 2.1.0 create child component on the fly, dynamically
问题描述
我在 angular 2.1.0
中尝试做的是动态创建子组件,这些子组件应该注入到父组件中.例如,父组件是 lessonDetails
,它包含所有课程的共享内容,例如 转到上一课
、转到下一课
等按钮和其他内容.根据路由参数,应该是子组件的课程内容需要动态注入父组件.子组件(课程内容)的 HTML 被定义为外面某处的纯字符串,它可以是像这样的对象:
What i'm trying to do in angular 2.1.0
is creating child components on the fly which should be injected into parent component. For example parent component is lessonDetails
which contains shared stuff for all lessons such as buttons like Go to previous lesson
, Go to next lesson
and other stuff. Based on route params, lesson content which should be child component needs to be injected dynamically into parent component. HTML for child components (lesson content) is defined as plain string somewhere outside, it can be object like:
export const LESSONS = {
"lesson-1": `<p> lesson 1 </p>`,
"lesson-2": `<p> lesson 2 </p>`
}
问题可以通过innerHtml
在父组件模板中具有类似以下内容轻松解决.
Problem can be easily solved through innerHtml
having something like following in parent component template.
<div [innerHTML]="lessonContent"></div>
在每次更改路由参数时,父组件的属性 lessonContent
将更改(内容(新模板)将从 LESSON
对象中获取)导致父组件模板被更新.这有效,但 angular 不会处理通过 innerHtml
注入的内容,因此不可能使用 routerLink
和其他东西.
Where on each change of route params, property lessonContent
of parent component would change(content(new template) would be taken from LESSON
object) causing parent component template to be updated. This works but angular will not process content injected through innerHtml
so it is impossible to use routerLink
and other stuff.
在新的角度发布之前,我使用来自 的解决方案解决了这个问题http://blog.lacolaco.net/post/dynamic-component-creation-in-angular-2/,我一直在使用 ComponentMetadata
和 ComponentResolvercode> 动态创建子组件,例如:
Before new angular release i solved this problem using solution from http://blog.lacolaco.net/post/dynamic-component-creation-in-angular-2/, where i have been using ComponentMetadata
together with ComponentResolver
to create child components on the fly, like:
const metadata = new ComponentMetadata({
template: this.templateString,
});
templateString
作为 Input
属性传递给子组件的地方.MetaData
和 ComponentResolver
在 angular 2.1.0
中均已弃用/删除.
Where templateString
was passed to child component as Input
property to child component. Both MetaData
and ComponentResolver
are deprecated/removed in angular 2.1.0
.
所以问题不仅仅是动态组件创建,就像在一些相关的 SO 问题中描述的那样,如果我为每个课程内容定义了组件,问题会更容易解决.这意味着我需要为 100 个不同的课程预先声明 100 个不同的组件.已弃用的元数据提供的行为类似于在单个组件的运行时更新模板(在路由参数更改时创建和销毁单个组件).
So problem is not just about dynamic component creation, like described in few related SO questions, problem would be easier to solve if i would have defined component for each lesson-content. This would mean that i need to predeclare 100 different components for 100 different lessons. Deprecated Metadata was providing behaviour that was like updating template at runtime of single component(creating and destroying single component on route params change).
更新 1: 在最近的 Angular 版本中,所有需要动态创建/注入的组件都需要在 @NgModule 内的
entryComponents
中预定义代码>.因此,在我看来,与上述问题相关,如果我需要 100 节课(需要动态创建的组件),这意味着我需要预定义 100 个组件
Update 1: As it seems in recent angular release, all components that needs to be created/injected dynamically needs to be predefined in entryComponents
within @NgModule
. So as it seems to me, related to question above, if i need to have 100 lessons(components that needs to be created dynamically on the fly) that means i need to predefine 100 components
更新 2:基于更新 1,可以通过以下方式通过 ViewContainerRef.createComponent()
完成:
Update 2: Based on Update 1, it can be done through ViewContainerRef.createComponent()
in following way:
// lessons.ts
@Component({ template: html string loaded from somewhere })
class LESSON_1 {}
@Component({ template: html string loaded from somewhere })
class LESSON_2 {}
// exported value to be used in entryComponents in @NgModule
export const LESSON_CONTENT_COMPONENTS = [ LESSON_1, LESSON_2 ]
现在在路由参数的父组件中更改
Now in parent component on route params change
const key = // determine lesson name from route params
/**
* class is just buzzword for function
* find Component by name (LESSON_1 for example)
* here name is property of function (class)
*/
const dynamicComponent = _.find(LESSON_CONTENT_COMPONENTS, { name: key });
const lessonContentFactory = this.resolver.resolveComponentFactory(dynamicComponent);
this.componentRef = this.lessonContent.createComponent(lessonContentFactory);
父模板看起来像:
<div *ngIf="something" #lessonContentContainer></div>
其中 lessonContentContainer
被修饰为 @ViewChildren
属性,lessonContent
被修饰为 @ViewChild
并在ngAfterViewInit()
为:
Where lessonContentContainer
is decorated @ViewChildren
property and lessonContent
is decorated as @ViewChild
and it is initialized in ngAfterViewInit ()
as:
ngAfterViewInit () {
this.lessonContentContainer.changes.subscribe((items) => {
this.lessonContent = items.first;
this.subscription = this.activatedRoute.params.subscribe((params) => {
// logic that needs to show lessons
})
})
}
解决方案有一个缺点,即所有组件(LESSON_CONTENT_COMPONENTS)都需要预定义.
有没有办法使用单个组件并更改该组件的模板在运行时(在路由参数更改时)?
Solution has one drawback and that is, all components(LESSON_CONTENT_COMPONENTS) needs to be predefined.
Is there a way to use one single component and to change template of that component at runtime (on route params change)?
推荐答案
您可以使用以下 HtmlOutlet
指令:
You can use the following HtmlOutlet
directive:
import {
Component,
Directive,
NgModule,
Input,
ViewContainerRef,
Compiler,
ComponentFactory,
ModuleWithComponentFactories,
ComponentRef,
ReflectiveInjector
} from '@angular/core';
import { RouterModule } from '@angular/router';
import { CommonModule } from '@angular/common';
export function createComponentFactory(compiler: Compiler, metadata: Component): Promise<ComponentFactory<any>> {
const cmpClass = class DynamicComponent {};
const decoratedCmp = Component(metadata)(cmpClass);
@NgModule({ imports: [CommonModule, RouterModule], declarations: [decoratedCmp] })
class DynamicHtmlModule { }
return compiler.compileModuleAndAllComponentsAsync(DynamicHtmlModule)
.then((moduleWithComponentFactory: ModuleWithComponentFactories<any>) => {
return moduleWithComponentFactory.componentFactories.find(x => x.componentType === decoratedCmp);
});
}
@Directive({ selector: 'html-outlet' })
export class HtmlOutlet {
@Input() html: string;
cmpRef: ComponentRef<any>;
constructor(private vcRef: ViewContainerRef, private compiler: Compiler) { }
ngOnChanges() {
const html = this.html;
if (!html) return;
if(this.cmpRef) {
this.cmpRef.destroy();
}
const compMetadata = new Component({
selector: 'dynamic-html',
template: this.html,
});
createComponentFactory(this.compiler, compMetadata)
.then(factory => {
const injector = ReflectiveInjector.fromResolvedProviders([], this.vcRef.parentInjector);
this.cmpRef = this.vcRef.createComponent(factory, 0, injector, []);
});
}
ngOnDestroy() {
if(this.cmpRef) {
this.cmpRef.destroy();
}
}
}
See also Plunker Example
对于 AOT 编译,请参阅这些线程
For AOT compilation see these threads
- https://github.com/angular/angular/issues/15510
- http://blog.assaf.co/angular-2-harmony-aot-compilation-with-lazy-jit-2/
另见 github Webpack AOT 示例 https://github.com/alexzuza/angular2-build-examples/tree/master/ngc-webpack
这篇关于Angular 2.1.0 动态创建子组件的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!