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>
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;
}
@for(module of convertToFormArray(modules).controls; track $index){
...
}