angulartypescriptangular-reactive-formsiterable

Angular: TypeError: Type 'any' must have a Symbol.iterator method that returns an iterator


I'm encountering a "TypeError: Type 'any' must have a Symbol.iterator method that returns an iterator" error in my code. I'm trying to create a feature where users can input subtasks into an input field and confirm them by pressing Enter. These subtasks should then be displayed in a bulleted list format on the screen.

Note: I've commented out the and

  • elements in the html-file to ensure the StackBlitz demo compiles properly.

    Demo@StackBlitz

    Here's a simplified version of my code:

    task-form.component.html:

    <div mat-dialog-title>Create Task</div>
    
    <mat-dialog-content>
      <form [formGroup]="taskForm" (ngSubmit)="onSubmit()">
        <mat-form-field>
          <mat-label>Title</mat-label>
          <input
            matInput
            formControlName="title"
            type="text"
            placeholder="Enter a title"
          />
        </mat-form-field>
    
        <mat-form-field>
          <mat-label>Subtasks</mat-label>
          <input
            matInput
            formControlName="subtasks"
            type="text"
            placeholder="Enter a subtask"
          />
          <button
            mat-icon-button
            matSuffix
            (keydown.enter)="addSubtask()"
            (click)="addSubtask()"
          >
            <mat-icon>add</mat-icon>
          </button>
          <div class="vertical-divider" matSuffix></div>
          <button mat-icon-button matSuffix (click)="clearSubtask()">
            <mat-icon>close</mat-icon>
          </button>
          <mat-hint>Press enter to add a subtask</mat-hint>
        </mat-form-field>
      </form>
      <!-- SHOW HERE ALL SUBTASKS-->
      <!-- 
      <ul>
        @for (subtask of subtasksFormControl.value; track subtask.id){
        <li>
          {{ subtask }}
          <button mat-icon-button (click)="editSubtask(subtask)">
            <mat-icon>edit</mat-icon>
          </button>
          <button mat-icon-button (click)="deleteSubtask(subtask)">
            <mat-icon>delete</mat-icon>
          </button>
        </li>
        }
      </ul>-->
    </mat-dialog-content>
    
    <mat-dialog-actions>
      <button (click)="onSubmit()" mat-raised-button color="primary" type="submit">
        Create Task
      </button>
    </mat-dialog-actions>
    

    task-form.component.ts

    export class TaskFormComponent {
      protected readonly Object = Object;
      taskForm!: FormGroup;
      fromPopup = false;
      constructor(
        private fb: FormBuilder,
        @Optional() private dialogRef: MatDialogRef<TaskFormComponent>,
        @Optional()
        @Inject(MAT_DIALOG_DATA)
        public data: { fromPopup: boolean; task: Task }
      ) {}
    
      ngOnInit() {
        this.fromPopup = !!this.data?.fromPopup;
    
        this.taskForm = this.fb.group({
          id: this.data?.task?.id,
          title: new FormControl(''),
          subtasks: new FormControl([]),
        });
    
        if (this.data?.task) {
          this.taskForm.patchValue({
            id: this.data.task?.id,
            title: this.data.task.title,
            subTasks: this.data.task.subtasks,
          });
        }
      }
    
      public get subtasksFormControl() {
        return this.taskForm.get('subtasks') as FormControl;
      }
    
      public addSubtask() {
        this.taskForm.value.subtasks;
        console.log(this.taskForm.value.subtasks);
      }
    
      public clearSubtask() {
        this.taskForm.patchValue({ subtasks: '' });
      }
    }
    

  • Solution

  • You have to keep the subtasks as an array and the control name subtask to contain only the description, then the complexity goes down and you build all your logic on top of this and leave the form control to only edit the description.

    import { Component } from '@angular/core';
    import { CommonModule, JsonPipe } from '@angular/common';
    import { Inject, Optional } from '@angular/core';
    import { MatButtonModule } from '@angular/material/button';
    import { MatCheckbox } from '@angular/material/checkbox';
    import {
      MatError,
      MatFormField,
      MatFormFieldModule,
      MatLabel,
    } from '@angular/material/form-field';
    import {
      FormArray,
      FormBuilder,
      FormControl,
      FormGroup,
      ReactiveFormsModule,
    } from '@angular/forms';
    import { MatDatepickerModule } from '@angular/material/datepicker';
    import { MatInputModule } from '@angular/material/input';
    import { MatOption } from '@angular/material/core';
    import { MatButtonToggleModule } from '@angular/material/button-toggle';
    import { TitleCasePipe } from '@angular/common';
    import { MatSelect } from '@angular/material/select';
    import { MatIconModule } from '@angular/material/icon';
    import { MatRadioButton, MatRadioGroup } from '@angular/material/radio';
    import {
      MAT_DIALOG_DATA,
      MatDialogModule,
      MatDialogRef,
    } from '@angular/material/dialog';
    import { Task } from '../../app/task';
    import { Subtask } from '../subtask';
    
    @Component({
      selector: 'app-task-form',
      standalone: true,
      imports: [
        CommonModule,
        MatButtonModule,
        MatCheckbox,
        MatError,
        MatFormField,
        MatLabel,
        ReactiveFormsModule,
        MatDatepickerModule,
        MatInputModule,
        MatFormFieldModule,
        MatButtonToggleModule,
        TitleCasePipe,
        MatSelect,
        MatOption,
        MatRadioGroup,
        MatRadioButton,
        MatDialogModule,
        MatIconModule,
        JsonPipe,
      ],
      templateUrl: './task-form.component.html',
      styleUrl: './task-form.component.css',
    })
    export class TaskFormComponent {
      protected readonly Object = Object;
      taskForm!: FormGroup;
      fromPopup = false;
      constructor(
        private fb: FormBuilder,
        @Optional() private dialogRef: MatDialogRef<TaskFormComponent>,
        @Optional()
        @Inject(MAT_DIALOG_DATA)
        public data: { fromPopup: boolean; task: Task }
      ) {
        if (!this.data) {
          this.data = {
            fromPopup: true,
            task: {
              id: 1,
              title: '',
              subtasks: [],
            },
          };
        }
      }
    
      ngOnInit() {
        this.fromPopup = !!this.data?.fromPopup;
    
        this.taskForm = this.fb.group({
          id: this.data?.task?.id,
          title: new FormControl(''),
          subtask: new FormControl(''),
        });
    
        if (this.data?.task) {
          this.taskForm.patchValue({
            id: this.data.task?.id,
            title: this.data.task.title,
          });
          if (this.data.task.subtasks.length) {
            this.data.task.subtasks.forEach((subTask: Subtask) => {
              this.subtasksFormArray.push(new FormControl(subTask.description));
            });
          }
        }
      }
    
      public get subtasksFormArray() {
        return this.taskForm.get('subtasks') as FormArray;
      }
    
      public get subtasksFormArrayControls() {
        return (this.taskForm.get('subtasks') as FormArray)
          .controls as FormControl[];
      }
    
      public addSubtask() {
        const ctrl = this.taskForm.get('subtask');
        this.data.task.subtasks.push({
          id: Math.random(),
          description: this.taskForm.value.subtask,
          isDone: false,
          taskId: this.taskForm.value.id,
        });
      }
    
      public clearSubtask() {
        this.taskForm.patchValue({ subtask: '' });
      }
    
      public editSubtask(subtask: Subtask) {}
    
      public deleteSubtask(subtask: Subtask) {}
    
      public onSubmit() {}
    
      public onReset() {}
    }
    

    Stackblitz Demo