angulartypescriptangular-reactive-formsformbuilderformarray

Angular form with nested arrays error: Cannot read properties of undefined (reading '0')


I have a problem with the form in which the arrays are nested. In the console I saw this error:

ERROR TypeError: Cannot read properties of undefined (reading '0')

This is related to FormBuild and FormArray with FormArray inside

Code snippet:

export class TestPreviewDialogComponent implements OnInit {
    testModule: TestModule;
    testModuleForm: FormGroup;
    levels: LanguageLevel[] = [];

    constructor(
        @Inject(MAT_DIALOG_DATA) public test: TestModule,
        public dialogRef: MatDialogRef<TestPreviewDialogComponent>,
        private formBuilder: FormBuilder,
        private commonService: CommonService
    ) {
        this.testModule = test;

        this.testModuleForm = this.formBuilder.group({
            moduleName: '',
            moduleLevelCode: '',
            moduleDescription: '',
            questions: this.formBuilder.array([]),
        });
    }

    ngOnInit(): void {
        this.getLevels();

        this.setTestModuleForm();

        setTimeout(() => {
            this.testModuleForm.disable();
        }, 1000);
    }

    // Questions
    private get questions(): FormArray {
        return this.testModuleForm.get('questions') as FormArray;
    }

    // PRIVATE
    private getLevels() {
        this.commonService.getLanguageLevels().subscribe({
            next: (value) => {
                this.levels = value;
            },
            error: (err) => {
                console.log(err);
            }
        });
    }

    private setTestModuleForm() {
        this.testModuleForm.controls['moduleName'].setValue(this.testModule.name);
        this.testModuleForm.controls['moduleLevelCode'].setValue(this.testModule.level);
        this.testModuleForm.controls['moduleDescription'].setValue(this.testModule.description);
        this.testModuleForm.controls['questions'].setValue(this.setQuestions());  // Console points this place in code
    }

    setQuestions() {
        this.testModule.questions.forEach((x: Question) => {
            this.questions.push(
                this.formBuilder.group({
                    questionText: x.questionText,
                    questionExplanation: x.explanation,
                    answers: this.setAnswers(x)
                })
            );
        });
    }

    setAnswers(x: Question) {
        let arr = new FormArray([]);
        x.answers.forEach((y: Answer) => {
            arr.push(
                this.formBuilder.group({
                    answerText: y.text,
                    isCorrect: y.isCorrect
                })
            );
        });
        return arr;
    }
}

Full error with stack:

ERROR TypeError: Cannot read properties of undefined (reading '0')
at forms.mjs:1671:18
at forms.mjs:7018:13
at Array.forEach ()
at FormArray._forEachChild (forms.mjs:7017:23)
at assertAllValuesPresent (forms.mjs:1670:13)
at FormArray.setValue (forms.mjs:6840:9)
at TestPreviewDialogComponent.setTestModuleForm (test-preview-dialog.component.ts:66:51)
at TestPreviewDialogComponent.ngOnInit (test-preview-dialog.component.ts:38:14)
at callHook (core.mjs:2498:22)
at callHooks (core.mjs:2467:17)

I feel that the questions are not initialized when setQuestions() function is executed
and that's why it says reading '0'. But I can't overcome it as I haven't much experience

EDIT:

<div class="container-box">

    <div class="d-flex justify-content-center mat-elevation-z8"
         style="border-radius: 10px; margin-bottom: 20px; height: 60px; background-color: #3f51b5;">
        <h1 style="color: #ffffff; text-shadow: 1px 1px black; line-height: 60px;">
            {{testModule.name}} - {{testModule.level}}
        </h1>
    </div>

    <form class="row px-3 py-3 mx-auto mat-elevation-z8" style="border-radius: 10px" [formGroup]="testModuleForm">

        <div class="col-6">
            <div>
                <mat-label>Nazwa testu</mat-label>
                <mat-form-field class="w-100" appearance="fill">
                    <input formControlName="moduleName" matInput>
                </mat-form-field>
            </div>

            <div>
                <mat-label>Poziom testu</mat-label>
                <mat-form-field class="w-100" appearance="fill">
                    <mat-select formControlName="moduleLevelCode" placeholder="Poziom zaawansowania">
                        <mat-option *ngFor="let lvl of levels" [value]="lvl.shortName">
                            {{lvl.shortName}} - {{lvl.fullName}}
                        </mat-option>
                    </mat-select>
                </mat-form-field>
            </div>
        </div>

        <div class="col-6">
            <div>
                <mat-label>Opis</mat-label>
                <mat-form-field class="w-100" appearance="fill">
                    <textarea formControlName="moduleDescription" matInput style="height: 107px; resize: none;"
                    >
                    </textarea>
                </mat-form-field>
            </div>
        </div>

        <div class="mt-3">
            <div class="d-flex justify-content-center">
                <h3 style="font-size: 20px; font-weight:bold;">List pytań</h3>
            </div>
        </div>

        <div class="row col-12 px-3 py-2 mx-auto" formArrayName="questions">
            <div class="question-box px-3 py-2" *ngFor="let question of testModule.questions; let questionIndex=index">

                <div [formGroupName]="questionIndex">

                    <div class="row">
                        <div class="col-1 question-number-box">
                            <span class="question-number">{{questionIndex + 1}}.</span>
                        </div>

                        <div class="col-6">
                            <mat-label>Pytanie</mat-label>
                            <mat-form-field class="w-100" appearance="fill">
                                <input formControlName="questionText" matInput>
                            </mat-form-field>
                        </div>

                        <div class="col-4">
                            <mat-label>Wyjaśnienie</mat-label>
                            <mat-form-field class="w-100" appearance="fill">
                                <input formControlName="questionExplanation" matInput>
                            </mat-form-field>
                        </div>

                        <div class="col-1 remove-question-button-box"></div>
                    </div>

                    <div class="row mt-3">
                        <div class="col-10 d-flex justify-content-center">
                            <h3 style="font-size: 16px; font-weight:bold;">Odpowiedzi</h3>
                        </div>
                        <div class="col-2 d-flex justify-content-center"></div>
                    </div>

                    <div class="answers-box row col-12 py-2" formArrayName="answers">
                        <div *ngFor="let answer of testModule.questions[questionIndex].answers; let answerIndex=index">
                            <div [formGroupName]="answerIndex">
                                <div class="row col-12">
                                    <div class="col-2"></div>
                                    <div class="col-1 answer-number-box">
                                        <span class="answer-number">{{answerIndex + 1}}.</span>
                                    </div>

                                    <div class="col-6">
                                        <mat-label>Odpowiedź</mat-label>
                                        <mat-form-field class="w-100" appearance="fill">
                                            <input formControlName="answerText" matInput>
                                        </mat-form-field>
                                    </div>

                                    <div class="col-2">
                                        <mat-label>Poprawna</mat-label>
                                        <mat-form-field class="w-100" appearance="fill">
                                            <mat-select formControlName="isCorrect" placeholder="Poprawna?">
                                                <mat-option [value]="true">Tak</mat-option>
                                                <mat-option [value]="false">Nie</mat-option>
                                            </mat-select>
                                        </mat-form-field>
                                    </div>

                                    <div class="col-1 remove-answer-button-box"></div>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
                <div class="divider"></div>
            </div>
        </div>
    </form>
</div>


Solution

  • Update: the problem is the line

    this.testModuleForm.controls['questions']
               .setValue(this.setQuestions()); //<--WRONG
    

    Replace by

    //should be simply a call to the function:
    this.setQuestions() //<--OK
    

    See that not give value to the array "questions", else setQuestions give value to the formGroup

    note: Always you mannage a FormArray not loop over a variable, loop over the formArray.controls.

    <!---WRONG--->
    <div *ngFor="let question of testModule.questions; 
                                  let questionIndex=index">
    
    <!--OK--->
    <div *ngFor="let question of questions.controls; 
                    let questionIndex=index">
    

    See that when we have a FormArray inside a FormArray we need a function to "reach" the formArray -can not be a getter else a function we pass an "index" (the function getAnswer)

    <!--WRONG-->
    <div *ngFor="let answer of testModule.questions[questionIndex].answers;
             let answerIndex=index">
    
    <!--OK-->
    <div *ngFor="let answer of getAnswers(questionIndex).controls;
             let answerIndex=index">
    
    
    getAnswer(index:number)
    {
        return this.questions.at(index).get('answers') as FormArray
    }
    

    NOTE: I don't check your code (and don't know if you has really a FormArray), only I want to give a clue.

    NOTE2:You can write in .html

    <pre>
    {{testModuleForm?.value|json}}
    </pre>
    

    To see if you really has the data you want -even you can comment the whole form until the data match do you want-