r3plica
r3plica

Reputation: 13367

Angular repeat html for children using ng-template

I assume this is simple but for some reason I can't figure it out. I am building a simple submenu. I have created the component:

export class SubMenuComponent implements OnInit {
    @Input() links: MenuItem[];

    constructor() {}

    ngOnInit(): void {}
}

The MenuItem looks like this:

export class MenuItem {
    label: string;
    path: string;
    open: boolean;
    children?: MenuItem[];
}

And the Html looks like this:

<ul class="app-sub-menu list-unstyled">
    <li *ngFor="let link of links" routerLinkActive="active"><a class="btn-link" [routerLink]="link.path"
            routerLinkActive="active" [routerLinkActiveOptions]="{exact: link.path === '/'}">{{ link.label }}</a>

        <span class="toggle" *ngIf="link.children?.length" (click)="link.open = !link.open">
            <mat-icon *ngIf="link.isActive || link.open">keyboard_arrow_down</mat-icon>
            <mat-icon *ngIf="!link.isActive && !link.open">keyboard_arrow_up</mat-icon>
        </span>

        <ul class="list-unstyled" [class.open]="link.open" *ngIf="link.children?.length">
            <li *ngFor="let link of link.children" routerLinkActive="active"><a class="btn-link"
                    routerLinkActive="active" [routerLink]="link.path" #link>{{ link.label }}</a>

                <span class="toggle" *ngIf="link.children?.length" (click)="link.open = !link.open">
                    <mat-icon *ngIf="link.isActive || link.open">keyboard_arrow_down</mat-icon>
                    <mat-icon *ngIf="!link.isActive && !link.open">keyboard_arrow_up</mat-icon>
                </span>

                <ul class="list-unstyled" [class.open]="link.open" *ngIf="link.children?.length">
                    <li *ngFor="let link of link.children" routerLinkActive="active"><a class="btn-link"
                            routerLinkActive="active" [routerLink]="link.path">{{ link.label }}</a>
                    </li>
                </ul>
            </li>
        </ul>
    </li>
</ul>

As you can see, this current sub-menu only goes down 3 levels. I would like it to be infinitely nested. So I decided to use ng-template, but I can't get it to work. I thought it would be as simple as:

<ul class="app-sub-menu list-unstyled">
    <ng-template *ngTemplateOutlet="link" *ngFor="let link of links"></ng-template>
</ul>

<ng-template #link>
    <li *ngFor="let link of links" routerLinkActive="active"><a class="btn-link" [routerLink]="link.path"
            routerLinkActive="active" [routerLinkActiveOptions]="{exact: link.path === '/'}">{{ link.label }}</a>

        <span class="toggle" *ngIf="link.children?.length" (click)="link.open = !link.open">
            <mat-icon *ngIf="link.isActive || link.open">keyboard_arrow_down</mat-icon>
            <mat-icon *ngIf="!link.isActive && !link.open">keyboard_arrow_up</mat-icon>
        </span>

        <ul class="list-unstyled" [class.open]="link.open" *ngIf="link.children?.length">
            <ng-template *ngTemplateOutlet="link" *ngFor="let link of link.children"></ng-template>
        </ul>
    </li>
</ng-template>

But when I try to use that, I get an error:

Can't have multiple template bindings on one element. Use only one attribute prefixed with *

So I changed it to this:

And now I get a new error:

templateRef.createEmbeddedView is not a function

Does anyone know what I can do to get this to work?


Reading this:

https://medium.com/@nehaabrol87/the-power-of-angulars-ngtemplateoutlet-used-to-implement-a-nested-data-structure-11c741e6b48c

It looks like I was using my template incorrectly, so I changed it to this:

<ul class="app-sub-menu list-unstyled">
    <ng-container *ngTemplateOutlet="link; context: { $implicit: links }"></ng-container>
</ul>

<ng-template #link let-links>
    <li *ngFor="let link of links" routerLinkActive="active"><a class="btn-link" [routerLink]="link.path"
            routerLinkActive="active" [routerLinkActiveOptions]="{exact: link.path === '/'}">{{ link.label }}</a>

        <span class="toggle" *ngIf="link.children?.length" (click)="link.open = !link.open">
            <mat-icon *ngIf="link.isActive || link.open">keyboard_arrow_down</mat-icon>
            <mat-icon *ngIf="!link.isActive && !link.open">keyboard_arrow_up</mat-icon>
        </span>

        <ul class="list-unstyled" [class.open]="link.open" *ngIf="link.children?.length">
            <ng-container *ngTemplateOutlet="link; context: { $implicit: link.children }"></ng-container>
        </ul>
    </li>
</ng-template>

But I am still getting the error :(

templateRef.createEmbeddedView is not a function

Upvotes: 0

Views: 526

Answers (1)

r3plica
r3plica

Reputation: 13367

I figured it out, it was because my template id was the same as the actual model. So I changed it to this:

<ul class="app-sub-menu list-unstyled">
    <ng-template #nestedList let-links>
        <li *ngFor="let link of links" routerLinkActive="active"><a class="btn-link" [routerLink]="link.path"
                routerLinkActive="active" [routerLinkActiveOptions]="{exact: link.path === '/'}">{{ link.label }}</a>

            <span class="toggle" *ngIf="link.children?.length" (click)="link.open = !link.open">
                <mat-icon *ngIf="link.isActive || link.open">keyboard_arrow_down</mat-icon>
                <mat-icon *ngIf="!link.isActive && !link.open">keyboard_arrow_up</mat-icon>
            </span>

            <ul class="list-unstyled" [class.open]="link.open" *ngIf="link.children?.length">
                <ng-container *ngTemplateOutlet="nestedList; context: { $implicit: link.children }"></ng-container>
            </ul>
        </li>
    </ng-template>
    <ng-container *ngTemplateOutlet="nestedList; context: { $implicit: links }"></ng-container>
</ul>

Upvotes: 2

Related Questions