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
--minimaloption 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:
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:
- We are looping through all
replies - We are printing each
reply'sidandvaluein<li> - Next, in
<li>we are checking if that reply has children - If so, we are creating child list and showing the
idandvalue
The output looks like below:
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.
NgTemplateOutletInserts 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:
- We created a
<ng-template>with#engas TemplateRef. This template also prints thenamefrom it's context object, thanks tolet-name. - We created a
<ng-container>. We asked it to renderengtemplate withmyContextas context. - We created
myContextclass property, which has only one key-value pair:{$implicit: 'World'}. Thanks to$implicit, it's value is set as default value in<ng-template> -
<ng-template>useslet-name, accesses default value frommyContextand assigns it innameand 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:
- We are creating a
<ng-container>. And we are asking it to renderreplyThreadtemplate with{ $implicit: replies }as context. - Next, we are creating a
<ng-template>withreplyThreadas TemplateRef. We are also usinglet-childReplies, so that inner code can usechildReplies. - Now, in
<ng-template>, first we are looping through allchildReplies. - Then, we are checking, if any
replyofchildReplieshas children. - If yes, then we are repeating step 1, but with
{ $implicit: reply.children }as context.
Now, the output is like below:
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:
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:
- We are accepting
repliesas@Input() - We are looping through all the replies and printing
idandvalueinul>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:
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:
The code is available at a public Github repo:
shhdharmen
/
recursive-child
Handle Recursive Inner Child Elements in Angular
RecursiveChild
This is code for my article on Dev.to: Handle Recursive Inner Child Elements in Angular
Thank you,
For reading this article. Let me know your feedback and suggestions in comments sections.
And yes, always believe in yourslef:





Top comments (0)