40

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();
}
Heretic Monkey
  • 11,687
  • 7
  • 53
  • 122
thpnk
  • 616
  • 1
  • 5
  • 9
  • 3
    In the past you could do something like this.. `ref.instance.someData = someData`. I am not sure if that is still the case though. – khollenbeck Nov 02 '16 at 14:26
  • 2
    That still works. There is no other way (except using shared services to communicate). http://stackoverflow.com/questions/36325212/angular-2-dynamic-tabs-with-user-click-chosen-components/36325468#36325468 contains some more details. – Günter Zöchbauer Nov 02 '16 at 14:29
  • @KrisHollenbeck @Günter Zöchbauer Thanks. So everytime the data changes in the parent component, i have to set it like 'ref.instance.someData = someData' and trigger change detection ('ref.changeDetectorRef.detectChanges();') on my own? And how to find out what property in the sub component is the right on? Could be named completely different ... :/ Maybe use `Reflection` directly Will try it :) – thpnk Nov 02 '16 at 14:38
  • @thpnk, Yeah I believe so, that sounds right. I haven't tried doing this since one of the later RC versions. I would refer to that post by Gunter Zochbauer for more info. http://stackoverflow.com/questions/36325212/angular-2-dynamic-tabs-with-user-click-chosen-components/36325468#36325468 – khollenbeck Nov 02 '16 at 16:22

5 Answers5

15

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.

anteAdamovic
  • 1,462
  • 12
  • 23
  • does this actually set data using the input name ? or just using the field name ? – bvdb Jul 12 '19 at 10:01
  • @bvdb To be honest I am not sure, it was a pretty long time ago but you can easily check. If I would guess I would say it's using the field name but can't be sure. – anteAdamovic Aug 22 '19 at 10:01
  • 4
    I suppose it has to be `ref.instance.someData` instead of `ref.someData` – Alexander Nov 17 '20 at 19:54
  • Hey @Alexander , doing that gives me 'someData' doesn't exist on type 'unknown'. How to fix to know know the type if it hasn't resolved the type of dynamic component yet? – kt-workflow Jun 16 '21 at 00:21
3

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.

William Neely
  • 1,923
  • 1
  • 20
  • 23
0
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)
 }    
}
ayyappa maddi
  • 854
  • 3
  • 10
  • 18
-2

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(){

}
Nour
  • 5,609
  • 3
  • 21
  • 24
-7

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>