PingZing
PingZing

Reputation: 962

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?

Upvotes: 1

Views: 22

Answers (1)

Naren Murali
Naren Murali

Reputation: 57986

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

Upvotes: 1

Related Questions