Search code examples
javascriptangulartypescriptrxjs

Can Deeply Nested ng-container Elements Still Render Content Correctly in an Angular Layout Component?


In my Angular application, I've developed a layout component with two columns using CSS. Inside this layout component, I've defined placeholders for the aside and main content using ng-content.

The data for the aside and main sections is fetched from the server. During the loading phase, a loading flag is set to indicate that the data is being fetched. Upon successful data retrieval, an isSuccess flag is returned from the observable along with the response.

To simulate this behavior, I've created a mock data$ observable using the of operator from RxJS. This observable includes a delay to mimic the asynchronous data retrieval process.

The layout isn't rendering the aside and main content properly. This issue arises because the ng-content directive expects the attributes aside and main directly, but they are deeply nested inside ng-container.

The question is, can this setup be modified to render the content correctly?

Here's the revised code:

import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { delay, of, startWith } from 'rxjs';
import 'zone.js';

@Component({
  selector: 'layout',
  standalone: true,
  template: `
    <div class="container">
      <aside>
        <ng-content select="[aside]"></ng-content>
      </aside>
      <main>
        <ng-content select="[main]"></ng-content>
      </main>
    </div>
  `,
  styles: [`.container {display:grid; grid-template-columns: 1fr 2fr} `],
})
export class LayoutComponent {}

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [LayoutComponent, CommonModule],
  template: `
    <layout>
      <ng-container *ngIf="data$ | async as result">
        <div *ngIf="result.loading">loading...</div>
        <ng-container *ngIf="result.isSuccess">
          <div aside>aside content: {{ result.response.aside | json }}</div>
          <div main>main content: {{ result.response.main | json }}</div>
        </ng-container>
      </ng-container>
    </layout>
  `,
})
export class App {
  data$ = of<any>({
    response: { aside: [1, 2, 3, 4], main: [10, 12, 13, 14] },
    isSuccess: true,
  }).pipe(delay(1 * 1000), startWith({ loading: true }));
}

bootstrapApplication(App);

Here's the link to the code: StackBlitz


Solution

  • The ng-content tags must be present at the top level for them to be visible for content projection.

    So I restructured the HTML so that, we get the data and conditionally show the content based on if all the conditions are met, then, using if else (ng-template) we can show the loading section!

    Feel free to modify the code to suit your requirements!

    full code:

    import { CommonModule } from '@angular/common';
    import { Component } from '@angular/core';
    import { bootstrapApplication } from '@angular/platform-browser';
    import { delay, of, startWith } from 'rxjs';
    import { endWith } from 'rxjs/operators';
    import 'zone.js';
    console.clear();
    
    @Component({
      selector: 'layout',
      standalone: true,
      template: `
        <div class="container">
          <aside>
            <ng-content select="[aside]"></ng-content>
          </aside>
          <main>
            <ng-content select="[main]"></ng-content>
          </main>
        </div>
      `,
      styles: [`.container {display:grid; grid-template-columns: 1fr 2fr} `],
    })
    export class LayoutComponent {}
    
    @Component({
      selector: 'app-root',
      standalone: true,
      imports: [LayoutComponent, CommonModule],
      template: `
        <ng-container *ngIf="data$ | async as result">
          <layout *ngIf="result.isSuccess;else loading">
            <div aside>aside content: {{ result.response.aside | json }}</div>
            <div main>main content: {{ result.response.main | json }}</div>
          </layout>
        </ng-container>
        <ng-template #loading>
          <div>loading...</div>
        </ng-template>
      `,
    })
    export class App {
      name = 'Angular';
    
      data$ = of<any>({
        response: { aside: [1, 2, 3, 4], main: [10, 12, 13, 14] },
        isSuccess: true,
      }).pipe(delay(1 * 1000), startWith({ loading: true }));
    }
    
    bootstrapApplication(App);
    

    Stackblitz Demo