Angular 2.1.0 create child component on the fly, dynamically
Asked Answered
H

1

10

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)?

Haeckel answered 15/10, 2016 at 14:53 Comment(4)
See hl7.org/fhir/StructureDefinition/…. Adding HTML just adds HTML, if you want components dynamically you can use ViewContainerRef.createComponent(). Otherwise components and directives are only created for selectors that are added statically to the template of a component.Keewatin
@GünterZöchbauer thank you for reply, actually i'm using ViewContainerRef.createComponent() please check Update 2 part in questionHaeckel
You can't modify the template of a component at runtime. There are ways to create new components at runtime. I don't know details about this but there are answers to similar questions on SO.Keewatin
Similar issue is covered here How can I use/create dynamic template to compile dynamic Component with Angular 2.0?Ungula
L
16

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

See also github Webpack AOT example https://github.com/alexzuza/angular2-build-examples/tree/master/ngc-webpack

Linzer answered 17/10, 2016 at 7:3 Comment(29)
thanks on nice answer. Just one more question, shouldnt we save reference to current cmpRef and destroy it manually before creating new dynamic component? Having something like in HtmlOutlet directive, private cmpRef: ComponentRef<any> then inside of ngOnChanges before creating new component if (this.cmpRef) { this.cmpRef.destroy(); }. Or it will be automatically destroyed?Haeckel
Yes, of course. We have to do it manually. I'm not sure but seems this.vcRef.clear do the same thing. I updated my answerLinzer
Just one more question, a little bit unrelated to question. Trying to apply safeHtml pipe on html outlet directive as <html-outlet [html]="htmlString | safeHtml></html-outlet>" getting error Cannot set property stack of [object Object] which has only a getter(…). safeHtml is very simple pipe having transform method implemented as transform (html: string) { return this.sanitizer.bypassSecurityTrustHtml(html); }Haeckel
Related answer #38038260Linzer
I've tried you're plnkr as a solution to my problem. This solution does not support custom components. .#40093139, and i've created a new plnkr - plnkr.co/edit/UACDPBRWNmvjVVsr0dWC?p=previewBoost
@yurzui, I want to access dom object of html-outlet, for that I have to create constructor inside dynamic component. But when I am creating constructor(public eleRef: ElementRef) {}, inside dynamic component, it's giving errot- Can't resolve all parameters for DynamicComponent: (?)Szeged
@rish Try to do the same at your root component. I guess you will get the same errorLinzer
@yurzui, I tried inside root component, I am not getting error?Szeged
@Linzer I don't know it's possible or not but I want to get only DOM object from dynamic template I don't want to rendered the template. After getting DOM template I have to return it to metawidget API which take care of rendering part with some modification. So please suggest me if it's possible or not?Szeged
@rish Check this #40314636 it helps you.Linzer
nice solution, anyway I am having problem with using custom component solution. In my case angular always shout with 'some-componentt' is not a known element. I changed in your plunker test-component usage to home-component (<my-test></my-test> >>> <my-about></my-about>) and it also gets error. So only my-test can be used there but why ??Tinker
@Tinker Can you show me this plunker that reproduces it?Linzer
plnkr.co/edit/4AljRIDxU8rNzKlcOKFq?p=preview in app.ts you can revert to my-test and it will be working. But only with this componentTinker
@Tinker Why do you want to use the same component as in router configuration? plnkr.co/edit/klzTnlldGo57ETlqt0mm?p=preview You need to declare your desired component within SharedModuleLinzer
Just in matter of tests. I am having almost same project structure but I cannot use nor the shared components nor the app components. I think thats the same problem, maybe some dependency cycle or something.Tinker
@Tinker Can you share your project on github?Linzer
I will try to figure it out on my own a little bit more. I cannot publish whole project, so I need a little bit time to clean up it before publishingTinker
Ok I found that i copied html outlet from example without imported shared module ... sry for problem. Works great!Tinker
@Tinker Glad to hear :)Linzer
@Linzer [ts] Property 'find' does not exist on type 'ComponentFactory<any>[]'., this error I get. I am using angular 2.2Szeged
Using this solution, but since new angular-cli versions, it is throwing this error: No NgModule metadata found for 'DynamicHtmlModule'. Any suggestion?Mason
Hello, I'm using Angular 4.1 and the signature of createComponent doesn't accept the factory anymore : this.vcRef.createComponent(factory, 0, injector, []); throws "Argument of type '{}' is not assignable to parameter of type ComponentFactory<{}> for the first parameterPolyphemus
@AlexandreCouret Are you sure? github.com/angular/angular/blob/4.1.1/packages/core/src/linker/… How do you get componentFactory? Can you create a plunker?Linzer
@Linzer In my string template i'm using ngFor. But the dynamic module is not containing Common module How can I import the common module into the dynamic module ?Edition
@Linzer How can we use angular's interpolation inside the html passed to the dynamic component? Like '<div> Mix at {{speed}} rpm</div>'Tanagra
@Linzer If I use expression inside the braces like the one in your plunker example it will work. But when I give variable it's not working.Tanagra
@SruthiVarghese Can you reproduce it?Linzer
@Linzer For example if I use a variable instead of the expression {{5 + 6}} in your code nothing is showing the UI. It's coming as blankTanagra
@Linzer I copy paste your plunkr code and it works for me too. May be there was some mistake where I was initializing the variable. Thanks man .Tanagra

© 2022 - 2024 — McMap. All rights reserved.