Handle @Input and @Output for dynamically created Component in Angular 2

13,697

Solution 1

You can easily bind it when you create the component:

createSub() {
    const factory = this.resolver.resolveComponentFactory(SubComponent);
    const ref = this.location.createComponent(factory, this.location.length, this.location.parentInjector, []);
    ref.someData = { data: '123' }; // send data to input
    ref.onClick.subscribe( // subscribe to event emitter
      (event: any) => {
        console.log('click');
      }
    )
    ref.changeDetectorRef.detectChanges();
    return ref;
  }

Sending data is really straigthforward, just do ref.someData = data where data is the data you wish to send.

Getting data from output is also very easy, since it's an EventEmitter you can simply subscribe to it and the clojure you pass in will execute whenever you emit() a value from the component.

Solution 2

I found the following code to generate components on the fly from a string (angular2 generate component from just a string) and created a compileBoundHtml directive from it that passes along input data (doesn't handle outputs but I think the same strategy would apply so you could modify this):

    @Directive({selector: '[compileBoundHtml]', exportAs: 'compileBoundHtmlDirective'})
export class CompileBoundHtmlDirective {
    // input must be same as selector so it can be named as property on the DOM element it's on
    @Input() compileBoundHtml: string;
    @Input() inputs?: {[x: string]: any};
    // keep reference to temp component (created below) so it can be garbage collected
    protected cmpRef: ComponentRef<any>;

    constructor( private vc: ViewContainerRef,
                private compiler: Compiler,
                private injector: Injector,
                private m: NgModuleRef<any>) {
        this.cmpRef = undefined;
    }
    /**
     * Compile new temporary component using input string as template,
     * and then insert adjacently into directive's viewContainerRef
     */
    ngOnChanges() {
        class TmpClass {
            [x: string]: any;
        }
        // create component and module temps
        const tmpCmp = Component({template: this.compileBoundHtml})(TmpClass);

        // note: switch to using annotations here so coverage sees this function
        @NgModule({imports: [/*your modules that have directives/components on them need to be passed here, potential for circular references unfortunately*/], declarations: [tmpCmp]})
        class TmpModule {};

        this.compiler.compileModuleAndAllComponentsAsync(TmpModule)
          .then((factories) => {
            // create and insert component (from the only compiled component factory) into the container view
            const f = factories.componentFactories[0];
            this.cmpRef = f.create(this.injector, [], null, this.m);
            Object.assign(this.cmpRef.instance, this.inputs);
            this.vc.insert(this.cmpRef.hostView);
          });
    }
    /**
     * Destroy temporary component when directive is destroyed
     */
    ngOnDestroy() {
      if (this.cmpRef) {
        this.cmpRef.destroy();
      }
    }
}

The important modification is in the addition of:

Object.assign(this.cmpRef.instance, this.inputs);

Basically, it copies the values you want to be on the new component into the tmp component class so that they can be used in the generated components.

It would be used like:

<div [compileBoundHtml]="someContentThatHasComponentHtmlInIt" [inputs]="{anInput: anInputValue}"></div>

Hopefully this saves someone the massive amount of Googling I had to do.

Share:
13,697
thpnk
Author by

thpnk

Updated on June 03, 2022

Comments

  • thpnk
    thpnk almost 2 years

    How to handle/provide @Input and @Output properties for dynamically created Components in Angular 2?

    The idea is to dynamically create (in this case) the SubComponent when the createSub method is called. Forks fine, but how do I provide data for the @Input properties in the SubComponent. Also, how to handle/subscribe to the @Output events the SubComponent provides?

    Example: (Both components are in the same NgModule)

    AppComponent

    @Component({
      selector: 'app-root'
    })  
    export class AppComponent {
    
      someData: 'asdfasf'
    
      constructor(private resolver: ComponentFactoryResolver, private location: ViewContainerRef) { }
    
      createSub() {
        const factory = this.resolver.resolveComponentFactory(SubComponent);
        const ref = this.location.createComponent(factory, this.location.length, this.location.parentInjector, []);
        ref.changeDetectorRef.detectChanges();
        return ref;
      }
    
      onClick() {
        // do something
      }
    }
    

    SubComponent

    @Component({
      selector: 'app-sub'
    })
    export class SubComponent {
      @Input('data') someData: string;
      @Output('onClick') onClick = new EventEmitter();
    }