I am trying to dynamically generate the following html table, as seen on the screenshot
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.
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