angularangular-factoryangular-compiler

Angular Dynamic Html Template component with event emitter


I am trying to extend the functionality of this component by adding a generic event emitter

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();
    }
  }
}

Crediting original author https://gist.github.com/benjamincharity/8116414c7f38cffe3cef0e44fe44295d

example of the desired event

@Output() genericEventEmitter = new EventEmitter<string>();

emitEvent(data:string){
this.genericEventEmitter.emit(data)
}

I've tried adding this to the htmlOutlet class and also the DynamicComponent I'm getting the error message

ERROR TypeError: _co.emitEvent is not a function
    at Object.eval [as handleEvent] (DynamicComponent.html:6)

This is telling me that the function is not being properly added to the class when it's being created within the componentFactory

Any Angular Gurus, able to shed some insight on how I can get this to work?


Solution

  • Managed to daisy chain a Subject in the dynamic component to an event emitter in the html outlet which can be captured by the parent of the html outlet

    import {
      Component,
      Directive,
      NgModule,
      Input,
      ViewContainerRef,
      Compiler,
      ComponentFactory,
      ModuleWithComponentFactories,
      ComponentRef,
      ReflectiveInjector,
      EventEmitter,
      Output,
    
    
    } from '@angular/core';
    import { Observable, Subject } from 'rxjs'
    
    import { RouterModule } from '@angular/router';
    import { CommonModule } from '@angular/common';
    import { PartialObject } from 'lodash';
    
    export function createComponentFactory(compiler: Compiler, metadata: Component): Promise<ComponentFactory<any>> {
      const cmpClass = class DynamicComponent {
        outputter: Subject<Object> = new Subject();
        genEmit(data) {
          this.outputter.next(data)
        }
    
      };
      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>;
      @Output() genericEmitter = new EventEmitter();
    
    
      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, []);
            this.cmpRef.instance.outputter.subscribe(v => {
              this.genericEmitter.emit(v)
    
            })
    
    
    
          });
    
      }
    
      ngOnDestroy() {
        if (this.cmpRef) {
          this.cmpRef.destroy();
        }
      }
    }