angulartypescriptangular-reactive-formsangular-forms

How to sum the value from dynamic nested array field and create a object for per input field


I am trying to develop a number grading system for a course using Angular 8. The real scenario is quite complex, so I separated the part and created a new project to explain where I have the problem. If you look at the image you will get an idea, what I am trying to accomplish. Assume this is the grade form of one course. For a course, there can have different exams. Students who are enrolled in this course, they will attend those specific exams for this course and then teacher will submit the number using this form. Suppose, for the first row first column (Mid Exam), the form should not allow you to provide more than 25 marks or less than zero. If you put 20 on the Mid Exam field then Total will show 20, GP will show 0.00, Grade will show F. Then if you put 30 on the Final exam field, Total value will be updated to 50, GP will be updated to 0.00 and will be updated to F. Again if you put 40 on the Assessment field, Total value will be updated to 90, GP will be updated to 4.00 and will be updated to A. Same thing will be happened to for all the row. If you click on the Save button, numbers will be submitted.

enter image description here

Now I am describing the code that I tried and my problem, here studentList, examList and gradePointList array will populate the data for a course from backend service. I created populateSomeDemoData method to generate this data. studentList is an array of the Student object which contains students' id and name. examList is an array of the Exam object which contains exam id, exam name and total marks of this exam. gradePointList contains the range of the number of letter grade and grade point. Suppose if the total mark is between 90 (minimumNumber) and 100 (maximumNumber) the grade point will be 4.00 and letter grade will be A. I tried to create a form gradeForm and applied a loop on examList for per row to create input field. But for this, I couldn't able to access the specific field using form control that's why I failed to calculate total marks and grade point for a row. Also for the same reason, I failed to create the list of Grade object which will be sent to the backend. Grade is the object of every field which will contain student, exam and number. If a field is updated, this grade object will also be updated. If Save button triggers, this number list will be passed to the backend.

HTML part is:

<form [formGroup]="gradeForm" (ngSubmit)="saveGrade()">
  <table>
    <thead>
    <tr>
      <th> #</th>
      <th>Student</th>
      <th *ngFor="let exam of examList">{{exam.name}} ({{exam.marks}}%)</th>
      <th>Total</th>
      <th>GP</th>
      <th>Grade</th>
    </tr>
    </thead>

    <tbody>
    <tr *ngFor="let student of studentList; let i = index">
      <th> {{ i + 1 }} </th>
      <th> {{ student.id }} <br/> {{student.name}} </th>
      <th *ngFor="let grade of gradeFields.controls; let j = index" formArrayName="gradeFields">
        <input type="number" [formControlName]="j">
      </th>
      <th>??</th>
      <th>??</th>
      <th>??</th>
    </tr>
    </tbody>
  </table>

  <div class="wrapper">
    <button class="button">Save Grade</button>
  </div>
</form>

TypeScript Part is:

export class AppComponent {

  public gradeForm: FormGroup;

  // This arrays will populate dynamically from the database
  public examList: Exam[] = [];
  public studentList: Student[] = [];
  public gradePointList: GradePoint[] = [];
  public gradeList: Grade[] = [];

  constructor(private formBuilder: FormBuilder) {

    this.populateSomeDemoData();

    this.createGradeForm();
  }

  createGradeForm(): void {
    this.gradeForm = this.formBuilder.group({
      gradeFields: this.formBuilder.array([
        this.formBuilder.control('')
      ])
    });

    for (let i = 0; i < this.examList.length - 1; i++) {
      this.gradeFields.push(this.formBuilder.control(''));
    }
  }

  get gradeFields() {
    return this.gradeForm.get('gradeFields') as FormArray;
  }

  saveGrade(): void {
    console.log(this.gradeForm.get('gradeFields').value);
    console.log(this.gradeList);
    console.log('Grade List Saved');
  }

  populateSomeDemoData(): void {
    // Static value for populating arrays for easy explanation
    const midTerm = new Exam();
    midTerm.id = '1';
    midTerm.name = 'Mid Exam';
    midTerm.marks = 25;

    const finalExam = new Exam();
    finalExam.id = '2';
    finalExam.name = 'Final Exam';
    finalExam.marks = 30;

    const assessmentExam = new Exam();
    assessmentExam.id = '3';
    assessmentExam.name = 'Assessment';
    assessmentExam.marks = 40;

    const attendance = new Exam();
    attendance.id = '4';
    attendance.name = 'Attendance';
    attendance.marks = 5;

    this.examList.push(midTerm);
    this.examList.push(finalExam);
    this.examList.push(assessmentExam);
    this.examList.push(attendance);


    // Static value for populating arrays for easy explanation
    const student1 = new Student();
    student1.id = '14101561';
    student1.name = 'Petey Cruiser';

    const student2 = new Student();
    student2.id = '14112201';
    student2.name = 'Bob Frapples';

    const student3 = new Student();
    student3.id = '14112202';
    student3.name = 'Paul Molive';

    const student4 = new Student();
    student4.id = '16113004';
    student4.name = 'Anna Mull';

    const student5 = new Student();
    student5.id = '16113005';
    student5.name = 'Gail Forcewind';

    this.studentList.push(student1);
    this.studentList.push(student2);
    this.studentList.push(student3);
    this.studentList.push(student4);
    this.studentList.push(student5);


    // Static value for populating arrays for easy explanation
    const aGradePoint = new GradePoint();
    aGradePoint.minimumNumber = 90;
    aGradePoint.maximumNumber = 100;
    aGradePoint.gradePoint = 4;
    aGradePoint.letterGrade = 'A';

    const bGradePoint = new GradePoint();
    bGradePoint.minimumNumber = 80;
    bGradePoint.maximumNumber = 89;
    bGradePoint.gradePoint = 3.5;
    bGradePoint.letterGrade = 'B';

    const cGradePoint = new GradePoint();
    cGradePoint.minimumNumber = 70;
    cGradePoint.maximumNumber = 79;
    cGradePoint.gradePoint = 3;
    cGradePoint.letterGrade = 'C';

    const dGradePoint = new GradePoint();
    dGradePoint.minimumNumber = 60;
    dGradePoint.maximumNumber = 69;
    dGradePoint.gradePoint = 2.5;
    dGradePoint.letterGrade = 'D';

    const fGradePoint = new GradePoint();
    fGradePoint.minimumNumber = 0;
    fGradePoint.maximumNumber = 59;
    fGradePoint.gradePoint = 0;
    fGradePoint.letterGrade = 'F';
    this.gradePointList.push(aGradePoint);
    this.gradePointList.push(bGradePoint);
    this.gradePointList.push(cGradePoint);
    this.gradePointList.push(dGradePoint);
    this.gradePointList.push(fGradePoint);
  }
}

export class Student {
  public id: string;
  public name: string;

  constructor(student?) {
    student = student || {};
    this.id = student.id || null;
    this.name = student.name || null;
  }
}

export class Exam {
  public id: string;
  public name: string;
  public marks: number;

  constructor(exam?) {
    exam = exam || {};
    this.id = exam.id || null;
    this.name = exam.name || null;
    this.marks = exam.marks || 0;
  }
}

export class GradePoint {
  public minimumNumber: number;
  public maximumNumber: number;
  public gradePoint: number;
  public letterGrade: string;

  constructor(gradePoint?) {
    gradePoint = gradePoint || {};
    this.minimumNumber = gradePoint.minimumNumber || 0;
    this.maximumNumber = gradePoint.maximumNumber || 59;
    this.gradePoint = gradePoint.gradePoint || 0;
    this.letterGrade = gradePoint.letterGrade || 'F';
  }
}

export class Grade {
  public student: Student;
  public exam: Exam;
  public number: number;

  constructor(grade?) {
    grade = grade || {};
    this.student = grade.student || null;
    this.exam = grade.exam || null;
    this.number = grade.number || null;
  }
}

I have created a StackBlitz for better understanding of the code. Please click on this link to view the code. Click here

I saw a lot of post which is related to Form Arrays but my scenario is quite different. Here I don't have any Add button to push array or calculate the values. And there are fixed forms for all cases. But in my case, the array also will be generated from the list because examList is dynamic.

I assume I have problem with creating on the form or my approaches are wrong. Please help me to accomplish my objective generate total marks and creating Grade list. It would be appreciated. Thanks in advance.


Solution

  • Ok I can see that to meet your requirements you have to use nested arrays, you need to follow this structure, or at least a 2 d array, grades[][]: grades[studentId][examId], you will understand more when you read the code, there is a lot missing in your code, for example the validations for the marks and the calculation for total and GP fields, I will not bother with these parts (I added a sample for calculation and validation, but you need to modify it), but I will show you how to populate your form group properly, and how to use it, having a dynamic view. As I mention you need a 2d array, for me I went with more info, so I have an array of object, the object has 2 properties, one is student control (hold student id), and one is an array of grades for this student here is the loop I used to populate my form group

    createGradeForm(): void {
        this.gradeForm = this.formBuilder.group({
            gradeFields: this.formBuilder.array([])
        });
        //loop all students 
        for (let b = 0; b < this.studentList.length; b++) {
        // create a form group to hold student data 
            const userGroup = this.formBuilder.group({
                student: this.formBuilder.control(this.studentList[b].id),
                grades: this.formBuilder.array([])
            })
            //populate grades form array 
            for (let i = 0; i < this.examList.length; i++) {
                //Bonus, I added a validator for you, max value is the mark, please apply min and other validations on your own. 
                userGroup.get('grades').push(this.formBuilder.control('', [Validators.max(this.examList[i].marks)]));
            }
            // add the form group to your array 
            this.gradeFields.push(userGroup);
        } 
        //Check result here 
        console.log(this.gradeFields);
    
    }
    

    For your view, you will have nested ng-for, one to loop students rows, and one to loop through your grades (columns)

    <tbody>
        <!-- loop through the higher level array (students) --> 
        <ng-container *ngFor="let student of gradeFields.controls; let i = index" formArrayName='gradeFields'>
            <ng-container [formGroupName]='i'>
                <tr>
                    <th> {{ i + 1 }} </th>
                    <-- this is student control -->
                    <th> {{ student.get('student').value }} </th>
                    <-- loop through your grades array --> 
                    <th *ngFor="let grade of student.get('grades').controls; let j = index" formArrayName="grades">
                        <-- bonus here, I added an example for validation, add style to your css for .inValid to see when invalid -->
                        <input [class.inValid]='!grade.valid' type="number" [formControlName]="j">
                    </th>
                     <-- one more bonus, an example of calculation getTotal -->
                    <th>{{getTotal(i)}}</th>
                    <th>??</th>
                    <th>??</th>
                </tr>
            </ng-container>
        </ng-container>
    </tbody>
    

    Get Total Function

    //It takes the index of the student, get all grades, and accumlate them, the logic is wrong, please modify it, I just sum all grades, you have to add the logic
    getTotal(ind) {
        const studentGrades = this.gradeFields.value[ind].grades;
    
        return studentGrades.reduce((a, b) => Number(a) + Number(b));
    }
    

    working stackblitz here

    Read More about form arrays and reactive forms