1

I am trying to dynamically generate the following html table, as seen on the screenshot enter image description here

I was able to manually create the table using dummy data, but my problem is that I am trying to combine multiple data sources in order to achieve this HTML table structure.

SEE STACKBLITZ for the full example.

The Data looks like this (focus on the activities field):

    let data = {

id: '60bf06e6fc8f613117de1db9',
    activities: {
      '0': ['Power', 4, '', 2.5, '', 0, ''],
      '1': ['Attitude', 2, '', 3, '', 0, ''],
      '2': ['NR', 4.5, '', 1.5, '', 0, ''],
      '3': ['FMS', 4, '', 4, '', 0, ''],
      '4': ['Automation', 2.5, '', 2.5, '', 0, ''],
      '5': ['Path', 4.5, '', 2.5, '', 0, ''],
      '6': ['Systems', 2, '', 2.5, '', 0, ''],
      '7': ['Environment', 4.5, '', 2.5, '', 0, ''],
      '8': ['Planning', 2, '', 2.5, '', 0, ''],
      '9': ['Co-ordinate', 4.5, '', 3, '', 0, ''],
      '10': ['Prioritize', 2.5, '', 3, '', 0, ''],
      '11': ['Workload', 4.5, '', 2.5, '', 0, ''],
      '12': ['Crew', 4, '', 3, '', 0, ''],
      '13': ['ATC', 2.5, '', 3, '', 0, ''],
      '14': ['Identify', 4, '', 2.5, '', 0, ''],
      '15': ['Ass. Risk', 2, '', 4.5, '', 0, ''],
      '16': ['Checklist', 4, '', 2.5, '', 0, ''],
      '17': ['Analysis', 3, '', 3, '', 0, '']
    }}

The activities field has a total of 18 activities. Each one is identified by its id, and an array of activities. For instance '0': ['Power', 4, '', 2.5, '', 0, ''],. '0' represents the id of the activity Power. 4 represents Attempt1 - Grade & '' represents Attempt1 - Note; etc (Refer to the screenshot for clarification).

The complete list of activities is stored in a different variable/file and has the following structure.

Component.ts

        this.activities = [{
                "id": 0,
                "activity": "Power",
                "subject": "Control",
                "icon": "icon-link"
            },
            {
                "id": 1,
                "activity": "Attitude",
                "subject": "Control",
                "icon": "icon-link"
            },{...}]
    this.groupedActivities = customGroupByFunction(this.activities, 'subject');
this.colors = {Control: '#bfbfbf', 'AFCS': '#bfbfbf', ...}
    this.activitiesListKey = Object.keys(this.activitiesList);

Below is my html code.

        <table>
        <caption>Session Summary</caption>
        <tr>
          <th style="width: 40%"></th>
          <th style="width: auto"></th>
          <!-- Loop through AttemptCount variable
            to populate this heading -->
          <th style="width: auto" colspan="2" *ngFor="let attempt of attemptCounts(3)">Attempt {{attempt}}</th>
    
        </tr>
    
        <tr>
          <th style="width: 40%">Subject Grouping</th>
          <th style="width: auto"></th>
    
          <!-- Populate these heading using the formula
            AttemptCount * 2 (columns - Grade & Note) -->
          <ng-container *ngFor="let attemptLabel of attemptCounts(3)">
    
            <th style="width: auto">Grade</th>
            <th style="width: auto">Note</th>
          </ng-container>
        </tr>
    
        <!-- Loop through all Activity Subjects
        And create heading accordingly. 6 Main Subjects so far -->
    
        <!-- Next Subject -->
    
        <ng-container *ngFor="let id of activitiesListKey">
          <tr>
    
            <!-- Generate [Rowspan] = (Subject.Array.Length + 1) -->
            <th style="width: 40%;" rowspan="4" [style.background]="colors[id]">
              {{id}}</th>
    
            <!-- Loop through each Subject.Array Elements
            and populate -->
          <tr style="width: auto">
            <th>Power</th>
            <td>2.6</td>
            <td>Can Improve</td>
            <td>5.0</td>
            <td>Excellent</td>
            <td>4.5</td>
            <td>Regressed</td>
          </tr>
          <tr style="width: auto">
            <th>Attitude</th>
            <td>4.0</td>
            <td>Fantastic</td>
            <td>4.5</td>
            <td>Getting Better</td>
            <td>5.0</td>
            <td>Nice</td>
          </tr>
          <tr style="width: auto">
            <th>NR</th>
            <td>2.6</td>
            <td>Can Improve</td>
            <td>5.0</td>
            <td>Excellent</td>
            <td>4.5</td>
            <td>Regressed</td>
          </tr>
          <!-- </tr> -->
        </ng-container>
    
      </table>

N.B: attemptCounts(n) is simply a function that returns an array of n elements. for example attemptCounts(3) will return [0,1,2]

I am willing to change the structure of my data.activities if it is going to make the table easier to generate. So please if anyone has a solution that works with a different data model, please do share.

the values "Getting Better", "Excellent" are entered by the instructor from a form field. So they could be constants or not. Or even just empty strings. That will work as well.

Can someone please help me dynamically generate this table? I have been struggling with this for days now, and I seem not able to find a solution that works for me.

SO, Please help a friend in distress.

15
  • Hey thanks for your comment, but I do not think Material-table will help me achieve this specific architecture. I have looked into it, but I feel like this table is way to specific for mat-table to work in this instance. But thanks for your suggestion. Commented Jun 13, 2021 at 10:34
  • 1
    @GaurangDhorda, You could do something like this this.groupedActivities = customGroupByFunction(this.activities, 'activity'); Commented Jun 13, 2021 at 11:19
  • 1
    @AllJs stackblitz.com/edit/… take this stacblitz and fork it first, then add your all code in this stackblitz and then share your updated code link of stackblitz here. It would be more helpful to understand it Commented Jun 13, 2021 at 11:25
  • 1
    Here is the stackblitz @robert stackblitz.com/edit/angular-ivy-xkatki?file=src/app/…. I also updated the question with a link to stackblitz. Thanks Commented Jun 13, 2021 at 11:55
  • 1
    @AllJs here is demo stackblitz.com/edit/… check it out. Commented Jun 13, 2021 at 16:09

1 Answer 1

3

One possible way to make it work without changing the data structure.

First some Interfaces:

interface Activity {
  id: number;
  activity: string;
  subject: string;
  icon: string;
}

interface Attempt {
  grade: number;
  note: string;
}

interface ActivityAttemp {
  activityName: string;
  attempts: Attempt[];
}

interface ActivitiesBySubject {
  subject: string;
  activities: ActivityAttemp[];
}

Calculate Attempts count from data.activities:

attemptCounts: number[] = [];

const count =
  ((Object.values(this.data.activities)[0] as string[]).length - 1) / 2;
for (let i = 1; i <= count; ++i) {
  this.attemptCounts.push(i);
}

Before giving data to Angular to render it some pre-processing:

this.subjects.forEach((subject: string) => {
  this.activitiesBySubject.push({
    subject,
    activities: this.activities
      .filter((act: Activity) => act.subject === subject)
      .map((act: Activity) => {
        return {
          activityName: act.activity,
          attempts: this.getAttemptsForActivity(act.activity)
        };
      })
  });
});

The idea is to have all needed data in one place this way HTML template becomes much simpler:

<div style="overflow: auto">
  <table>
    <caption>Session Summary</caption>
    <tr>
      <th style="width: 40%"></th>
      <th style="width: auto"></th>
      <th style="width: auto" colspan="2" *ngFor="let attempt of attemptCounts">Attempt {{attempt}}</th>
    </tr>

    <tr>
      <th style="width: 40%">Subject Grouping</th>
      <th style="width: auto">Activity</th>

      <ng-container *ngFor="let attemptLabel of attemptCounts">
        <th style="width: auto">Grade</th>
        <th style="width: auto">Note</th>
      </ng-container>
    </tr>


    <ng-container *ngFor="let actsBySubj of activitiesBySubject">

      <tr>
        <td [attr.rowspan]="actsBySubj.activities.length + 1">
          {{ actsBySubj.subject }}
        </td>
      </tr>

      <tr *ngFor="let activity of actsBySubj.activities">
        <td>
          {{ activity.activityName }}
        </td>

        <ng-container *ngFor="let attempt of activity.attempts">

          <td>
            {{attempt.grade}}
          </td>
          <td>
            {{attempt.note}}
          </td>

        </ng-container>

      </tr>

    </ng-container>

  </table>
</div>

Working Stackblitz

Thanks @GaurangDhorda for initial Stackblitz.

UPDATE

To work with dropdown the processing has to be moved to a separate method:

  private calcTableData() {
    // reset
    this.attemptCounts = [];
    this.activitiesBySubject = [];

    // find exercise from "selectedExerciseId"
    const selectedExercise = this.data.find(
      (x: any) => x.id === this.selectedExerciseId
    );

    if (!selectedExercise) {
      return; // unable to find exercise
    }

    // calc attempt count eg.: 1, 2, 3
    for (let i = 1; i <= selectedExercise.attemptCount; ++i) {
      this.attemptCounts.push(i);
    }

    this.subjects.forEach((subject: string) => {
      this.activitiesBySubject.push({
        subject,
        activities: this.activities
          .filter((act: Activity) => act.subject === subject)
          .map((act: Activity) => {
            return {
              activityName: act.activity,
              attempts: this.getAttemptsForActivity(
                selectedExercise,
                act.activity
              )
            };
          })
      });
    });

    console.log(this.activitiesBySubject);
  }

This method uses a member variable selectedExerciseId to get the selected item. Also the color palette is moved to:

  colors: { [key: string]: string } = {
    Control: '#bfbfbf',
    AFCS: '#fac090',
    'Situational Awareness': '#c4bd97',
    'Leadership / Teamwork': '#d99694',
    Communication: '#c3d69b',
    'PB. Solving / Decision Making': '#b3a2c7'
  };

Event handler onChange and life cycle hook ngOnit just calls calcTableData:

  ngOnInit(): void {
    this.calcTableData();
  }

  onChange(e: Event) {
    this.calcTableData();
  }

Updated Stackblitz

Sign up to request clarification or add additional context in comments.

5 Comments

Hey @Robert Thanks for your answer. It works as expected. I really appreciate your help on this. My last would be this: How could I apply the color palette I previously created to each subject matter, using the setColorPalette() function. Thanks.
Thanks you. Here is what I did to apply the colors scheme to each subject <td [attr.rowspan]="actsBySubj.activities.length + 1" [style.background]="colors[actsBySubj.subject]"> {{ actsBySubj.subject }} </td>. All I added was the [style.background]="colors[actsBySubj.subject]"
Hey @Robert, please the question. I updated the question with a new issue. Dropdown to update the table. Here is the stackblitz link stackblitz.com/edit/angular-ivy-rnkym4?file=src/app/…. Thanks
@AllJs link is broken

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.