angularangular-forms

TypeScript cannot guarantee that the controls I am accessing are of type FormGroup or FormArray in my Angular template. How to change that?


I am trying to build a LMS using Angular but I have some issues with Angular forms. I want to be able to add dynamically modules to a given course but also to add dynamically any sort of the 3 types of contents authorized in a given module.

Here is my template:

<main class="container d-flex flex-column">
    <mat-card class="my-5">
        <form [formGroup]="newCourseForm" class="d-flex flex-column">
            <mat-card-content class="card-container m-3">


                <section class="left container d-flex flex-column align-items-center justify-content-center">
                    <h4>Informations sur le cours</h4>
                    <mat-form-field appearance="outline">
                        <mat-label for="name">Nom du Cours:</mat-label>
                        <input matInput id="name" formControlName="name" required>
                    </mat-form-field>

                    <mat-form-field appearance="outline">
                        <mat-label for="imgUrl">l'URL de l'image:</mat-label>
                        <input matInput id="imgUrl" formControlName="imgUrl" required>
                    </mat-form-field>
                </section>

                <section class="right container d-flex flex-column align-items-center justify-content-center">
                    <h4>Informations sur les modules du cours</h4>
                    <div formArrayName="modules">
                        @for(module of modules.controls; track $index){
                            <div class="py-2 my-3 d-flex flex-column align-items-center justify-content-center" [formGroupName]="$index">
                                <mat-accordion >
                                    <mat-expansion-panel>
                                        <mat-expansion-panel-header>
                                            <mat-panel-title>
                                                <h3 >Module {{ modules.at($index).get('title')!.value || ($index + 1) }}</h3>
                                            </mat-panel-title>
    
                                        </mat-expansion-panel-header>
    
                                        <mat-form-field appearance="outline" class="m-1">
                                            <mat-label for="title">Titre du module:</mat-label>
                                            <input matInput formControlName="title" required>
                                        </mat-form-field>
    
                                        <mat-form-field appearance="outline" class="m-1">
                                            <mat-label for="link">Lien du module:</mat-label>
                                            <input matInput formControlName="link" required>
                                        </mat-form-field>
    
                                        <mat-form-field appearance="outline" class="m-1">
                                            <mat-label for="contentType">Type de contenu:</mat-label>
                                     
                                            <mat-select formControlName="contentType" (selectionChange)="onContentTypeChange(module)"  required>
                                                @for(option of options; track $index){
                                                    <mat-option value= "{{option}}">{{option}}</mat-option>
    
                                                }
                                              </mat-select>
    
                                        </mat-form-field>


                                        @if(module.get('contentType')?.value == "Questions"){
                                            <div class="field" formArrayName="parts">
                                                @for(question of module.get('parts')?.controls; track $index){
                                                    <div formGroupName="$index">
                                                        <mat-form-field appearance="outline" class="m-1">
                                                            <mat-label>Question:</mat-label>
                                                            <input matInput formControlName="question" required>
                                                          </mat-form-field>
                                                          <mat-form-field appearance="outline" class="m-1">
                                                            <mat-label>Réponse:</mat-label>
                                                            <input matInput formControlName="response" required>
                                                          </mat-form-field>
                                                    </div>
                                                    <button mat-button (click)="addQuestion(module)">Ajouter une question</button>

                                                }
                                            </div>
                                        }
                                      
                                        <mat-form-field appearance="outline" class="m-1">
                                            <mat-label for="content">Content:</mat-label>
                                            <textarea matInput formArrayName="content" required></textarea>
                                        </mat-form-field>
                                    </mat-expansion-panel>
                                </mat-accordion>
    
                            </div>
                        }
                       
                    </div>
                </section>



            </mat-card-content>
        </form>
        <mat-card-footer>
            <button mat-raised-button (click)="addModule()" class="m-2">Ajouter un module</button>
            <button mat-flat-button (click)="onSubmit()" class="m-2">Créer le cours</button>
        </mat-card-footer>
    </mat-card>
</main>

My Component:

export class CreateCourseComponent implements OnInit{
  ngOnInit(): void {
    this.addModule()
  }
  
  courseService = inject(CourseService)

  options = this.courseService.options;
  // Create a new form group for the course
  newCourseForm = new FormGroup({
    name: new FormControl(""),
    imgUrl: new FormControl(""),
    modules: new FormArray([])      

  });

  addModule() {
    const moduleFormGroup = new FormGroup({
      title: new FormControl(""),
      link: new FormControl(""),
      contentType: new FormControl(this.options[0]), // Default to "Questions"
      content: new FormArray([]), 
      parts: new FormArray([])
    });
    (this.newCourseForm.get('modules') as FormArray).push(moduleFormGroup);
    this.onContentTypeChange(moduleFormGroup)
  }

  addQuestion(moduleFormGroup: any) {
    const partsArray = moduleFormGroup.get('parts') as FormArray;
    partsArray.push(new FormGroup({
      question: new FormControl(""),
      response: new FormControl(""),
    }))
  }

  onContentTypeChange(module: FormGroup | any) {
    if (!module) {
        return; 
    }

    const contentType = module.get('contentType')?.value;
    const contentArray = module.get('content') as FormArray;

    // Clear previous questions if the content type changes
    contentArray.clear();

    if (contentType === 'Questions') {
      module.removeControl('content'); // Remove the old content FormArray

      const partsArray = new FormArray([]);
      module.addControl('parts', partsArray);
      this.addQuestion(module);
    }
  }

  // Method to submit the form
  onSubmit() {
    console.log(this.newCourseForm.value)
    if (this.newCourseForm.valid) {
      const courseData = this.newCourseForm.value;
      console.log(courseData)
      // Call the API to create a new course
      this.courseService.createCourse(courseData).subscribe({
        next: (response: any) => {
          // Handle successful response
          console.log('Course created successfully:', response);
        },
        error: (error: any) => {
          // Handle error
          console.error('Error creating course:', error);
        }
      });
    } else {
      console.error('Form is invalid');
    }
  }


  get modules(): FormArray {
    return this.newCourseForm.get('modules') as FormArray; 
  }

}

I tried to change Type Assertions inside the template using the 'as' keyword but it seems that Angular does not permit that. The $any hack does not work

Take a look:

<div formArrayName="modules">
    @for(module of (modules as FormArray).controls; track $index){
        <div class="py-2 my-3 d-flex flex-column align-items-center justify-content-center" [formGroupName]="$index">
            <mat-accordion>
                <mat-expansion-panel>
                    <mat-expansion-panel-header>
                        <mat-panel-title>
                            <h3>Module {{ module.get('title')?.value || ($index + 1) }}</h3>
                        </mat-panel-title>
                    </mat-expansion-panel-header>

                    <mat-form-field appearance="outline" class="m-1">
                        <mat-label for="title">Titre du module:</mat-label>
                        <input matInput formControlName="title" required>
                    </mat-form-field>

                    <mat-form-field appearance="outline" class="m-1">
                        <mat-label for="link">Lien du module:</mat-label>
                        <input matInput formControlName="link" required>
                    </mat-form-field>

                    <mat-form-field appearance="outline" class="m-1">
                        <mat-label for="contentType">Type de contenu:</mat-label>
                        <mat-select formControlName="contentType" (selectionChange)="onContentTypeChange(module)" required>
                            @for(option of options; track $index){
                                <mat-option value="{{option}}">{{option}}</mat-option>
                            }
                        </mat-select>
                    </mat-form-field>

                    @if(module.get('contentType')?.value == "Questions"){
                        <div class="field" formArrayName="parts">
                            @for(question of module.get('parts')?.controls; track $index){
                                <div formGroupName="$index">
                                    <mat-form-field appearance="outline" class="m-1">
                                        <mat-label>Question:</mat-label>
                                        <input matInput formControlName="question" required>
                                    </mat-form-field>
                                    <mat-form-field appearance="outline" class="m-1">
                                        <mat-label>Réponse:</mat-label>
                                        <input matInput formControlName="response" required>
                                    </mat-form-field>
                                </div>
                            }
                            <button mat-button (click)="addQuestion(module)">Ajouter une question</button>
                        </div>
                    }

                    <mat-form-field appearance="outline" class="m-1">
                        <mat-label for="content">Content:</mat-label>
                        <textarea matInput formControlName="content" required></textarea>
                    </mat-form-field>
                </mat-expansion-panel>
            </mat-accordion>
        </div>
    }
</div>

Solution

  • You can write the same using normal functions, which casts the control passed in:

    convertToFormArray(control) {
      return control as FormArray;
    }
    
    convertToFormGroup(control) {
      return control as FormGroup;
    }
    

    HTML:

    @for(module of convertToFormArray(modules).controls; track $index){
      ...
    }