Search code examples
javascriptangularviewchild

Passing data to a dynamic component loader using a service in Angular


I have a dynamic component loader, and I need to pass in data through a service. I can get the data to display if I fire the function on click for example, but not OnInit.

  • I have tried using AfterViewInit
  • Eventually the data will be coming from an API

Update: Working StackBlitz

app.component.html

<app-answer-set></app-answer-set>

header.component.html

<ng-template appHeaderHost></ng-template>

header.component.ts

export class HeaderComponent implements OnInit {
  @Input() component;
  @ViewChild(HeaderHostDirective) headerHost: HeaderHostDirective;

  constructor(private componentFactoryResolver: ComponentFactoryResolver) { }

  ngOnInit() {
    this.loadComponent();
  }

  loadComponent() {
    const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.component);
    const viewContainerRef = this.headerHost.viewContainerRef;

    viewContainerRef.createComponent(componentFactory);
  }

}

header-host.directive.ts

export class HeaderHostDirective {

  constructor(public viewContainerRef: ViewContainerRef) { }

}

header-data.service.ts

export class HeaderDataService {

  private headerDataSource = new Subject<any>();
  headerData$ = this.headerDataSource.asObservable();

  constructor() { }

  setHeaderData(data: any) {
    this.headerDataSource.next(data);
  }

}

answer-set.component.html

<app-header [component]="component"></app-header>
<button (click)="setHeaderData()">click</button>

answer-set.component.ts

export class AnswerSetComponent implements OnInit {
  component: any = AnswerSetHeaderDetailsComponent;

  constructor(private headerDataService: HeaderDataService) { }

  ngOnInit() {
    this.setHeaderData();
  }

  setHeaderData() {
    const data = [{name: 'Header stuff'}];
    this.headerDataService.setHeaderData(data);
  }

}

answer-set-header-details.html

<dt>First:</dt>
<dd>Description</dd>
<dt>Second</dt>
<dd>Description</dd>
<p>
  data will show on click of button but not <code>onInit</code>:
</p>
<p>
  {{headerData}}
</p>

answer-set-header-details.component.ts

export class AnswerSetHeaderDetailsComponent implements OnInit {

  constructor(private headerDataService: HeaderDataService) { }
  headerData: any;

  ngOnInit() {
    this.headerDataService.headerData$
      .subscribe(data => {
        this.headerData = data[0].name;
      });
  }

}

Solution

  • After running through the Angular documentation I found that I needed to pass the data to the componentRef instance, something like this:

    (<IHeader>componentRef.instance).data = headerItems.data;
    

    After a little refactoring I ended up passing both the component and the data through a service, see the updated StackBlitz for a working example.

    Eventually I'll look to pass in multiple components, but for now this works.

    header.component.html

    <ng-template appHeaderHost></ng-template>
    

    header.component.ts

    /* ... */
    
    export class HeaderComponent implements OnInit {
      @ViewChild(HeaderHostDirective) headerHost: HeaderHostDirective;
    
      subscriptionManager = new Subscription();
    
      constructor(
        private headerService: HeaderService,
        private componentFactoryResolver: ComponentFactoryResolver
      ) {
    
        const headerConfigSubscription = this.headerService.headerConfig$
          .subscribe((headerItems: HeaderItem) => {
            this.loadComponent(headerItems);
          });
    
        this.subscriptionManager
          .add(headerConfigSubscription);
      }
    
      ngOnInit() {
      }
    
      loadComponent(headerItems: HeaderItem) {
        const componentFactory = this.componentFactoryResolver.resolveComponentFactory(headerItems.component);
        const viewContainerRef = this.headerHost.viewContainerRef;
    
        viewContainerRef.clear();
    
        const componentRef = viewContainerRef.createComponent(componentFactory);
    
        (<IHeader>componentRef.instance).data = headerItems.data ? headerItems.data : null;
      }
    
      ngOnDestroy() {
        /**
         * @description Unsubscribe from all subscriptions
         * to prevent memory leaks
         */
        this.subscriptionManager.unsubscribe();
      }
    
    }
    

    header.service.ts

    /* ... */
    
    export class HeaderService {
      private headerConfigSource = new Subject<any>();
      headerConfig$ = this.headerConfigSource.asObservable();
    
      constructor() { }
    
      configureHeaderItems(headerItems: HeaderItem) {
        this.headerConfigSource.next(headerItems);
      }
    }
    

    header.ts

    /* ... */
    
    export interface IHeader {
      data: any;
    }
    
    export interface IHeaderItem {
      component: Type<any>;
      data?: any;
    }
    
    export class HeaderItem implements IHeaderItem {
      constructor(
        public component: Type<any>,
        public data?: any
      ) {
        this.component = component;
        this.data = data;
      }
    }
    

    main.component.html

    <app-header></app-header>
    

    main.component.ts

    export class MainComponent implements OnInit {
      headerItems: HeaderItem;
    
      constructor(private headerService: HeaderService) { }
    
      ngOnInit() {
        this.configureHeaderItems();
      }
    
      configureHeaderItems() {
        this.headerItems = new HeaderItem(MainHeaderDetailsComponent, {});
    
        this.getHeaderItemData();
      }
    
      /**
       * @description This is where we would make the API call and get the data
       * but we can mock it for now 
       */
      getHeaderItemData() {
        const data = {
          name: 'I am loaded dynamically!'
        };
    
        this.headerItems.data = data;
        this.headerService.configureHeaderItems(this.headerItems);
      }
    
    }
    

    main.module.ts

    @NgModule({
      /* ... */
    
      // Need to add to entryComponents
      entryComponents: [MainHeaderDetailsComponent]
    })
    export class AnswerSetModule { }
    

    main-header-details.component.html

    <p>
      Header content. {{data.name}}
    </p>
    

    main-header-details.component.ts

    /* ... */
    
    export class MainHeaderDetailsComponent implements OnInit {
    
      constructor() { }
      @Input() data: any;
    
      ngOnInit() {
      }
    
    }