I want to implement a recursive list structure to support nested sub-notes of arbitrary depth (n-levels)? I'm working on a to-do task list assignment, where I want to enable users to create subtasks within tasks, recursively. While I've managed to achieve this up to 3 levels, I'm experiencing issues adding subtasks beyond the 4th level, despite seeing the array elements being added in the console logs at every click but nothing changes on UI.
<div class="note-container">
<h2>Note-Taking App</h2>
<!-- Add Note Form -->
<div class="add-note">
<input type="text" placeholder="Note Title" #noteTitle />
<textarea placeholder="Note Content" #noteContent></textarea>
<button (click)="addNote(noteTitle.value, noteContent.value)">
Add Note
</button>
</div>
<!-- List of Notes -->
<div class="note" *ngFor="let note of notes">
<h3>{{ note.title }}</h3>
<p>{{ note.content }}</p>
<!-- Add Subtask -->
<div>
<input type="text" placeholder="Add Subtask" #subtaskInput />
<button (click)="addSubtask(note, subtaskInput.value)">+</button>
</div>
<!-- Subtask List -->
<ul>
<ng-container *ngFor="let subtask of note.subtasks">
<li>
<input
type="checkbox"
[checked]="subtask.completed"
(change)="toggleSubtask(subtask)"
/>
<span [class.completed]="subtask.completed">{{ subtask.text }}</span>
<button (click)="deleteSubtask(note, subtask.id)">❌</button>
<!-- Nested Subtask Input -->
<div>
<input type="text" placeholder="Add Subtask" #nestedSubtask />
<button (click)="addSubtask(subtask, nestedSubtask.value)">
+
</button>
</div>
<!-- Recursive Subtask List -->
<ul>
<ng-container *ngFor="let nested of subtask.subtasks">
<li>
<input
type="checkbox"
[checked]="nested.completed"
(change)="toggleSubtask(nested)"
/>
<span [class.completed]="nested.completed">{{
nested.text
}}</span>
<input type="text" placeholder="Add Subtask" #nestedD />
<button (click)="addSubtask(nested, nestedD.value)">+</button>
<button (click)="deleteSubtask(subtask, nested.id)">❌</button>
</li>
</ng-container>
</ul>
</li>
</ng-container>
</ul>
<button (click)="deleteNote(note.id)">Delete Note</button>
</div>
</div>
TS File
export class AppComponent {
notes: Note[] = [];
addNote(title: string, content: string) {
if (!title.trim() || !content.trim()) return;
this.notes.push({
id: Date.now(),
title,
content,
subtasks: [],
});
}
addSubtask(parent: { subtasks: Subtask[] }, text: string) {
console.log(parent.subtasks);
console.log(text);
if (!text.trim()) return;
console.log(text);
parent.subtasks.push({
id: Date.now(),
text,
completed: false,
subtasks: [], // Support nested subtasks
});
}
toggleSubtask(subtask: Subtask) {
subtask.completed = !subtask.completed;
}
deleteSubtask(parent: { subtasks: Subtask[] }, subtaskId: number) {
parent.subtasks = parent.subtasks.filter(
(subtask) => subtask.id !== subtaskId
);
}
deleteNote(id: number) {
this.notes = this.notes.filter((note) => note.id !== id);
}
}
Console
To write recursion you can follow the below process.
Here it will be the for loop that runs over the property subtasks
, it will show either a task
or subtask
and below it show the children of that particular element.
Here notice that we are passing a property call parent
, which will be either a task
, subtask
or note
this is because when we delete, we need the parent element to delete it from the subtasks
array.
To distinguish between task and subtask I use the property type
, you can also merge these two into a single template.
We call the sample template within the template (recursion) so that it can be any level of subtasks.
<ng-template #taskListTemplate let-parent="parent" let-type="type">
<ng-container *ngIf="parent?.subtasks?.length">
<ul>
<ng-container *ngFor="let task of parent.subtasks">
<li>
<ng-container *ngIf="type === 'task'">
<ng-container
*ngTemplateOutlet="
taskTemplate;
context: { task: task, parent: parent }
"
></ng-container>
</ng-container>
<ng-container *ngIf="type === 'subtask'">
<ng-container
*ngTemplateOutlet="
subTaskTemplate;
context: { subtask: task, parent: parent }
"
></ng-container>
</ng-container>
<ng-container
*ngTemplateOutlet="
taskListTemplate;
context: { parent: task, type: 'subtask' }
"
></ng-container>
</li>
</ng-container>
</ul>
</ng-container>
</ng-template>
We can split the logic of tasks
and subtasks
into two templates and call them from inside our list template.
<ng-template #taskTemplate let-task="task" let-parent="parent">
<input type="checkbox" [checked]="task.completed" (change)="toggleSubtask(task)" />
<span [class.completed]="task.completed">{{ task.text }}</span>
<button (click)="deleteSubtask(parent, task.id)">❌</button>
<!-- Nested Subtask Input -->
<div>
<input type="text" placeholder="Add Subtask" #nestedSubtask />
<button (click)="addSubtask(task, nestedSubtask.value)">+</button>
</div>
</ng-template>
<ng-template #subTaskTemplate let-subtask="subtask" let-parent="parent">
<input
type="checkbox"
[checked]="subtask.completed"
(change)="toggleSubtask(subtask)"
/>
<span [class.completed]="subtask.completed">{{ subtask.text }}</span>
<input type="text" placeholder="Add Subtask" #nestedD />
<button (click)="addSubtask(subtask, nestedD.value)">+</button>
<button (click)="deleteSubtask(parent, subtask.id)">❌</button>
</ng-template>
Now we can call this template initially using the note
as the parent. It will recursively create the lists.
<div>
<input type="text" placeholder="Add Subtask" #subtaskInput />
<button (click)="addSubtask(note, subtaskInput.value)">+</button>
</div>
<ng-container
*ngTemplateOutlet="taskListTemplate; context: { parent: note, type: 'task' }"
></ng-container>
import { Component } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-root',
imports: [CommonModule],
template: `
<div class="note-container">
<h2>Note-Taking App</h2>
<!-- Add Note Form -->
<div class="add-note">
<input type="text" placeholder="Note Title" #noteTitle />
<textarea placeholder="Note Content" #noteContent></textarea>
<button (click)="addNote(noteTitle.value, noteContent.value)">Add Note</button>
</div>
<!-- List of Notes -->
<div class="note" *ngFor="let note of notes">
<h3>{{ note.title }}</h3>
<p>{{ note.content }}</p>
<!-- Add Subtask -->
<div>
<input type="text" placeholder="Add Subtask" #subtaskInput />
<button (click)="addSubtask(note, subtaskInput.value)">+</button>
</div>
<ng-container
*ngTemplateOutlet="taskListTemplate; context: { parent: note, type: 'task' }"
></ng-container>
<button (click)="deleteNote(note.id)">Delete Note</button>
<ng-template #taskTemplate let-task="task" let-parent="parent">
<input type="checkbox" [checked]="task.completed" (change)="toggleSubtask(task)" />
<span [class.completed]="task.completed">{{ task.text }}</span>
<button (click)="deleteSubtask(parent, task.id)">❌</button>
<!-- Nested Subtask Input -->
<div>
<input type="text" placeholder="Add Subtask" #nestedSubtask />
<button (click)="addSubtask(task, nestedSubtask.value)">+</button>
</div>
</ng-template>
<ng-template #subTaskTemplate let-subtask="subtask" let-parent="parent">
<input
type="checkbox"
[checked]="subtask.completed"
(change)="toggleSubtask(subtask)"
/>
<span [class.completed]="subtask.completed">{{ subtask.text }}</span>
<input type="text" placeholder="Add Subtask" #nestedD />
<button (click)="addSubtask(subtask, nestedD.value)">+</button>
<button (click)="deleteSubtask(parent, subtask.id)">❌</button>
</ng-template>
<ng-template #taskListTemplate let-parent="parent" let-type="type">
<ng-container *ngIf="parent?.subtasks?.length">
<ul>
<ng-container *ngFor="let task of parent.subtasks">
<li>
<ng-container *ngIf="type === 'task'">
<ng-container
*ngTemplateOutlet="
taskTemplate;
context: { task: task, parent: parent }
"
></ng-container>
</ng-container>
<ng-container *ngIf="type === 'subtask'">
<ng-container
*ngTemplateOutlet="
subTaskTemplate;
context: { subtask: task, parent: parent }
"
></ng-container>
</ng-container>
<ng-container
*ngTemplateOutlet="
taskListTemplate;
context: { parent: task, type: 'subtask' }
"
></ng-container>
</li>
</ng-container>
</ul>
</ng-container>
</ng-template>
</div>
</div>
`,
})
export class App {
notes: any[] = [];
addNote(title: string, content: string) {
if (!title.trim() || !content.trim()) return;
this.notes.push({
id: Date.now(),
title,
content,
subtasks: [],
});
}
addSubtask(parent: { subtasks: any[] }, text: string) {
console.log(parent.subtasks);
console.log(text);
if (!text.trim()) return;
console.log(text);
parent.subtasks.push({
id: Date.now(),
text,
completed: false,
subtasks: [], // Support nested subtasks
});
}
toggleSubtask(subtask: any) {
subtask.completed = !subtask.completed;
}
deleteSubtask(parent: { subtasks: any[] }, subtaskId: number) {
parent.subtasks = parent.subtasks.filter(
(subtask) => subtask.id !== subtaskId
);
}
deleteNote(id: number) {
this.notes = this.notes.filter((note) => note.id !== id);
}
}
bootstrapApplication(App);