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
's id
and value
in <li>
- Next, in
<li>
we are checking if that reply has children
- If so, we are creating child list and showing the
id
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.
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 the name
from it's context object, thanks to let-name
.
- We created a
<ng-container>
. We asked it to render eng
template with myContext
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>
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:
- We are creating a
<ng-container>
. And we are asking it to render replyThread
template with { $implicit: replies }
as context.
- Next, we are creating a
<ng-template>
with replyThread
as TemplateRef. We are also using let-childReplies
, so that inner code can use childReplies
.
- Now, in
<ng-template>
, first we are looping through all childReplies
.
- Then, we are checking, if any
reply
of childReplies
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:
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
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:
RecursiveChild
This project was generated with Angular CLI version 9.1.3.
Development server
Run ng serve
for a dev server. Navigate to http://localhost:4200/
. The app will automatically reload if you change any of the source files.
Code scaffolding
Run ng generate component component-name
to generate a new component. You can also use ng generate directive|pipe|service|class|guard|interface|enum|module
.
Build
Run ng build
to build the project. The build artifacts will be stored in the dist/
directory. Use the --prod
flag for a production build.
Running unit tests
Run ng test
to execute the unit tests via Karma.
Running end-to-end tests
Run ng e2e
to execute the end-to-end tests via Protractor.
Further help
To get more help on the Angular CLI use ng help
or go check out the Angular CLI README.
Thank you,
For reading this article. Let me know your feedback and suggestions in comments sections.
And yes, always believe in yourslef:
Credits
Footer: Photo by Cata on Unsplash