Handle Recursive Inner Child Elements in Angular

Handle Recursive Inner Child Elements in Angular

ยท

10 min read

Let's see how we can manage recursive child elements in Angular using 2 methods

Many times, we face a situation, where we need some kind of architecture that helps us achieve recursive occurrence of child elements within same child elements. For example, replies or comments of in a discussion. Each reply has same functionality and UI and there can be many replies under one reply.


First things first

Open up your ๐Ÿ‘จโ€๐Ÿ’ป terminal and run

npm i -g @angular/cli
ng new recursive-child --defaults --minimal --inlineStyle

๐Ÿ‘‰ Tip: Do not use --minimal option in actual app. We are using it here only for learning purpose. You can learn more about CLI Options here.

cd recursive-child
ng serve -o

Great ๐Ÿ‘. We have completed the initial setup. Youโ€™ve done a lot today. What a ๐ŸŒ„ day. You should take a ๐Ÿ›Œ rest. Go ๐Ÿ˜ด nap or get a ๐Ÿฒ snack. Continue once you're ๐Ÿ˜€ awake.

Code

We will try to keep this as minimum as possible.

First, open src\app\app.component.ts and add a class property name replies:

// src\app\app.component.ts

...
export class AppComponent {
  replies = [
    {
      id: 1,
      value: 'Lorem'
    },
    {
      id: 2,
      value: 'Ipsum'
    },
    {
      id: 3,
      value: 'Dolor'
    },
    {
      id: 4,
      value: 'Sit'
    }
  ]
}

and also replace the template HTML and styles with below:

// src\app\app.component.ts

...
template: `
    <ul>
      <li *ngFor="let reply of replies"><b>{{reply.id}}:</b> {{reply.value}}</li>
    </ul>
  `,
  styles: [
    "ul { list-style: none }"
  ]
...

The output will look like below:

Alt Text

Now, ideally the property replies should be coming from your API and you should set it in ngOnInit life-cycle hook.

As we discussed initially, in actual scenarios, a reply can have many replies. So, let's make change for the in our property:

// src\app\app.component.ts

...
replies = [
    {
      id: 1,
      value: 'Lorem',
      children: [
        {
          id: 1.1,
          value: 'consectetur',
          children: [
            {
              id: '1.1.1',
              value: 'adipiscing '
            }
          ]
        }
      ]
    },
    {
      id: 2,
      value: 'Ipsum'
    },
    {
      id: 3,
      value: 'Dolor',
      children: [
        {
          id: 3.1,
          value: 'eiusmod'
        },
        {
          id: 3.2,
          value: 'labore',
          children: [
            {
              id: '3.2.1',
              value: 'aliqua'
            }
          ]
        }
      ]
    },
    {
      id: 4,
      value: 'Sit'
    }
  ]

Now, this won't change anything in the output. Because we haven't handled children in our template.

Let's try something. Change template HTML to below:

// src\app\app.component.ts

...
template: `
    <ul>
      <li *ngFor="let reply of replies">
        <b>{{ reply.id }}:</b> {{ reply.value }}
        <ul *ngIf="reply.children">
          <li *ngFor="let childReply of reply.children">
            <b>{{ childReply.id }}:</b> {{ childReply.value }}
          </li>
        </ul>
      </li>
    </ul>
  `,

So, what we are doing above:

  1. We are looping through all replies
  2. We are printing each reply's id and value in <li>
  3. Next, in <li> we are checking if that reply has children
  4. If so, we are creating child list and showing the id and value

The output looks like below:

Alt Text

It worked, right? Yes, but... it's showing just first level of children. With our current approach, we can't cover all levels of children in each reply. Here, we need some ๐Ÿคฏ dynamic solution. There can be 2 ways to achieve this.

1. ng-template & ng-container

First, let's see what ng-template is, from Angular's documentation:

The is an Angular element for rendering HTML. It is never displayed directly. In fact, before rendering the view, Angular replaces the and its contents with a comment.

Simply put, ng-template does not render anything directly whatever we write inside it. I wrote directly, so it must render indirectly, right?

We can render content of ng-template using NgTemplateOutlet directive in ng-container.

The Angular <ng-container> is a grouping element that doesn't interfere with styles or layout because Angular doesn't put it in the DOM.

Angular doesn't render ng-container, but it renders content inside it.

NgTemplateOutlet Inserts an embedded view from a prepared TemplateRef.

NgTemplateOutlet takes an expression as input, which should return a TemplateRef. TemplateRef is nothing but #template given in ng-template. For example, templateName is TemplateRef in below line:

<ng-template #templateName> some content </ng-template>

We can also give some data to ng-template by setting [ngTemplateOutletContext]. [ngTemplateOutletContext] should be an object, the object's keys will be available for binding by the local template let declarations. Using the key $implicit in the context object will set its value as default.

See below code for example:

// example

@Component({
  selector: 'ng-template-outlet-example',
  template: `
    <ng-container *ngTemplateOutlet="eng; context: myContext"></ng-container>
    <ng-template #eng let-name><span>Hello {{name}}!</span></ng-template>
`
})
export class NgTemplateOutletExample {
  myContext = {$implicit: 'World'};
}

What's happening in above example:

  1. We created a <ng-template> with #eng as TemplateRef. This template also prints the name from it's context object, thanks to let-name.
  2. We created a <ng-container>. We asked it to render eng template with myContext as context.
  3. We created myContext class property, which has only one key-value pair: {$implicit: 'World'}. Thanks to $implicit, it's value is set as default value in <ng-template>
  4. <ng-template> uses let-name, accesses default value from myContext and assigns it in name and it prints

Okay. Let's see how we can use all of it in our problem.

Let's change the template HTML code to below:

// src\app\app.component.ts

...
template: `
    <ng-container
      *ngTemplateOutlet="replyThread; context: { $implicit: replies }"
    ></ng-container>
    <ng-template #replyThread let-childReplies>
      <ul>
        <li *ngFor="let reply of childReplies">
          <b>{{ reply.id }}:</b> {{ reply.value }}
          <ng-container *ngIf="reply.children">
            <ng-container
              *ngTemplateOutlet="
              replyThread;
                context: { $implicit: reply.children }
              "
            ></ng-container>
          </ng-container>
        </li>
      </ul>
    </ng-template>
  `,
...

Almost everything is same as what was happening in previous example, but there are few additional things which are happening here. Let's see in details:

  1. We are creating a <ng-container>. And we are asking it to render replyThread template with { $implicit: replies } as context.
  2. Next, we are creating a <ng-template> with replyThread as TemplateRef. We are also using let-childReplies, so that inner code can use childReplies.
  3. Now, in <ng-template>, first we are looping through all childReplies.
  4. Then, we are checking, if any reply of childReplies has children.
  5. If yes, then we are repeating step 1, but with { $implicit: reply.children } as context.

Now, the output is like below:

Alt Text

Cool, it renders all the levels of child replies. Now, let's look at the second approach.

2. A reply Component

Instead of using ng-container and ng-template, we can also create a component to achieve same behavior.

Let's create a component:

ng g c reply

It will create a folder and component inside it like below:

Alt Text

Let's open src\app\reply\reply.component.ts and edit it like below:

// src\app\reply\reply.component.ts

import { Component, OnInit, Input } from "@angular/core";

@Component({
  selector: "app-reply",
  template: `
    <ul>
      <li *ngFor="let reply of replies">
        <b>{{ reply.id }}:</b> {{ reply.value }}
      </li>
    </ul>
  `,
  styles: [],
})
export class ReplyComponent implements OnInit {
  @Input() replies: { id: string | number; value: string; children: any[] }[];

  constructor() {}

  ngOnInit(): void {}
}

Here, we did 2 main things:

  1. We are accepting replies as @Input()
  2. We are looping through all the replies and printing id and value in ul > li

Let's use app-reply component in our main app-root component:

// src\app\app.component.ts

...
template: `
    <app-reply [replies]="replies"></app-reply>
  `,
...

Well, the output still reflects only 1st level of replies:

Alt Text

Let's handle children, too:

// src\app\reply\reply.component.ts

...
template: `
    <ul>
      <li *ngFor="let reply of replies">
        <b>{{ reply.id }}:</b> {{ reply.value }}
        <!-- ๐Ÿšจ Note the usage of component inside same component -->
        <app-reply *ngIf="reply.children" [replies]="reply.children"></app-reply>
      </li>
    </ul>
  `,
...

You noticed the change, right? We're using <app-reply> again inside <app-reply> if that reply has children.

Now the output is correct, it renders all levels of replies:

Alt Text

The code is available at a public Github repo:

Thank you,

For reading this article. Let me know your feedback and suggestions in comments sections.

And yes, always believe in yourself:

Alt Text

Credits

Footer Photo by Cata on Unsplash

Did you find this article valuable?

Support Dharmen Shah by becoming a sponsor. Any amount is appreciated!