Angular 2.1.0 create child component on the fly, dynamically
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>`
}
Problem can be easily solved through innerHtml
having something like following in parent component template.
<div [innerHTML]="lessonContent"></div>
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.
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,
});
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 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).
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
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);
Parent template looks like:
<div *ngIf="something" #lessonContentContainer></div>
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
})
})
}
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)?
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
Example with custom component
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/
See also github Webpack AOT example https://github.com/alexzuza/angular2-build-examples/tree/master/ngc-webpack