Search code examples
angularng-bootstrapangular19

How can I create a recursive navigation tree using ng-bootstrap's NgbNav?


I'm trying to use ng-bootstrap's ngbNav directive to create a navigation tree of arbitrary depth. What I have almost works, but I'm fighting with what are think are Angular's template scoping around injection contexts. When I try to load what I have at the moment, I get the following error:

Component update failed: R3InjectorError(Standalone[_AppComponent])[_NgbNavItem -> _NgbNavItem -> _NgbNavItem]: NullInjectorError: No provider for _NgbNavItem!

I'm using Angular v19, and standalone components. I'm using ng-bootstrap v18.0.0.

Here's my template code:

<nav ngbNav #nav="ngbNav" orientation="vertical" class="nav-pills">
  <ng-container *ngTemplateOutlet="recursiveTree; context: { list: urlInfoTable, depth: 0 }"></ng-container>
</nav>

<ng-template #recursiveTree let-list="list" let-depth="depth">
  <ng-container *ngFor="let urlInfo of list">
    <a *ngIf="urlInfo.isLeaf" ngbNavLink [routerLink]="[urlInfo.url]">{{ urlInfo.name }}</a>
    <ul *ngIf="urlInfo.children$ !== null">
    <ng-container
        *ngTemplateOutlet="recursiveTree; context: { list: urlInfo.children$ | async, depth: depth + 1 }"
    ></ng-container>
    </ul>
  </ng-container>
</ng-template>

<main id="sideNavContent">
  <router-outlet />
</main>

And the relevant bits from my component code:

export class NavigationComponent {
  public urlInfoTable: UrlInfo[] = [
    { id: "1", url: "/", name: "Home", isLeaf: true, children$: null },
    { id: "2", url: "/users/self", isLeaf: true, name: "Self", children$: null },
    { id: "3", name: "Organizations", isLeaf: false, children$: this.getOrgs() }
  ];
}

interface UrlInfo {
  id: string;
  isLeaf: boolean;
  name: string;
  url?: string;
  children$: Observable<UrlInfo[]> | null;
}

...and the getOrgs() method returns a placeholder Observable<UrlInfo[]> at the moment.

I have noticed that if I remove ngbNavLink from the template, the error goes away (along with all the Nav functionality, of course). Looking at ng-bootstrap's code for the NgbNavLink directive, it even makes sense--the first thing it tries to do is resolve the NgbNavItem dependency. What I don't understand is how--if at all?--can I ensure that it has the required context here?


Solution

  • First convert the ng-container to a div or span with the navLink directive:

    <div *ngFor="let urlInfo of list" ngbNavItem>
        <a *ngIf="urlInfo.isLeaf" ngbNavLink [routerLink]="[urlInfo.url]"
        >{{ urlInfo.name }}</a
      >
        ...
    

    Then move the ng-template inside the nav so that it has access to ngbNav injection token.

    Full Code:

        <nav ngbNav #nav="ngbNav" orientation="vertical" class="nav-pills">
            <ng-container
                *ngTemplateOutlet="recursiveTree; context: { list: urlInfoTable, depth: 0 }"
            ></ng-container>
    
            <ng-template #recursiveTree let-list="list" let-depth="depth">
                <div *ngFor="let urlInfo of list" ngbNavItem>
                    <a *ngIf="urlInfo.isLeaf" ngbNavLink [routerLink]="[urlInfo.url]"
                        >{{ urlInfo.name }}</a
                    >
                    <ul *ngIf="urlInfo.children$ !== null">
                        <ng-container
                            *ngTemplateOutlet="recursiveTree; context: { list: urlInfo.children$ | async, depth: depth + 1 }"
                        ></ng-container>
                    </ul>
                </div>
            </ng-template>
        </nav>
    

    Stackblitz Demo