RecursiveChild
This is code for my article on Dev.to: Handle Recursive Inner Child Elements in Angular
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.
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.
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:
replies
reply
's id
and value
in <li>
<li>
we are checking if that reply has childrenid
and value
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.
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:
<ng-template>
with #eng
as TemplateRef. This template also prints the name
from it's context object, thanks to let-name
.<ng-container>
. We asked it to render eng
template with myContext
as context.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>
uses let-name
, accesses default value from myContext
and assigns it in name
and it printsOkay. 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:
<ng-container>
. And we are asking it to render replyThread
template with { $implicit: replies }
as context.<ng-template>
with replyThread
as TemplateRef. We are also using let-childReplies
, so that inner code can use childReplies
.<ng-template>
, first we are looping through all childReplies
.reply
of childReplies
has children.{ $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.
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:
replies
as @Input()
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:
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:
This is code for my article on Dev.to: Handle Recursive Inner Child Elements in Angular
For reading this article. Let me know your feedback and suggestions in comments sections.
And yes, always believe in yourslef: