#viewContainerRef not showing dynamic components when inside of an @else in template

8 messages · Page 1 of 1 (latest)

delicate depot
#

I am creating components dynamically using the viewContainerRef approach.

For some reason if I put the ng-container within the if block, it doesn't render any the components> I can see the loading skeletons, but after that nothing.
However, if I place the ng-containeroutside the else block, it works fine.

I wonder if I am missing something..

Any help is much appreciated.

template

  @if (isLoading) {
    <lib-skeleton-main-tile></lib-skeleton-main-tile>--> <lib-skeleton-sub-tile></lib-skeleton-sub-tile>-->
    <lib-skeleton-sub-tile></lib-skeleton-sub-tile>--> <lib-skeleton-sub-tile></lib-skeleton-sub-tile>-->
    <lib-skeleton-sub-tile></lib-skeleton-sub-tile>-->
  } @else {
    <ng-container #dashboardTileContainer></ng-container>
  }
#
export class DashboardTilesComponent extends BaseSubscription implements OnInit, OnDestroy {
  loggedInUser$ = this.store.select(UserState.loggedInUser);
  currentSelectedCompanyId$ = this.store.select(CompanyState.currentSelectedId);
  isLoading = true;
  private tileFactory: TileFactory;
  private dashboardTileContainer = viewChild('dashboardTileContainer', { read: ViewContainerRef });
  constructor(
    private dashboardService: DashboardService,
    private store: Store
  ) {
    super();
    this.tileFactory = new TileFactory();
  }

  ngOnInit() {
    this.subscription.add(
      combineLatest([this.currentSelectedCompanyId$, this.loggedInUser$.pipe(take(1))])
        .pipe(
          switchMap(([currentSelectedCompanyId]) =>
            this.dashboardService.apiV2DashboardGet$Json({
              companyId: currentSelectedCompanyId ?? '',
            })
          ),
          tap({
            next: async (data: DashboardDto) => {
              this.dashboardTileContainer()?.clear();
              console.log('1111 data', data);
              await this.initializeTiles(data.tiles ?? []);
              this.isLoading = false;
            },
          })
        )
        .subscribe()
    );
  }
  override ngOnDestroy() {
    super.ngOnDestroy();
    this.dashboardTileContainer()?.clear();
  }
  private async initializeTiles(tiles: TileDto[]): Promise<void> {
    const sortedTiles = (tiles ?? []).sort((a, b) => (a.position?.order ?? 0) - (b.position?.order ?? 0));
    for (const tile of sortedTiles) {
        const componentClass = await this.tileFactory.getTileComponentClass(tile.type);
        if (componentClass) {
          this.dashboardTileContainer()?.createComponent(componentClass);
        }
    }
  }
#

Here is the component file..basically we dynamically load some dashboard components based the data we get back from an api call
It all seems to work fine, but once the ng-container is placed inside of the @else block, it doesn't render the components.

blazing haven
#

Here is what's happening:

  1. isLoading is false, so the else block is not rendered. dashboardTileContainer() is null.
  2. The tap({next}) receives the value.
  3. dashboardTileContainer() is still null, so this.dashboardTileContainer()?.clear(); does nothing.
  4. this.dashboardTileContainer()?.createComponent in initializeTiles does nothing, because dashboardTileContainer() is still null
  5. The tap({next}) sets isLoading to false
  6. Angular re-renders, because isLoading is now false.
  7. dashboardTileContainer() is now the ViewContainerRef.

Done. Result: Nothing is rendered, because you tried to render while the ViewContainerRef did not exist yet.

#

An additional comment: Making the next callback async achieves nothing. next is always synchronous. By doing this you are simply ignoring the resulting Promise (because next ignores whatever the callback returns). This is not a good idea, because Promises can fail and then that error just disappears into the void (you might get a console log, but that's not really handling the error...).

#

Additionally, because the Promise is ignored, nothing stops two next callbacks running simultaneously, if the Observable emits faster than the callback completes. That would surely result in a messed up component layout.

delicate depot
#

Ahh I understand now..thanks for the breakdown and sequence of events.
Will try to refactor and fix it. Thanks a lot

blazing haven
#

Really all you need to do is move the ng-container outside of the if. Due to the sequence of events it'll still only show containers after they are loaded.