angulartypescriptangular11angular-dynamic-componentsangular-renderer2

`this` not set to component instance when creating component programatically


I have a directive that reads src attribute and appends a DynamicComponent. It also sets an input property of DynamicComponent.

@Directive({
  selector: '[appDynamic]'
})
export class InjectDirective {
  @ViewChild('container', {read: ViewContainerRef}) container: ViewContainerRef;

  constructor(
    private element: ElementRef,
    private viewContainerRef: ViewContainerRef,
    private componentFactoryResolver: ComponentFactoryResolver
  ) { }

  ngOnInit(){
    const nativeElement = this.element.nativeElement;
    const src = nativeElement.getAttribute('src');

    if (src) {
      const componentFactory = this.componentFactoryResolver.resolveComponentFactory(DynamicComponent);
      const componentRef = this.viewContainerRef.createComponent(componentFactory);
      componentRef.instance.mediaPath = src;
      const parent = this.element.nativeElement.parentElement;
      parent.insertBefore(componentRef.location.nativeElement, this.element.nativeElement.nextSibling)
    }
  }
}

The injection happens, but I can't read mediaPath in the DynamicComponent due to this not being set to the right value. this is an object that only has property __ngContext__.

export class DynamicComponent implements OnInit {

  @Input() mediaPath:string;
  constructor() { }

  ngOnInit(): void {
  }

  openSection(event){ // click event
    console.log(this); // this does not refer to instance of DynamicComponent
    console.log(this.mediaPath); // this.mediaPath is undefined
  }

}

What is the correct way of creating a component dynamically and also passing values to the component properties? I am using angular 11.

UPDATED to Angular 14. Still the same problem:

@Directive({
  selector: '[appInject]'
})
export class InjectDirective {
  @ViewChild('container', {read: ViewContainerRef}) container: ViewContainerRef;

  constructor(
    private element: ElementRef,
    private viewContainerRef: ViewContainerRef,
  ) { }

  ngOnInit(){
    const nativeElement = this.element.nativeElement;
    const src = nativeElement.getAttribute('src');

    if (src) {
      const componentRef = this.viewContainerRef.createComponent(DynamicComponent);
      componentRef.setInput("mediaPath",src)
      const parent = this.element.nativeElement.parentElement;
      parent.insertBefore(componentRef.location.nativeElement, this.element.nativeElement.nextSibling)
    }
  }
}


export class DynamicComponent implements OnInit {

  @Input() mediaPath:string;
  constructor() { }

  ngOnInit(): void {}
  
  openFindSimilarSection(event){
    console.log(this); // only has __ngContext__ property
    console.log(this.mediaPath); // this is undefined
  }
}

//used like this:

  <img [src]="dynamically.added.path" appInjectFindSimilar>

Solution

  • Try using setInput method to pass in the input.

      const componentRef =
        this.viewContainerRef.createComponent(componentFactory);
      componentRef.setInput('mediaPath', src);
    

    Full Code:

    import {
      Component,
      Input,
      Directive,
      ViewChild,
      ViewContainerRef,
      ComponentFactoryResolver,
      ElementRef,
      Renderer2,
      Attribute,
    } from '@angular/core';
    import { bootstrapApplication } from '@angular/platform-browser';
    
    @Component({
      selector: 'dynamic-component',
      template: `dynamic component`,
    })
    export class DynamicComponent {
      @Input() mediaPath!: string;
      constructor() {}
    
      ngOnInit(): void {
        // click event
        console.log(this); // this does not refer to instance of DynamicComponent
        console.log(this.mediaPath); // this.mediaPath is undefined
      }
    
      openSection(event: any) {}
    }
    
    @Directive({
      selector: '[appDynamic]',
    })
    export class InjectDirective {
      @ViewChild('container', { read: ViewContainerRef })
      container!: ViewContainerRef;
    
      constructor(
        private element: ElementRef,
        private viewContainerRef: ViewContainerRef,
        private componentFactoryResolver: ComponentFactoryResolver,
        private renderer: Renderer2,
        @Attribute('src') public src: string
      ) {}
    
      ngOnInit() {
        const nativeElement = this.element.nativeElement;
    
        if (this.src) {
          const componentFactory =
            this.componentFactoryResolver.resolveComponentFactory(DynamicComponent);
          const componentRef =
            this.viewContainerRef.createComponent(componentFactory);
          componentRef.setInput('mediaPath', this.src);
          // componentRef.instance.mediaPath = src;
          const parent = this.element.nativeElement.parentElement;
          parent.insertBefore(
            componentRef.location.nativeElement,
            this.element.nativeElement.nextSibling
          );
        }
      }
    }
    
    @Component({
      selector: 'app-root',
      imports: [InjectDirective],
      template: `
        <img appDynamic [src]="'test'"/>
      `,
    })
    export class App {
      name = 'Angular';
    }
    
    bootstrapApplication(App);
    

    Stackblitz Demo