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:
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
'sid
andvalue
in<li>
- Next, in
<li>
we are checking if that reply has children - If so, we are creating child list and showing the
id
andvalue
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.
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:
- We created a
<ng-template>
with#eng
as TemplateRef. This template also prints thename
from it's context object, thanks tolet-name
. - We created a
<ng-container>
. We asked it to rendereng
template withmyContext
as context. - 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>
<ng-template>
useslet-name
, accesses default value frommyContext
and assigns it inname
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:
- We are creating a
<ng-container>
. And we are asking it to renderreplyThread
template with{ $implicit: replies }
as context. - Next, we are creating a
<ng-template>
withreplyThread
as 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
reply
ofchildReplies
has 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
replies
as@Input()
- We are looping through all the replies and printing
id
andvalue
inul
>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:
Thank you,
For reading this article. Let me know your feedback and suggestions in comments sections.
And yes, always believe in yourself: