最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

html - How to create a recursive, n-level nested task list, where users can add subtasks within tasks, and display it correctly&

programmeradmin2浏览0评论

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]="subtaskpleted"
        (change)="toggleSubtask(subtask)"
      />
      <span [classpleted]="subtaskpleted">{{ 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]="nestedpleted"
              (change)="toggleSubtask(nested)"
            />
            <span [classpleted]="nestedpleted">{{
              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) {
subtaskpleted = !subtaskpleted;
}

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

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]="subtaskpleted"
        (change)="toggleSubtask(subtask)"
      />
      <span [classpleted]="subtaskpleted">{{ 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]="nestedpleted"
              (change)="toggleSubtask(nested)"
            />
            <span [classpleted]="nestedpleted">{{
              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) {
subtaskpleted = !subtaskpleted;
}

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

Share Improve this question asked Mar 17 at 13:10 sejal purohitsejal purohit 411 silver badge5 bronze badges
Add a comment  | 

1 Answer 1

Reset to default 1

To write recursion you can follow the below process.

Identify the initial templates:

List Template:

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>

TASK/SUBTASK 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]="taskpleted" (change)="toggleSubtask(task)" />
        <span [classpleted]="taskpleted">{{ 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]="subtaskpleted"
            (change)="toggleSubtask(subtask)"
        />
        <span [classpleted]="subtaskpleted">{{ 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>

Calling the list template once (recursion takes care of the rest):

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>

Full Code:

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]="taskpleted" (change)="toggleSubtask(task)" />
      <span [classpleted]="taskpleted">{{ 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]="subtaskpleted"
        (change)="toggleSubtask(subtask)"
      />
      <span [classpleted]="subtaskpleted">{{ 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) {
    subtaskpleted = !subtaskpleted;
  }

  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);

Stackblitz Demo

与本文相关的文章

发布评论

评论列表(0)

  1. 暂无评论