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


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

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.


createSub() {
  const factory = this.resolver.resolveComponentFactory(SubComponent);
  const ref = this.location.createComponent(factory, this.location.length, 
  ref.instance.model = {Which you like to send}
  ref.instance.outPut = (data) =>{ //will get called from from SubComponent} 
  this.location.parentInjector, []);
  ref.changeDetectorRef.detectChanges();
return ref;
}

SubComponent{
 public model;
 public outPut = <any>{};  
 constructor(){ console.log("Your input will be seen here",this.model) }
 sendDataOnClick(){
    this.outPut(inputData)
 }    
}

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.


If you know the type of the component you want to add i think you can use another approach.

In your app root component html:

<div *ngIf="functionHasCalled">
    <app-sub [data]="dataInput" (onClick)="onSubComponentClick()"></app-sub>
</div>

In your app root component typescript:

private functionHasCalled:boolean = false;
private dataInput:string;

onClick(){
   //And you can initialize the input property also if you need
   this.dataInput = 'asfsdfasdf';
   this.functionHasCalled = true;
}

onSubComponentClick(){

}

Providing data for @Input is very easy. You have named your component app-sub and it has a @Input property named data. Providing this data can be done by doing this:

<app-sub [data]="whateverdatayouwant"></app-sub>