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

Mistalis
  • 17,793
  • 13
  • 73
  • 97
Srle
  • 10,366
  • 8
  • 34
  • 63
  • See http://hl7.org/fhir/StructureDefinition/structuredefinition-explicit-type-name. 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. – Günter Zöchbauer Oct 16 '16 at 19:35
  • @GünterZöchbauer thank you for reply, actually i'm using `ViewContainerRef.createComponent()` please check Update 2 part in question – Srle Oct 16 '16 at 20:13
  • 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. – Günter Zöchbauer Oct 17 '16 at 05:35
  • Similar issue is covered here [How can I use/create dynamic template to compile dynamic Component with Angular 2.0?](http://stackoverflow.com/q/38888008/1679310) – Radim Köhler Oct 17 '16 at 13:03

1 Answers1

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

yurzui
  • 205,937
  • 32
  • 433
  • 399
  • 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` then inside of `ngOnChanges` before creating new component `if (this.cmpRef) { this.cmpRef.destroy(); }`. Or it will be automatically destroyed? – Srle Oct 17 '16 at 11:58
  • 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 answer – yurzui Oct 17 '16 at 12:04
  • Just one more question, a little bit unrelated to question. Trying to apply `safeHtml` pipe on html outlet directive as `"` 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); }` – Srle Oct 17 '16 at 12:12
  • Related answer http://stackoverflow.com/questions/38037760/how-to-set-iframe-src-in-angular-2-without-causing-unsafe-value-exception/38037914#38037914 – yurzui Oct 17 '16 at 12:17
  • I've tried you're plnkr as a solution to my problem. This solution does not support custom components. .http://stackoverflow.com/questions/40092639/how-to-render-a-dynamic-template-with-components-in-angular2#40092639, and i've created a new plnkr - https://plnkr.co/edit/UACDPBRWNmvjVVsr0dWC?p=preview – Linksonder Oct 17 '16 at 18:12
  • @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: (?)** – deen Oct 18 '16 at 10:40
  • @rish Try to do the same at your root component. I guess you will get the same error – yurzui Oct 18 '16 at 10:56
  • @yurzui, I tried inside root component, I am not getting error? – deen Oct 18 '16 at 11:01
  • @yurzui 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? – deen Nov 11 '16 at 06:22
  • @rish Check this http://stackoverflow.com/questions/40314136/dynamic-add-component-using-gridstack-and-angular2-jquery-parsehtml-does-not/40446974#40446974.Maybe it helps you. – yurzui Nov 11 '16 at 06:36
  • 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 ( >>> ) and it also gets error. So only my-test can be used there but why ?? – user2771738 Dec 07 '16 at 17:57
  • @user2771738 Can you show me this plunker that reproduces it? – yurzui Dec 07 '16 at 18:00
  • https://plnkr.co/edit/4AljRIDxU8rNzKlcOKFq?p=preview in app.ts you can revert to my-test and it will be working. But only with this component – user2771738 Dec 07 '16 at 18:06
  • @user2771738 Why do you want to use the same component as in router configuration? https://plnkr.co/edit/klzTnlldGo57ETlqt0mm?p=preview You need to declare your desired component within SharedModule – yurzui Dec 07 '16 at 18:21
  • 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. – user2771738 Dec 07 '16 at 18:27
  • @user2771738 Can you share your project on github? – yurzui Dec 07 '16 at 18:28
  • 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 publishing – user2771738 Dec 07 '16 at 18:35
  • Ok I found that i copied html outlet from example without imported shared module ... sry for problem. Works great! – user2771738 Dec 07 '16 at 20:06
  • @user2771738 Glad to hear :) – yurzui Dec 07 '16 at 20:07
  • @yurzui **[ts] Property 'find' does not exist on type 'ComponentFactory[]'.**, this error I get. I am using angular 2.2 – deen Dec 14 '16 at 08:53
  • 1
    Using this solution, but since new angular-cli versions, it is throwing this error: No NgModule metadata found for 'DynamicHtmlModule'. Any suggestion? – Cybey Feb 15 '17 at 12:52
  • 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 parameter – Alexandre Couret May 08 '17 at 14:02
  • @AlexandreCouret Are you sure? https://github.com/angular/angular/blob/4.1.1/packages/core/src/linker/view_container_ref.ts#L85-L86 How do you get componentFactory? Can you create a plunker? – yurzui May 08 '17 at 14:05
  • @yurzui 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 ? – Royi Namir May 07 '18 at 11:33
  • @yurzui How can we use angular's interpolation inside the html passed to the dynamic component? Like `'
    Mix at {{speed}} rpm
    '`
    – Sruthi Varghese Oct 15 '18 at 11:32
  • @yurzui 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. – Sruthi Varghese Oct 15 '18 at 12:20
  • @SruthiVarghese Can you reproduce it? – yurzui Oct 15 '18 at 12:22
  • @yurzui 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 blank – Sruthi Varghese Oct 15 '18 at 12:24
  • @yurzui 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 . – Sruthi Varghese Oct 16 '18 at 04:29